CTE列で `in_order_of` がNoMethodErrorを起こす問題を修正

rails/rails

ActiveRecord::QueryMethods#in_order_of が、CTE(共通テーブル式)から取得したカラムを対象にしたときに NoMethodError を発生させていた問題を解消しました。型情報が欠如している列に対しては型キャストをスキップし、例外を回避できるようになっています。

背景

in_order_of は指定されたカラムの型情報を元に値をキャスト してから IN 句を生成しますが、カラム名が解決できない場合 order_columnArel::Nodes::SqlLiteral を返します。Arel::Nodes::SqlLiteraltype_caster メソッドを持たないため、キャスト処理で NoMethodError がスローされていました。CTEから取得した列はデータベース側で型情報が提供されないため、同様の例外が頻発していました。

この挙動は PR #44746 での整数範囲外エラー修正と同様に、型情報が不正確または欠如しているケースのハンドリングが不足していた ことに起因します。実際に、CTEを用いたテストで in_order_of が例外を投げる様子が再現され、バグとして報告されました。

技術的な変更

in_order_of の実装に 型キャストを条件付きで実行 するガードを追加しました。具体的には arel_columnArel::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 をサポートしているかを条件判定し、以下の流れを確認します:

  1. CTEで format カラムを別名 book_format として取得
  2. in_order_of(:book_format, order) で期待順にソート
  3. 結果配列が指定順序と一致することをアサート
@@
-  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 を利用した順序付けが安全に行えるようになりました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
4bd86f38

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文→背景→技術的な変更→設計判断→まとめの流れが整っており、各要素が明確に記述されています。まとめは単なる繰り返しでなく、修正の意義と今後の影響を述べています。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しい形式ですが、PRリンクが `[PR #57758](URL)` となっており、仕様が求める `[#57758](URL)` の形式と完全に一致していません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容はRailsやActiveRecordに精通したエンジニア向けで、初心者向けの過度な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが『総論→各論→結論』の構成を保ち、段落はトピックセンテンスで始まり一段落一テーマで書かれています。段落長も適切で、空行で区切られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事に掲載されたコードスニペットは提供されたDiffと一致しており、ファイル名も正しいです。抜粋部分は文脈を失わない範囲で省略されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`type_caster`、`Arel::Nodes::SqlLiteral`、`CTE` などの用語はPRおよびDiffと一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明はPRの背景・目的と合致し、コード変更の意図と結果を正確に伝えています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の主張はすべてPRタイトル、Description、Diffで裏付けられており、推測や捏造はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号#57758 が正しく記載されています。その他の数値(例: 配列内容)もコード例と一致しています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPR「Fix `in_order_of` raising on CTEs」の日本語要約になっており、内容と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPRに記載された情報以外の外部知識を付加していません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現のずれはなく、PRの記述と一致しています。