CTE列で `in_order_of` がNoMethodErrorを起こす問題を修正
ActiveRecord::QueryMethods#in_order_of が、CTE(共通テーブル式)から取得したカラムを対象にしたときに NoMethodError を発生させていた問題を解消しました。型情報が欠如している列に対しては型キャストをスキップし、例外を回避できるようになっています。
背景
in_order_of は指定されたカラムの型情報を元に値をキャスト してから IN 句を生成しますが、カラム名が解決できない場合 order_column が Arel::Nodes::SqlLiteral を返します。Arel::Nodes::SqlLiteral は type_caster メソッドを持たないため、キャスト処理で NoMethodError がスローされていました。CTEから取得した列はデータベース側で型情報が提供されないため、同様の例外が頻発していました。
この挙動は PR #44746 での整数範囲外エラー修正と同様に、型情報が不正確または欠如しているケースのハンドリングが不足していた ことに起因します。実際に、CTEを用いたテストで in_order_of が例外を投げる様子が再現され、バグとして報告されました。
技術的な変更
in_order_of の実装に 型キャストを条件付きで実行 するガードを追加しました。具体的には arel_column が Arel::Nodes::SqlLiteral であるかどうかを判定し、リテラルの場合は cast_values_for_in_order_of 呼び出しをスキップします。
@@
- values = cast_values_for_in_order_of(values, arel_column.type_caster)
+ unless arel_column.is_a?(Arel::Nodes::SqlLiteral)
+ values = cast_values_for_in_order_of(values, arel_column.type_caster)
+ end
この変更により、CTE列が SqlLiteral として扱われるケースでも例外が発生せず、空のキャスト結果がそのまま使用されます。values.empty? 判定は従来どおり保持され、空配列の場合は none! が返ります。
加えて、テストスイートに CTE列での in_order_of 動作を検証するケース を追加しました。テストはデータベースが CTE をサポートしているかを条件判定し、以下の流れを確認します:
- CTEで
formatカラムを別名book_formatとして取得 -
in_order_of(:book_format, order)で期待順にソート - 結果配列が指定順序と一致することをアサート
@@
- def test_in_order_of_with_array_values_with_nil
+ if ActiveRecord::Base.lease_connection.supports_common_table_expressions?
+ def test_in_order_of_with_column_from_cte
+ Book.destroy_all
+ Book.create!(format: "paperback")
+ Book.create!(format: "ebook")
+ Book.create!(format: "hardcover")
+
+ cte = Book.select(:id, format: :book_format)
+ order = ["hardcover", "paperback", "ebook"]
+
+ books = Book
+ .with(cte_books: cte)
+ .from("cte_books")
+ .select(:id, :book_format)
+ .in_order_of(:book_format, order)
+
+ assert_equal(order, books.map(&:book_format))
+ end
+ end
設計判断
型情報が取得できない列に対してはキャスト処理を省く という方針が採用されました。type_caster が存在しないことは実装上明らかであり、例外を防ぐための最小変更としてガード節を挿入した点が特徴です。新たな設定項目や API の拡張は行わず、既存コードに最小限の分岐を加えることで後方互換性を確保しています。
このアプローチは、「可能な限り既存の振る舞いを保ちつつエッジケースだけを特化」 する設計哲学に沿っています。CTE列が頻繁に使用されるユースケースを想定し、型情報が不在でも in_order_of が利用可能になることで、開発者は追加のラッパーや手作業のキャストなしに並び替えロジックを記述できます。
まとめ
CTEから取得したカラムが Arel::Nodes::SqlLiteral として扱われるケースで in_order_of が例外を投げていた問題を、型キャストの呼び出しを条件付きにするシンプルなガードで修正しました。テストが追加され、今後も CTE を利用した順序付けが安全に行えるようになりました。