`in_order_of` で範囲外整数を渡した際の `RangeError` を修正
ActiveRecord::QueryMethods#in_order_of に範囲外の整数を渡すと発生していた ActiveModel::RangeError を修正し、Enumerable 版と同様に無効な値を無視する挙動に統一しました。
背景
#44745 として報告されたこのバグでは、4バイト整数の上限を超えた値を in_order_of に渡すと例外が発生していました。9999999999999 のような大きな整数はもちろん、文字列として渡した "9999999999999" でも同様に ActiveModel::RangeError: 9999999999999 is out of range for ActiveModel::Type::Integer with limit 4 bytes が発生します。
この問題の根本原因は #43322 の変更にあります。#43322 はenum(文字列)を整数カラムと比較できるよう type_cast_for_database を導入しましたが、この実装はサポート外の値も一律でキャストしようとするため、範囲外整数では例外を投げてしまっていました。一方、Enumerable の in_order_of は範囲外の値を単に無視するため、ARとEnumerableで挙動が異なるという一貫性の問題も存在していました。
技術的な変更
query_methods.rb の in_order_of メソッドで、type_cast_for_database の一律呼び出しを caster.serializable? による条件付きシリアライズに置き換えました。
変更前:
values = values.map { |value| model.type_caster.type_cast_for_database(column, value) }
arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
変更後:
if column.is_a?(Arel::Nodes::SqlLiteral)
arel_column = column
else
arel_column = order_column(column.to_s)
caster = arel_column.type_caster
values = values.map do |value|
caster.serialize(value) if caster.serializable?(value)
end
end
caster.serializable?(value) が false を返す値は nil にマップされます。これにより、範囲外の整数や変換不能な値は ORDER BY CASE 式の対象から実質的に除外されます。この手法はすでに Arel::Nodes::HomogeneousIn や Arel::Visitors::ToSQL でも採用されており、コードベース内での一貫したアプローチです。
あわせて field_ordered_values_test.rb にテストが追加されました。Post.type_for_attribute(:id).send(:max_value) で上限値を取得し、それを含む配列を渡しても例外が発生せず、範囲内の有効なIDのみ返ることを検証しています。
設計判断
serializable? による事前チェックを挟んでキャストをスキップする方式が採用されました。
SqlLiteral カラムの場合はキャスト処理をそもそも行わないよう条件分岐も整理されています。変更前は三項演算子で arel_column の生成とvaluesのマップを同じ行に並べており、SqlLiteral の場合でも type_cast_for_database が呼ばれる実装になっていました。変更後はブロックを分離することで、SqlLiteral 時はキャストをスキップするという意図が明示的になっています。
無効な値をエラーにするのではなく無視する設計は WHERE id IN (...) の挙動(Post.where(id: [1, 9999999999999]) は範囲外を無視する)とも整合しており、Rails全体で一貫したセマンティクスを持たせる意図が読み取れます。
まとめ
この修正は in_order_of のARとEnumerable実装間の挙動の差異を解消し、無効な値を無視するという一貫したセマンティクスを実現します。serializable? チェックの導入により例外を防ぎながら、既存の #43322 で実現したenum文字列のキャスト機能も維持されています。