Arel Or ノードのネストを回避してスタックオーバーフローを防止
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_any や not_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配列を含むクエリも安全に実行できるようになります。