Arel Or ノードのネストを回避してスタックオーバーフローを防止

rails/rails

Arelの Or ノード を N項演算として適切に扱う改善により、深いネスト構造によるスタックオーバーフローが回避されるようになりました。この変更により、大量のRange条件を含むクエリでも SystemStackError が発生しにくくなります。

背景

Arelの Or ノード は過去にバイナリ(2項)からN項に変更されましたが、一部のコードパスがこの変更に追従していませんでした。具体的には、where(bar: ranges) のように大量のRange配列を渡すと、各Rangeが BETWEEN 条件に変換され、それらが OR で結合されます。このとき、バイナリツリー構造のネストが深くなり、約2000個のRangeで SystemStackError: stack level too deep が発生していました #56869

# 問題を再現するコード
ranges = (0...2000).map { |i| (Date.new(2024, 1, 1) + i).all_day }
Foo.where(bar: ranges).to_sql  # SystemStackError が発生

バイナリツリーでは Or(left, Or(left, Or(...))) のように右側が再帰的にネストし、ArelツリーがRubyスタックを使い果たす原因となっていました。

技術的な変更

本PRでは、Or ノードをN項として扱うよう2つのコードパスを修正しました。

ArrayHandlerの修正

変更前:

array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) }
array_predicates.inject(values_predicate, &:or)

変更後:

array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) }
values_predicate.or(
  Arel::Nodes::Grouping.new Arel::Nodes::Or.new(array_predicates)
)

inject(&:or) による逐次的なORの連結をやめ、Arel::Nodes::Or.new(array_predicates) で全ての述語を一度に渡すように変更されました。これにより、バイナリツリーの深いネストが解消されます。

Predicationsの修正

変更前:

def grouping_any(method_id, others, *extras)
  nodes = others.map { |expr| send(method_id, expr, *extras) }
  Nodes::Grouping.new nodes.inject { |memo, node|
    Nodes::Or.new([memo, node])
  }
end

変更後:

def grouping_any(method_id, others, *extras)
  nodes = others.map { |expr| send(method_id, expr, *extras) }
  Nodes::Grouping.new Nodes::Or.new(nodes)
end

grouping_any メソッドでも同様に、inject による段階的な構築から、全ノードを一度に渡す方式に変更されています。この変更により、matches_anynot_in_any などのメソッド経由でも浅いツリーが生成されます。

設計判断

PRでは「Or was made Nary so that it could have N children instead of just 2」と述べられており、Or ノードのN項対応は既存の変更であることが示されています。本PRは、この既存機能を活用していなかったコードパスを修正するものです。

inject を使った段階的な構築 から 一括での配列渡し への変更は、実装の一貫性を高める判断といえます。Or ノードの内部実装がN項をサポートしている以上、それを活用することでツリーの深さを O(n) から O(1) に削減できます。Rubyのスタックサイズには限界があるため、深いネストの回避はシステムの安定性に直結します。

まとめ

本PRは、Arel Or ノードのN項対応を活用してツリー構造を浅く保つことで、大量のRange条件でのスタックオーバーフローを防止しました。inject による段階的構築をやめ、全述語を一度に渡す方式に統一することで、コードの一貫性とシステムの安定性が向上しています。この変更により、数千件規模のRange配列を含むクエリも安全に実行できるようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されており、非常に分かりやすい構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)およびGitHubのPR・Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Arel、SystemStackErrorなどの専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合した内容です。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiff情報と完全に一致しており、変更点が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「N項演算」「バイナリツリー」「Arel Or ノード」などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「injectによる逐次的な構築が深いネストを生む」という原因と、「全ノードを一度に渡すことでネストを浅くする」という解決策の説明が技術的に正確で論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(OrノードがN項であった背景、関連Issueなど)は、PRのDescriptionや関連情報によって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#56880)および関連Issue番号(#56869)が正確に記載されています。

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

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

記事のタイトル「Arel Or ノードのネストを回避してスタックオーバーフローを防止」は、PRのタイトル「Avoid nesting Arel Or Nodes」の内容を的確に反映しています。

外部知識の正確性 ✓ PASS

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

記事には、PR情報に記載のないバージョンサポート状況やリリース日程などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

「Or ノードは過去に...変更されました」といった時間表現は、PR Descriptionの "Or was made Nary" と整合しており、正確です。