`from`句でUnionにエイリアスを指定できるよう修正

rails/rails

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::UnionArel::Nodes::UnionAllArel::Nodes::IntersectArel::Nodes::Exceptの4つのノードタイプを対象とする新ブランチが追加され、opts.as(name)でエイリアスが適用されるようになりました。

テストにはSQLite3Adapterを除外する条件が付いています。これはSQLiteがFROM句でのUnionサブクエリの書き方に制限を持つためで、unless current_adapter?(:SQLite3Adapter)でPostgreSQLおよびMySQLでのみ実行されます。

設計判断

集合演算の全4ノードをまとめて1ブランチで処理する設計が採用されました。

UNIONのみを対象とするのではなく、UnionAllIntersectExceptも同一ブランチで扱っています。これらはいずれもArelの集合演算ノードであり、#asメソッドによるエイリアス付与という処理は共通です。個別に対応するより、潜在的に同じ問題を抱えうる関連ノードをまとめて修正することで、同種のバグの再発を防いでいます。

また、nameの解決をRelationブランチの外に出したことで、デフォルト値"subquery"の付与ロジックが重複せず一箇所に集約されています。エイリアス未指定時は"subquery"がデフォルトとして使われるため、Comment.select("subquery.*").from(union)のような記述も意図通りに機能します。

まとめ

本PRは、#fromが受け付けるオブジェクト型の増加に対してエイリアス処理の適用範囲が追いついていなかった実装の抜け穴を塞ぐ修正です。Arelの集合演算ノードすべてを同一ブランチで処理し、エイリアス解決ロジックを共通化したことで、UNIONを使ったクエリ最適化がActiveRecordの標準的なAPIで完結できるようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
f38dd1c4

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という構成が明確で、ガイドラインの要件をすべて満たしています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)とPR番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

ActiveRecordの内部実装やArelに関する知識を前提としており、専門的なエンジニアという対象読者に適合した内容です。

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

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

各セクション、各パラグラフが「総論→各論」の構造を持ち、トピックセンテンスが明確です。1段落1トピックの原則も守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内のコードブロック(変更前・変更後)は、提供されたDiff情報と完全に一致しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Arel::Nodes::Union」「集合演算ノード」「エイリアス」などの技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

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

エイリアス適用処理が欠落していた原因と、`case`文にブランチを追加して対応するという解決策の説明は、Diffの内容と一致しており技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコード変更によって裏付けられています。「設計判断」セクションもコードから論理的に導かれる洞察であり、ハルシネーションは検出されませんでした。

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

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

PR番号(#54829)や、コード内のクラス名・メソッド名などの固有名詞はすべて正確に記載されています。

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

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

記事のタイトル「`from`句でUnionにエイリアスを指定できるよう修正」は、PRのタイトル「Allow using aliases for unions in `from` clause」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

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

PR情報にないバージョンサポート状況やリリース日程などの外部知識の追加はありません。SQLiteに関する言及は、テストコードのdiffを説明するための妥当な補足です。

時間表現の正確性 ✓ PASS

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

「既に」や「将来」といった時間表現の歪曲はなく、事実を客観的に記述しています。