Follow-up to #625.
#625 fixed closed VLP patterns (same variable both endpoints) for lower bound >= 1 by emitting t.start_id = t.end_id on the recursive-CTE outer query. The *0..N case is currently made to fail loud (UnsupportedFeature) rather than return a silent undercount.
Why loud, not fixed: a *0..N recursive CTE enforces NODE-uniqueness (NOT has(vp.path_nodes, end_id)) — the zero-hop base case has no edges to seed path_edges, so it cannot use edge-uniqueness (documented in variable_length_cte.rs, #598 part 2). Node-uniqueness forbids a walk from ever revisiting any node including its start, so no hop>0 row can satisfy start_id = end_id. Adding the cycle constraint therefore matched ONLY the zero-length self rows: (a)-[:FOLLOWS*0..2]->(a) returned 8 where the true cycle count is 14 (8 zero-length self + 6 real 2-cycles); undirected returned 8 vs true 20.
Repro (social benchmark, currently errors):
MATCH (a:User)-[:FOLLOWS*0..2]->(a) RETURN count(*) -- want 14
MATCH (a:User)-[:FOLLOWS*0..2]-(a) RETURN count(*) -- want 20
Fix needed: when a closed pattern (left_connection == right_connection) has lower bound 0, route the recursive arm to EDGE-uniqueness (path_edges) instead of node-uniqueness, so real cycles survive. The zero-hop base has empty path_edges (CAST([] AS Array(...))), and hops>=1 seed normally — need to confirm this doesn't hit the base/recursive column-mismatch the #598 note warns about (which was about seeding path_edges only in the base). Result should be: all zero-length self rows (start=end trivially) UNION real cycles (hop>=1 returning to start, edge-unique).
Workaround today: use lower bound >= 1 (*1..N) to count cycles.
shortestPath *0.. self is correctly handled (exempt from the guard) — node-uniqueness is right there and the zero-length path is the shortest self-path.
Follow-up to #625.
#625 fixed closed VLP patterns (same variable both endpoints) for lower bound >= 1 by emitting
t.start_id = t.end_idon the recursive-CTE outer query. The*0..Ncase is currently made to fail loud (UnsupportedFeature) rather than return a silent undercount.Why loud, not fixed: a
*0..Nrecursive CTE enforces NODE-uniqueness (NOT has(vp.path_nodes, end_id)) — the zero-hop base case has no edges to seedpath_edges, so it cannot use edge-uniqueness (documented invariable_length_cte.rs, #598 part 2). Node-uniqueness forbids a walk from ever revisiting any node including its start, so no hop>0 row can satisfystart_id = end_id. Adding the cycle constraint therefore matched ONLY the zero-length self rows:(a)-[:FOLLOWS*0..2]->(a)returned 8 where the true cycle count is 14 (8 zero-length self + 6 real 2-cycles); undirected returned 8 vs true 20.Repro (social benchmark, currently errors):
Fix needed: when a closed pattern (
left_connection == right_connection) has lower bound 0, route the recursive arm to EDGE-uniqueness (path_edges) instead of node-uniqueness, so real cycles survive. The zero-hop base has emptypath_edges(CAST([] AS Array(...))), and hops>=1 seed normally — need to confirm this doesn't hit the base/recursive column-mismatch the #598 note warns about (which was about seedingpath_edgesonly in the base). Result should be: all zero-length self rows (start=end trivially) UNION real cycles (hop>=1 returning to start, edge-unique).Workaround today: use lower bound >= 1 (
*1..N) to count cycles.shortestPath
*0..self is correctly handled (exempt from the guard) — node-uniqueness is right there and the zero-length path is the shortest self-path.