未知のenumキーや範囲外整数でNULL行が出現する `in_order_of` の挙動を修正

rails/rails

ActiveRecord::QueryMethods#in_order_of が、列挙型の未知キーや整数型の範囲外値を受け取ったときに、NULL 行を意図せず結果に混入させるバグを修正しました。これにより、Enumerable#in_order_of と同様に、表現できない値は単に無視されるようになります。

背景

in_order_of は渡された値を シリアライズ し、NULL 判定とともに WHERE … OR column IS NULL 句を生成していました。シリアライズに失敗した場合は nil が返り、呼び出し側が意図した nil と区別できなかったため、範囲外整数や未知の enum キーが NULL 行としてマッチしてしまっていました。結果として、Book.in_order_of(:status, [:written, :bogus, :proposed]) が NULL ステータスのレコードを :bogus の位置に混入させていました。

技術的な変更

in_order_of の内部ロジックを cast_values_for_in_order_of ヘルパーメソッドに置き換え、シリアライズできない値を明示的に除外するようにしました。変更前は次のようにマッピングしていました。

values = values.map do |value|
  if value.is_a?(Array)
    value.map do |current_value|
      caster.serialize(current_value) if caster.serializable?(current_value)
    end
  else
    caster.serialize(value) if caster.serializable?(value)
  end
end

変更後は以下のように簡略化し、空リストの場合は spawn.none! を即座に返します。

values = cast_values_for_in_order_of(values, arel_column.type_caster)
return spawn.none! if values.empty?

新たに導入された cast_values_for_in_order_of は、シリアライズ成功時はその結果を、失敗時は bad_value と呼ばれる特殊オブジェクトでマークし、最後にそれらを除去します。配列内の空要素や bad_value はすべて reject され、結果的に表現できない値はクエリに現れません。

def cast_values_for_in_order_of(values, type_caster)
  bad_value = Symbol # sentinel that cannot be a valid column value

  values = values.map do |value|
    if value.is_a?(Array)
      cast_values_for_in_order_of(value, type_caster).without(bad_value)
    else
      serialized = type_caster.serialize(value) if type_caster.serializable?(value)
      if value.nil?
        nil
      elsif !serialized.nil?
        serialized
      else
        bad_value
      end
    end
  end

  values.reject { |v| v == bad_value || (v.is_a?(Array) && v.empty?) }
end

設計判断

今回の修正は Enumerable#in_order_of の挙動と完全に整合させる ことを設計上の指針としています。表現できない値は単に除外し、nil(呼び出し側が明示的に渡した)だけが NULL 行とマッチするようにすることで、期待通りに「無視」されるというドキュメント通りの動作を実現しました。空リストへの対応として spawn.none! を返す方針は、既存の空配列入力と同等の挙動を保ち、SQL の CASE END 生成エラーを防止しています。

まとめ

in_order_of が未知の enum キーや範囲外整数を NULL 行として誤分類していた問題を、シリアライズ失敗値の除外ロジックに置き換えることで解消しました。これにより、NULL データが意図せず結果に混入することはなくなり、ドキュメントと実装の整合性が保たれます。追加テストにより、enum、整数、空リストすべてで期待通りの動作が確認されています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
a15f4944

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断(任意)、まとめが揃っており、総論→各論→結論の構成が明確です。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは `ruby:ファイルパス` の形式で正しく記述され、PRへのリンクは `[PR #57607](URL)` の形式で正しくハイパーリンク化されています。

対象読者への適合性 ✓ PASS

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

対象はActiveRecordやRailsの内部実装に詳しいエンジニア向けで、初心者向けの過度な解説はありません。

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

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

各セクションは総論・各論・結論のパラグラフで構成され、段落はトピックセンテンスで始まり、1段落1トピック、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは提供されたDiffと完全に一致しており、追加・削除されたロジックが正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語(enum、serializer、bad_value、spawn.none! など)はPRで使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

バグの原因、修正内容、影響範囲がPRの説明と整合しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事の全ての主張はPRのタイトル、説明、Diff、テストコードで裏付けられており、ハルシネーションは見られません。

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

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

PR番号 #57607 が正しく記載されています。その他数値(例: 追加テスト件数)はPRのdiffから確認可能です。

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

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

日本語タイトルはPRの要旨を正確に反映しており、内容と一致しています。

外部知識の正確性 ✓ PASS

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

記事内にPRに記載されていないバージョン情報やリリース日程等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現は使用されておらず、PRの記述と矛盾する表現もありません。