`from`句でUnionにエイリアスを指定できるよう修正
ActiveRecord::Relation#fromにArel Unionノードを渡した際、第2引数のエイリアスが無視されていたバグが修正されました。これによりSomeModel.from(union, :some_models)のような記述が正しく動作するようになります。
背景
OR条件をUNIONに書き換えるクエリ最適化を行った際に、このバグが発見されました。SomeModel.from(union, :some_models)のように記述すると、PostgreSQLでmissing FROM-clause entry for table "some_models"というエラーが発生していました。
#fromメソッドはUnionを受け入れるものの、Arel::Nodes::UnionなどのArelノードが渡された場合のコードパスでエイリアスの適用処理が欠落していたことが原因です。Relationオブジェクトが渡された場合はopts.arel.as(name)でエイリアスが正しく適用される実装になっていましたが、Arelノード直接渡しのケースはそのままelseブランチに落ち、エイリアスが無視されていました。
技術的な変更
build_fromメソッドのcase文に新たなブランチが追加され、Arelの集合演算ノードに対してエイリアスが適用されるようになりました。
変更前:
def build_from
opts = from_clause.value
name = from_clause.name
case opts
when Relation
if opts.eager_loading?
opts = opts.send(:apply_join_dependency)
end
name ||= "subquery"
opts.arel.as(name.to_s)
else
opts
end
end
変更後:
def build_from
opts = from_clause.value
name = from_clause.name&.to_s || "subquery"
case opts
when Relation
if opts.eager_loading?
opts = opts.send(:apply_join_dependency)
end
opts.arel.as(name)
when Arel::Nodes::Union, Arel::Nodes::UnionAll,
Arel::Nodes::Intersect, Arel::Nodes::Except
opts.as(name)
else
opts
end
end
変更は2点あります。第一に、nameの解決ロジックがRelationブランチの内部からcase文の外に移動し、name = from_clause.name&.to_s || "subquery"として統一されました。第二に、Arel::Nodes::Union・Arel::Nodes::UnionAll・Arel::Nodes::Intersect・Arel::Nodes::Exceptの4つのノードタイプを対象とする新ブランチが追加され、opts.as(name)でエイリアスが適用されるようになりました。
テストにはSQLite3Adapterを除外する条件が付いています。これはSQLiteがFROM句でのUnionサブクエリの書き方に制限を持つためで、unless current_adapter?(:SQLite3Adapter)でPostgreSQLおよびMySQLでのみ実行されます。
設計判断
集合演算の全4ノードをまとめて1ブランチで処理する設計が採用されました。
UNIONのみを対象とするのではなく、UnionAll・Intersect・Exceptも同一ブランチで扱っています。これらはいずれもArelの集合演算ノードであり、#asメソッドによるエイリアス付与という処理は共通です。個別に対応するより、潜在的に同じ問題を抱えうる関連ノードをまとめて修正することで、同種のバグの再発を防いでいます。
また、nameの解決をRelationブランチの外に出したことで、デフォルト値"subquery"の付与ロジックが重複せず一箇所に集約されています。エイリアス未指定時は"subquery"がデフォルトとして使われるため、Comment.select("subquery.*").from(union)のような記述も意図通りに機能します。
まとめ
本PRは、#fromが受け付けるオブジェクト型の増加に対してエイリアス処理の適用範囲が追いついていなかった実装の抜け穴を塞ぐ修正です。Arelの集合演算ノードすべてを同一ブランチで処理し、エイリアス解決ロジックを共通化したことで、UNIONを使ったクエリ最適化がActiveRecordの標準的なAPIで完結できるようになります。