`only_columns` 設定時にSELECTで列を明示列挙するよう修正
#55121 で導入された only_columns は ignored_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_names は load_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_columns と only_columns はスキーマフィルタリングの表裏として設計された機能であり、build_select においても同列に扱うべきという一貫性が判断の根拠です。別の実装アプローチとして投影ロジック自体を変更する案も考えられますが、column_names が既に正しく絞り込まれているという既存の設計を活かし、最小限の変更で対称性を回復しています。
SQLite3・PostgreSQL・MySQLの3種類のデータベースで動作確認されており、修正前は新規テストが失敗し、修正後は全テストが通ることも確認されています。
まとめ
本PRは only_columns のスキーマ制限がSQL生成まで伝播しないという設計の抜け穴を、条件式1箇所の追加で塞いだ修正です。column_names が既に正しい列集合を返しているという既存設計を活用することで、追加ロジックなしに ignored_columns との完全な対称性が実現されています。