Repository navigation
cypher: the out-of-scope RETURN check switches off above 32 declared names #1995
Description
What happens
#1922 made the engine refuse a RETURN that names a variable no pattern declared, instead
of answering with a blank column. That check switches itself off once a query declares
more than 32 names, and the old silence comes back.
Reproduction against any indexed project. The chain is anchored on a label that matches
nothing, so both queries are instant and neither depends on the graph's contents.
# 10 declared names — correctly refused
MATCH (a0:NoSuchLabelXYZ)-[:CALLS]->(a1)-[:CALLS]->…-[:CALLS]->(a9) RETURN zzz.name
→ variable 'zzz' is not in scope for RETURN — no pattern in this query names it
# 35 declared names — accepted, and answers
MATCH (a0:NoSuchLabelXYZ)-[:CALLS]->(a1)-[:CALLS]->…-[:CALLS]->(a34) RETURN zzz.name
→ rows: 0 (cols: zzz.name)
Same undeclared variable, same clause, opposite answers. The only difference is how many
names the pattern declares.
Why it happens
check_projection_scope (src/cypher/cypher.c:5085) models the declared names in a fixed
array of CYP_SCOPE_MAX_NAMES = 32:
int declared_n = collect_declared_names(q, declared, CYP_SCOPE_MAX_NAMES);
if (declared_n < 0) {
return NULL; /* too many names to model — stay quiet rather than guess */
}Nothing bounds how many names a query can declare, so collect_declared_names can
overflow that array and give up. The return NULL was deliberate — a wrong refusal costs
the caller a working query, which is worse than the silence — but it leaves the caller
with no way to know the check did not run.
The root cause is upstream of this
pat->node_count grows on demand at src/cypher/cypher.c:1868 with no cap, so a single
MATCH can declare an unbounded number of variables. That one gap degrades three separate
guards, all silently:
| Site | What it does past its bound |
|---|---|
check_projection_scope (cypher.c:5085) |
stops checking scope at 32 names |
binding_add_node (cypher.c:2558) |
never binds the 17th variable, so its column reads empty |
execute_default_projection (cypher.c:4588) |
stops collecting columns at 16 |
binding_add_edge (cypher.c:2515) is the same shape on the edge table, at 8.
Suggested fix
Cap the pattern's declared variable count at parse time and refuse beyond it, which is how
the sibling limits in this file are already handled — an over-wide RETURN at
cypher.c:1806, an over-long ORDER BY at cypher.c:1737, and an over-wide WITH in
PR #1918. A parse-time refusal costs no caller signature changes, whereas making
binding_add_node and binding_add_edge report an overflow means threading an error path
up through every caller — both functions return void today.
Once the pattern's variable count is bounded at or below CYP_SCOPE_MAX_NAMES, the
declared_n < 0 branch above becomes unreachable and the scope check can no longer switch
itself off.
Found while addressing the review on #1918.
Activity
fix(cypher): refuse a query naming more variables than a binding holds
DeusData commentedon Sep 3, 2026
Thank you for the bounded, fixture-free reproduction.
Current
mainstill has the exact escape hatch you identified:check_projection_scope()returns success whencollect_declared_names()exceeds its 32-name model, so the scope check becomes silent for the wide query while execution retains smaller binding limits. This is a real query-correctness bug.I have routed it as high priority. No additional evidence is requested right now; thank you for tracing the common capacity mismatch rather than treating the three silent guards as unrelated symptoms.
fix(cypher): refuse a query naming more variables than a binding holds
DeusData commentedon Sep 7, 2026
Fixed on main by 421d9d2 (PR #1998, thank you @CaptainMittens). Ships in v0.10.9.
fix(cypher): refuse a query naming more variables than a binding holds