`only_columns` 設定時にSELECTで列を明示列挙するよう修正

rails/rails

#55121 で導入された only_columnsignored_columns の対となる機能として設計されていましたが、実際のSQL生成では SELECT "table".* が発行され続けるバグがありました。本PRはこの不整合を1行の条件追加で修正します。

背景

only_columns は「このモデルが使用する列をホワイトリストで指定する」機能として #55121 で追加されました。ignored_columns がブラックリスト方式であるのに対し、only_columns はホワイトリスト方式の対称な設計として位置づけられています。

load_schema! 内では only_columns の設定が正しく反映され、columns_hash および column_names は指定した列のみに絞り込まれます。しかし build_select メソッドは列の明示列挙を行う条件として ignored_columns の有無と enumerate_columns_in_select_statements フラグしか参照しておらず、only_columns が考慮されていませんでした。その結果、以下のような不整合が発生していました。

class User < ApplicationRecord
  self.only_columns = %w[id name]
end

User.all.to_sql
# 期待: SELECT "users"."id", "users"."name" FROM "users"
# 実際: SELECT "users".* FROM "users"

User.first.attributes.keys
# 期待: ["id", "name"]
# 実際: ["id", "name", "email", "created_at", "updated_at"]

データベースが全列を返し、ロードされたレコードにも全属性が反映されてしまうため、only_columns の目的が完全に損なわれていました。

技術的な変更

ActiveRecord::QueryMethods#build_select の条件分岐に model.only_columns.any? を追加することで、only_columns 設定時にも列の明示列挙が行われるようになりました。

変更前:

def build_select(arel)
  if select_values.any?
    arel.project(*arel_columns(select_values))
  elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements
    arel.project(*model.column_names.map { |field| table[field] })
  else
    arel.project(table[Arel.star])
  end
end

変更後:

def build_select(arel)
  if select_values.any?
    arel.project(*arel_columns(select_values))
  elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements || model.only_columns.any?
    arel.project(*model.column_names.map { |field| table[field] })
  else
    arel.project(table[Arel.star])
  end
end

追加の投影ロジックは不要です。model.column_namesload_schema! で既に only_columns を反映した状態になっているため、この条件追加だけで正しい列集合がSELECT句に展開されます。

テストは既存の OnlyColumnsDeveloper フィクスチャモデルを再利用し、ignored_columns の同等テストの隣に追加されています。

test "only columns are enumerated in SELECT" do
  query = OnlyColumnsDeveloper.all.to_sql.downcase

  # not in only_columns
  assert_not query.include?("first_name")

  # in only_columns
  assert query.include?("name")
end

設計判断

build_select の条件式を対称に拡張する方式 が採用されました。

ignored_columnsonly_columns はスキーマフィルタリングの表裏として設計された機能であり、build_select においても同列に扱うべきという一貫性が判断の根拠です。別の実装アプローチとして投影ロジック自体を変更する案も考えられますが、column_names が既に正しく絞り込まれているという既存の設計を活かし、最小限の変更で対称性を回復しています。

SQLite3・PostgreSQL・MySQLの3種類のデータベースで動作確認されており、修正前は新規テストが失敗し、修正後は全テストが通ることも確認されています。

まとめ

本PRは only_columns のスキーマ制限がSQL生成まで伝播しないという設計の抜け穴を、条件式1箇所の追加で塞いだ修正です。column_names が既に正しい列集合を返しているという既存設計を活用することで、追加ロジックなしに ignored_columns との完全な対称性が実現されています。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という3部構成が明確に適用されており、ガイドラインに完全に準拠しています。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

ActiveRecordの内部実装に関する内容であり、専門知識を持つエンジニアを対象とした適切な技術レベルと表現で書かれています。

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

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

各セクションが「総論→各論→結論」で構成され、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。非常に読みやすい構成です。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロック(変更前、変更後、テストコード)は、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`only_columns`, `ignored_columns`, `build_select`, `load_schema!` などの専門用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「`model.column_names`が既に`only_columns`を反映している」という説明など、変更の背景と実装の技術的詳細がPR情報に基づき正確に解説されています。

事実の突合 ✓ PASS

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

記事内のすべての主張(バグの背景、修正内容、テスト、動作確認DBなど)は、PRのDescriptionやDiffによって裏付けられており、ハルシネーションは一切ありません。

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

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

PR番号(#57364, #55121)やクラス名・メソッド名などの固有名詞はすべて正確です。

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

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

記事のタイトルはPRのタイトル「Enumerate columns in SELECT when only_columns is set」の内容を的確に要約しており、主題と一致しています。

外部知識の正確性 ✓ PASS

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

記事の内容はすべて提供されたPR情報に基づいており、バージョンサポート状況などのPR外の知識は含まれていません。

時間表現の正確性 ✓ PASS

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

「[#55121]で導入された」といった時間表現は、PR Descriptionの記述と一致しており、正確です。