CPKモデルでの `eager_load` + `select` 併用時に不正なSELECT句が生成されるバグを修正
Composite Primary Key(CPK)を持つモデルに対して eager_load と select を同時に使用すると、SQLのSELECT句に配列を文字列化した不正なカラム名が混入するバグを修正しました。
背景
複合主キーを持つモデルで eager_load(:association).select("...") を呼び出すと、生成されるSQLのSELECT句に "posts"."[""id"", ""other_id""]" のような不正なフィールドが含まれていました。これは #57296 で報告されたバグで、単独の eager_load や select では問題が発生せず、両者を組み合わせた場合にのみ顕在化していました。
問題の根本は、JoinDependency がSELECT句に追加すべき主キーカラム名を決定する処理にありました。複合主キーモデルでは primary_key が配列を返すため、その配列がそのまま文字列化されてカラム名として扱われてしまっていました。
技術的な変更
修正箇所は activerecord/lib/active_record/associations/join_dependency.rb の aliases メソッド内、わずか2行です。
変更前:
column_names = if join_part == join_root && !join_root_alias
primary_key = join_root.primary_key
primary_key ? [primary_key] : []
else
join_part.column_names
end
変更後:
column_names = if join_part == join_root && !join_root_alias
Array(join_root.primary_key)
else
join_part.column_names
end
変更前のコードでは、primary_key が nil の場合に備えて primary_key ? [primary_key] : [] という三項演算で処理していました。単一主キーの場合は primary_key が文字列(例: "id")を返すため ["id"] となり正常に動作しますが、CPKモデルでは primary_key が配列(例: ["id", "other_id"])を返すため、[["id", "other_id"]] という配列のネストが生じ、後続の処理で不正なカラム名として展開されていました。
修正後の Array() は、Rubyの Kernel#Array を活用したシンプルな変換です。nil を渡せば []、文字列を渡せば ["id"]、配列を渡せばそのまま ["id", "other_id"] が返るため、単一主キー・複合主キー・主キーなしの3パターンを1行で正しく処理できます。
テストには activerecord/test/cases/associations/eager_test.rb に新規ケースが追加されました。Cpk::Order モデルを使って eager_load(:order_agreements).select("cpk_orders.status") を実行し、アソシエーションが正しくロードされることを確認しています。
設計判断
Array() による単一表現への統一が今回の核心的な設計判断です。
変更前のコードは「nil ガード」と「配列へのラップ」を手動で行っており、primary_key の返り値の型(nil / 文字列 / 配列)に対して明示的に分岐していませんでした。Array() への置き換えは、この暗黙的な型の前提を取り除き、CPKで導入された配列返却という新たな型バリアントを自然に扱える形に整理しています。変更行数が2→1行に削減される副次効果もありますが、主眼はロジックの正確性にあります。
まとめ
Kernel#Array の多態的な変換特性を活用することで、単一主キー・複合主キー・主キーなしの全ケースを1行で正しく処理できるようになりました。本修正により、CPKモデルに対して eager_load と select を組み合わせたクエリが正常に動作するようになります。