Repository navigation
Description
Version
codebase-memory-mcp dev — built from source at main 997d087b.
Platform
macOS (Apple Silicon)
Install channel
Built from source
Binary variant
standard
What happened, and what did you expect?
A RETURN may name a variable that the WITH before it dropped. The query
runs, exits clean, prints a column for that variable, and fills the column with
empty strings. Nothing reports an error.
Real Cypher refuses this query and names the offending variable. Here the
caller cannot tell a wrong query from a graph that holds no such data.
The empty column is the harmful part. A reader — a person or an agent — sees a
column of nothing and reads it as "the graph has no data here". The true answer
is "your query named something that is out of scope".
Reproduction
The repository's own cypher test fixture reproduces it, so no outside code is
needed. setup_cypher_store() in tests/test_cypher.c builds a store with
three CALLS edges.
-
Code being indexed: the fixture built by
setup_cypher_store()in
tests/test_cypher.c. -
The query:
MATCH (f:Function)-[:CALLS]->(g) WITH f.name AS caller RETURN caller, g.namegdoes not survive theWITH. A plainMATCHdoes this as readily as an
OPTIONAL MATCH, so the optional half is not the cause. -
Result: three rows. Column
g.nameis present and every value is"".
Return code 0. No error text anywhere.Expected: a parse or execution error naming
gas out of scope.
To see it without writing a test, run the same query through query_graph
against any indexed project, using a variable the WITH did not carry.
Logs
PROBE rc=0 col_count=2 row_count=3
PROBE column[0]="caller"
PROBE column[1]="g.name"
PROBE row[0] = ["HandleOrder"] [""]
PROBE row[1] = ["HandleOrder"] [""]
PROBE row[2] = ["ValidateOrder"] [""]
Captured by a throwaway test against the fixture above, on main 997d087b
plus the one commit in #1918. That commit only touches RETURN *, so it does
not affect this path.
Where it comes from
Nothing checks a projected name against the live scope. An unbound variable
resolves to NULL and renders as "", and that is deliberate:
src/cypher/cypher.c:2568—return NULL; /* unbound variable */src/cypher/cypher.c:3345and:3350— an unmatchedOPTIONAL MATCHtarget
stays unbound on purpose, and the projection renders it"".
That convention is right for the OPTIONAL MATCH case. Its cost is that a name
which was never carried through looks exactly like an optional row that did not
match, so no error can be raised at projection time.
A search for not in scope, unknown variable, undefined variable and
scope check across src/cypher/cypher.c at 997d087b returns nothing.
Suggested fix
Add one check after parsing, before execution: every variable a RETURN or
WITH item names must be either a pattern variable still in scope, or an alias
the previous WITH created. Report the name that fails.
The check belongs at parse time, which keeps the "" convention for an
unmatched OPTIONAL MATCH target exactly as it is. The check is about names
that were never carried through, not about rows that did not match.
Related
- query_graph: UNWIND is parsed but silently ignored (README says supported / should error) #1192 —
UNWINDis parsed but silently ignored and should error. Same shape:
a query that is wrong runs quietly instead of failing. - fix(cypher): RETURN * must read the live scope, not the query pattern #1918 — fixes a neighbouring fault where
RETURN *after aWITHanswered
every value empty. That one is the star projection reading the query pattern
instead of the live bindings. This issue is the named-column path and is a
separate fix, kept out of that PR on the one-issue-per-PR rule.
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code — this repository's own test fixture.
Activity
DeusData commentedon Aug 31, 2026
Fixed on
mainbyafc948bab13c, merged asa6468ed441b0ef120657cc14a8164087e44e72c4(#1922).The check runs between parse and execute and reads the parsed query, not the run-time bindings — so it asks whether the query declared a name, which is a different question from whether a row happened to bind one. That is what keeps an
OPTIONAL MATCHtarget that matched nothing legal and still projecting""; the convention atcypher.c:3345/:3350is untouched, andcypher_optional_match_target_still_allowed_issue1919pins it.Same failure shape as #373: a name the query cannot resolve now fails loudly instead of projecting a blank column that reads as "no such data".
fix(cypher): refuse a name that is not in scope instead of projecting…