From edca75457ec554e9c43bc4e4dcfaf482f87a1a5d Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Tue, 9 Jun 2026 17:18:24 +0200 Subject: [PATCH] fix(engine): restrict anti-join bulk fast path to one-hop expands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bulk_anti_join_applies accepted any single Expand, but try_bulk_anti_join_mask decides via the CSR has_neighbors one-hop existence check — wrong for multi-hop negations. Require min_hops==1 && max_hops==1 in the predicate; anything else falls to the slow path, whose inner Expand runs the real bounded traversal. Turns the multi-hop regression green; one-hop anti-joins unchanged. --- crates/omnigraph/src/exec/query.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/omnigraph/src/exec/query.rs b/crates/omnigraph/src/exec/query.rs index c5c0b12..5bc18f2 100644 --- a/crates/omnigraph/src/exec/query.rs +++ b/crates/omnigraph/src/exec/query.rs @@ -1558,8 +1558,15 @@ async fn hydrate_nodes( fn bulk_anti_join_applies(inner_pipeline: &[IROp], outer_var: &str) -> bool { matches!( inner_pipeline, - [IROp::Expand { src_var, dst_filters, .. }] - if src_var == outer_var && dst_filters.is_empty() + [IROp::Expand { src_var, dst_filters, min_hops, max_hops, .. }] + if src_var == outer_var + && dst_filters.is_empty() + // `has_neighbors` is a ONE-hop existence test, so the fast path + // is valid only for a single-hop expand. Multi-hop negations + // (e.g. `not { $p knows{2,2} $x }`) fall to the slow path, whose + // inner Expand runs the real bounded traversal. + && *min_hops == 1 + && (*max_hops).unwrap_or(1) == 1 ) }