fix(cypher): enforce variable-length path semantics - #883
Conversation
DeusData
left a comment
There was a problem hiding this comment.
Thanks for this, @jstar0 — the semantics here are exactly right. The node-variable unification (matching the fixed-length #627 behavior) and the no-repeated-relationship "trail" semantics are both correct, and the two regression tests are genuine guards: they fail on the old executor and pass only on the fix. Security, scope, and DCO all check out.
One blocking issue before we can merge, on the store.c change to cbm_store_bfs.
Pulling edge_path into the recursive CTE row changes the UNION dedup key from (node_id, hop) to (node_id, hop, edge_path). Because edge_path is distinct per path, UNION no longer collapses the many paths that reach a node — the CTE now enumerates every simple path up to max_depth instead of doing a bounded node-BFS. On a hub-heavy graph at depth 10 that's on the order of b^d intermediate rows, each carrying a growing TEXT path, and the outer ORDER BY bfs.hop LIMIT N can't rein it in (SQLite materializes the full CTE before it orders/limits).
Why this reaches beyond var-length Cypher: cbm_store_bfs is shared — it also backs the trace_call_path MCP tool, whose depth is client-controlled (and currently unclamped). So this turns a polynomial traversal into a potential exponential blow-up on a widely-used, untrusted-input path.
The PR notes the expensive-expansion guard is deferred — the catch is that this change is the amplifier that guard is meant to contain, so merging it now ships the blow-up ahead of its bound. Could you fold the bound into this same PR? A hard cap on the number of enumerated paths / CTE rows inside the recursion (or the deferred guard itself) would do it. Happy to think through the shape with you.
Once that's in, this is good to go — the correctness work is solid. Thanks again.
313059b to
85c32f3
Compare
|
Thanks — the two Cypher-level semantics fixes are right and well-tested: repeated-variable unification in One structural concern blocks the merge as-is: the edge-path tracking lives in the shared
Suggested shape: keep Happy to merge once the trail behavior is scoped to the Cypher path executor — the binding-unification part could even land on its own if you want to split it. |
85c32f3 to
6040d8a
Compare
|
Addressed the traversal review and rebased onto current
Local verification: |
6040d8a to
8db4cf1
Compare
|
Reviewed in depth — we're adopting the trail semantics. Thank you for this; the repeated-node-var unification and self-loop edge-reuse rejection are genuine openCypher-correctness fixes that our current shortest-path var-length mode gets wrong, and I especially appreciate that you scoped it cleanly: the new One thing before merge: the PR is CONFLICTING against current main. #797 (shortest-path var-length + advertised depth clamp) landed since you opened this — it reworked the same
Once it's rebased and green I'll re-verify the behavioral change end-to-end (result multiplicity on a multi-path fixture + the self-cycle case) and merge. Genuinely nice work on one of the subtler parts of the query engine. |
8db4cf1 to
d8c7c9a
Compare
|
Reviewed in depth. There is a genuinely good fix in here that I would like to take, and a much larger change riding alongside it that is a maintainer decision rather than a bug fix. Let me separate them clearly, because I think the framing matters more than the verdict. First, you are right about openCypher. With relationship-uniqueness-only semantics, But main's behaviour is a deliberate divergence, not an oversight. Commit That is a maintainer call, so I have taken it upstream rather than deciding it in review. The separable fix I do want. Your repeated-variable unification in The blast-radius care in this PR deserves credit too. Keeping the shared BFS, If the trail direction is taken, two things would be required first, and they are the two properties this codebase most deliberately protects:
Two smaller notes: per-(node,hop) rows change non- To be concrete about what happens next: the unification fix I would like as its own PR now. The semantics question is with the maintainer and I will come back to you with a real answer either way. |
|
Direction call resolved, and it goes your way: we're adopting trail semantics. Thank you for the patience while that sat — and my apologies for the previous review, which asked you to split out just the repeated-variable fix and take the rest to a maintainer decision. That decision has now been made, so please disregard the split request; the larger change is wanted. The reasoning, since you argued the openCypher case and deserve to know it landed: the earlier choice to keep variable-length traversal on shortest-path semantics (closing #797) was made for scale on call graphs, not because relationship-uniqueness was thought wrong. Your point stands that a query which reuses an edge to manufacture a longer path is returning something that isn't a path. Correct-by-default wins, with the blowup managed rather than avoided by approximation. Your isolation work is what makes that affordable: keeping Three gaps to close before it lands. Each is small, and each is a case where the current version could be quietly wrong rather than loudly wrong:
One small honesty note on the tests, and it's a compliment rather than a complaint: You'll also need a rebase — If you'd rather hand it off at this point, say so and I'll take the three fixes on with you credited as co-author; you've already done the hard part and the design thinking. But you've reworked this once already and done it well, so it's yours first if you want it. |
Signed-off-by: King Star <mcxin.y@gmail.com>
Signed-off-by: King Star <mcxin.y@gmail.com>
Signed-off-by: King Star <mcxin.y@gmail.com>
Signed-off-by: King Star <mcxin.y@gmail.com>
Signed-off-by: King Star <mcxin.y@gmail.com>
d8c7c9a to
12b7886
Compare
|
Rebased onto current upstream main at dfe67cc and pushed signed head b3d31ca. Addressed the three requested trail-budget issues:
Verification on the exact head:
|
Signed-off-by: King Star <mcxin.y@gmail.com>
|
The exact-head workflow The only failure is the Windows guard I attempted to rerun the failed job, but GitHub rejected the request because the contributor lacks repository admin rights. Could a maintainer rerun |
|
Rerun kicked off — and you were right not to call it a baseline failure without proof, so let me give you the proof.
The harness kills daemons with So: unrelated to Cypher, unrelated to your traversal changes, and not something you could have fixed. Thank you for flagging it precisely instead of hand-waving it. On the three gaps — (b) and (c) are closed, and I verified both rather than taking the summary on trust. (c) ordering — (b) truncation reaching the API — the whole chain is there: (a) is where I want to keep talking, and I want to be fair about it first. You took the depth-aware budget, which was the parenthetical alternative I offered — my words were "a cap on distinct nodes visited (or a depth-aware budget)" — so this is a legitimate reading of the ask, not a dodge, and you said plainly in your comment that that is what you did. Two things I would still like closed, one of which I think matters more than the original point:
One small leftover: Fix those and I will merge. Thank you for staying with this through a direction reversal that was my fault, not yours — the semantics call you argued for from the start is the one we ended up at. |
|
Rerun came back green — passed on the same head with no code change, which confirms the flake reading. Your CI is clean; nothing there for you to chase. That leaves only the four review items from my last comment, of which the truncation-determinism one is the one I care most about. |
|
Closing the loop: the Windows startup-coordination fix has merged as Notably it merged with |
|
Addressed the remaining review items in signed-off head
Focused verification after the change:
The hosted matrix is rerunning on the new head. |
Signed-off-by: King Star <mcxin.y@gmail.com>
f735088 to
7583376
Compare
DeusData
left a comment
There was a problem hiding this comment.
Approving — this supersedes my stale CHANGES_REQUESTED from 07-05, which was against a head that no longer exists.
I re-verified the current head (7583376f) rather than relying on the earlier reads. All three items from the 20th are closed: the row cap is non-silent (truncated flag plus a warning surfaced through the Cypher result and out to query_graph), the ordering is deterministic with a full (hop, node_id, edge_path) tie-break in the recursion and ORDER BY hop, n.id on the outer query, and both follow-ups landed too — the hub-scale test at 4,100 fan-out against a 1,602-row depth-2 budget, and the deterministic-truncation coverage.
Two things I checked specifically because they were the risk in the original design:
The blast radius is contained to Cypher. The shared cbm_store_bfs SQL is byte-identical — still MIN(hop) GROUP BY, still ORDER BY hop, n.id — so trace_path, neighbours and impact analysis keep their existing behaviour, and store_bfs_reachability_is_not_trail_capped pins that at 4,200 nodes. Scoping the trail tracking into its own cbm_store_bfs_trail rather than threading it through the shared BFS was the right call.
The tests are binding, not decorative. The issue797 regression genuinely fails on main's shortest-path engine — that is the actual semantics bug, not a restatement of the fix — and the truncation-warning test fails on main because the warning is never set.
Accepting the residual, on the record: under hostile node-id orderings a deep match can still be truncated at hub scale, when the deep branch's node id sorts after ~4,096 hub siblings. That is bounded by the depth clamp, the per-expansion row cap and the outer LIMIT, and — the part that matters — it is now loud rather than silent. Accepted as-is.
On the substance: you argued the openCypher relationship-uniqueness semantics from the first comment, held that position through a direction reversal when I had deliberately diverged in #797, and then executed every review round precisely, including splitting the trail tracking out and dropping the regression test that had landed upstream in the meantime. That is a genuinely well-run contribution and the engine is more correct for it.
Thank you — merging now.

What does this PR do?
Part of #797.
This fixes two variable-length Cypher path semantics and bounds the traversal expansion introduced by the trail check:
MATCH (f)-[:CALLS*1..2]->(f)only matches paths that return to the same node;cbm_store_bfs_trailcaps rows admitted to its recursive CTE, so tracking edge ids cannot enumerate an unbounded number of simple paths before the outerLIMITwhile shared BFS keeps its reachability behavior.Summary
Variable-length Cypher paths now follow the same binding and simple-path semantics as the fixed-length executor cases covered by the existing tests, while shared BFS keeps its node-reachability behavior and Cypher trail expansion has a hard recursive-row bound.
Changes
cbm_store_bfs_trail.Verification
make -f Makefile.cbm test make -f Makefile.cbm lint-ciChecklist
git commit -s) — required, CI rejectsunsigned commits (DCO, see CONTRIBUTING.md)
make -f Makefile.cbm test)make -f Makefile.cbm lint-ci)