Skip to content

fix(cypher): RETURN * must read the live scope, not the query pattern - #1918

Open
CaptainMittens wants to merge 1 commit into
DeusData:mainfrom
CaptainMittens:fix/return-star-respects-with-scope
Open

fix(cypher): RETURN * must read the live scope, not the query pattern#1918
CaptainMittens wants to merge 1 commit into
DeusData:mainfrom
CaptainMittens:fix/return-star-respects-with-scope

Conversation

@CaptainMittens

Copy link
Copy Markdown
Contributor

PROBLEM

RETURN * built its columns from the variables the query pattern named,
never from the bindings it was about to project. One line caused two
separate wrong answers, and neither reported an error — so a caller
cannot tell a broken query from an empty graph.

Measured against a real indexed project before the fix:

Query Columns Values
MATCH (f:Function) RETURN * 4, correct real
MATCH (f:Function) OPTIONAL MATCH (f)-[:CALLS]->(g) RETURN * 12 — f appears twice real
the same, plus WITH f.name AS caller, g.name AS callee the same 12 ALL empty

CAUSE

execute_return_star called collect_pattern_vars(q, ...), which reads
the parse-time pattern. Two consequences fall out of that one call.

After a WITH, the pattern variables are out of scope — the WITH
replaced them with the names it made, and the live bindings are keyed by
those names. Asking for f then finds nothing, and every column is
written as "".

Separately, collect_pattern_vars appended each pattern in turn with no
repeat check, so a variable named in two patterns got its four columns
twice. The OPTIONAL MATCH above names f twice.

FIX

  • New execute_return_star_after_with: when a WITH is present, columns
    come from its items. A name a WITH makes holds one value rather than
    a node, so it is ONE column, not the four a node variable gets.
  • collect_pattern_vars gained a repeat check. It has exactly one
    caller, so nothing else is affected.

The query above now answers two columns, caller and callee, holding
their values.

TESTS

Two, both failing against the old code:

cypher_return_star_dedups_repeated_pattern_var   col_count == 12, expected 8
cypher_return_star_after_with_names_aliases      col_count == 8,  expected 2

Before: 183 passed, 2 failed. After: 185 passed.

GATES RUN

  • scripts/lint.sh --ci — exit 0, all linters passed
  • scripts/test.sh --suites cypher — exit 0, 185 passed

NOT IN THIS PR

A second fault found alongside this one, kept out to keep the PR to one
change: a variable a WITH drops is still accepted afterwards and
renders empty, because nothing checks a projected name against the live
scope. MATCH (f:Function)-[:CALLS]->(g) WITH f.name AS caller RETURN caller, g.name runs and prints a g.name column of nothing, where real
Cypher refuses the query and names g. Happy to raise it as its own
issue.

🤖 Generated with Claude Code

RETURN * built its columns from the variables the query pattern named,
never from the bindings it was about to project. One line caused two
separate wrong answers, and neither one reported an error.

After a WITH, the pattern's variables are out of scope — the WITH
replaced them with the names it made. The old code still asked for the
old names, found none of them, and answered a full result of empty
strings. This query used to print twelve columns of nothing:

  MATCH (f:Function) OPTIONAL MATCH (f)-[:CALLS]->(g)
  WITH f.name AS caller, g.name AS callee RETURN *

It now prints two columns, caller and callee, holding their values. A
name the WITH made holds one value rather than a node, so it gets one
column, not the four a node variable gets.

Separately, collect_pattern_vars appended every pattern's variables with
no repeat check. A variable named in two patterns got its four columns
twice, which the OPTIONAL MATCH above does with f.

Two tests cover both faults and fail against the old code:
  cypher_return_star_dedups_repeated_pattern_var   col_count 12, want 8
  cypher_return_star_after_with_names_aliases      col_count 8,  want 2

Cypher suite: 185 passed, 0 failed. clang-format clean on both files.

Reported alongside a second fault this does NOT fix: a variable the WITH
dropped is still accepted afterwards and renders empty, because nothing
checks a projected name against the live scope. See
.agents/research/2026-08-29-cypher-return-star-and-with-scope.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant