fix(vlp): #1100 bare-node identity in a VLP filter predicate - #1102
Merged
Conversation
A predicate like `WHERE friend <> root` / `friend = root` compares node IDENTITY, but both sides are BARE node aliases (`RenderExpr::TableAlias`). In a variable-length path this predicate is categorized to the recursive CTE's WHERE, where the aliases are not in scope (its node columns are `start_node`/`end_node`). `render_expr_to_sql_string` renders a bare `TableAlias` as the literal alias name, emitting `friend != root` inside the CTE body → ClickHouse Code 47. The `.id` property-access form (`friend.id <> root.id`) already resolves correctly through the existing property- and alias-mapping machinery (→ `end_node.id != start_node.id`). So normalize the bare-node-identity form into the `.id` form for the two VLP endpoint aliases, right after the predicate is converted to RenderExpr and before categorization. `.id` is Cypher's generic node-identity synonym, resolved schema-appropriately downstream (renamed/composite ids included), so no schema lookup is needed at this seam. The normalization is tightly guarded: it fires only when BOTH operands are bare node aliases AND both are the VLP's own endpoint aliases; it recurses through AND/OR/NOT so a node-identity conjunct nested beside property filters is reached without disturbing its siblings. Non-VLP node-identity still flows through the outer-SELECT handler (to_sql_query.rs, #1076), untouched. Unblocks LDBC IC9 and IC5 end-to-end (both filter `WHERE NOT friend = root` on a `KNOWS*1..2` VLP): verified executing live against LDBC SF1 (ClickHouse 26.7) — IC9 returns friend/message rows ordered by date, IC5 returns forum post counts. Combined with the merged re-match projection fix (#1101), IC9/IC5 now run. The remaining #1100 shapes (IS2 compound-alias endpoint, IC10 pattern predicate, IC3 Code 62, BI12 endpoint property projection) have distinct root causes and stay tracked in #1100. Regression: full `cargo test` green (1738 lib + integration); ratchet green; fmt/clippy clean. 3 new golden tests (bare identity `<>`, `=` nested in AND with a surviving sibling filter) plus a strengthened ldbc_complex_9 guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Fleet-Machine: bronco Fleet-Agent: cca-61a8aa18f5144c5d
Review found the first cut hardcoded `.id`, which is wrong on renamed (`user_id`) and composite (`[bank_id, account_number]`) node ids — it emitted `end_node.id = start_node.id` verbatim (Code 47 live) on those schemas, violating the axis-dispatch rule (CLAUDE.md §7). It only worked on LDBC because its node_id happens to be literally `id`. Rework: resolve each endpoint's real node_id column(s) from the schema (`node_schema_opt(label).node_id.id.columns()`) at the render seam, where `start_label`/`end_label` and `schema` are already in scope, and build the comparison against those columns: - single column: `a =/<> b` → `a.col =/<> b.col` - composite `a = b` → `(a.c1 = b.c1 AND a.c2 = b.c2 …)` - composite `a <> b` → `NOT (a.c1 = b.c1 AND a.c2 = b.c2 …)` On any unresolved label the predicate is left untouched (loud fallback, never wrong SQL). Verified across all three axes: LDBC (`id`), renamed (`user_id`), composite (`[bank_id, account_number]`) — each emits the correct column(s) and executes live (no Code 47). Added renamed + composite golden tests. Also filed #1103: a pre-existing, separate defect this exposes — a both-endpoint WHERE filter (`friend <> root`) is applied only in the VLP base case, so cyclic self-paths leak at `*1..N` for N≥3. The property form `friend.id <> root.id` shows the identical leak, so it predates this fix; LDBC IC9/IC5 use `*1..2` and are unaffected (verified 0 self-rows). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Fleet-Machine: bronco Fleet-Agent: cca-61a8aa18f5144c5d
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second slice of #1100. Unblocks LDBC IC9 and IC5 end-to-end.
A predicate like
WHERE friend <> root/friend = rootcompares node identity, but both sides are bare node aliases. In a variable-length path this predicate lands in the recursive CTE's WHERE, where the aliases aren't in scope (the CTE's node columns arestart_node/end_node).render_expr_to_sql_stringrenders a bareTableAliasas the literal alias name, emittingfriend != rootinside the CTE body → ClickHouse Code 47.Fix
At the VLP
where_predicaterender seam (right after conversion toRenderExpr, before categorization), normalize a bare-node-identity comparison into property-access comparisons on the endpoints' schema-declarednode_idcolumn(s):a =/<> b→a.col =/<> b.cola = b→(a.c1 = b.c1 AND a.c2 = b.c2 …)a <> b→NOT (a.c1 = b.c1 AND …)This uses the real
node_id(resolved fromstart_label/end_label+ schema, both already in scope), not a hardcoded.id— correct on renamed and composite node ids per the axis-dispatch rule (CLAUDE.md §7). The resulting property-access form flows through the existing property + alias mapping. Tightly guarded: fires only when both operands are bare node aliases and both are the VLP's own endpoints; recurses throughAND/OR/NOT; leaves the predicate untouched (loud fallback) on any unresolved label.Validation
id), renamed (User.node_id: user_id), composite (Account.node_id: [bank_id, account_number]) — each emits the correct column(s) and executes live with no Code 47.cargo testgreen (1738 lib + integration); ratchet green; fmt/clippy clean.<>,=nested in AND (sibling filter survives), renamed node_id, composite node_id, plus a strengthenedldbc_complex_9guard.Review response
An adversarial review caught that the first cut hardcoded
.id(broken on renamed/composite schemas). The reworked commit resolves the realnode_idinstead, with dedicated axis tests. The review also flagged a pre-existing cyclic self-path leak (both-endpoint filter applied only in the base case) — confirmed independent of this fix (the.idproperty form shows the identical leak) and filed as #1103; LDBC IC9/IC5 use*1..2and are unaffected.Remaining under #1100
IS2 (compound-alias endpoint), IC10 (pattern predicate), IC3 (Code 62), BI12 (endpoint property projection) have distinct root causes and stay tracked in #1100.
🤖 Generated with Claude Code