fix(cypher): refuse a query naming more variables than a binding holds - #1998
fix(cypher): refuse a query naming more variables than a binding holds#1998CaptainMittens wants to merge 1 commit into
Conversation
|
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. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
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. |
|
This is one of the best-argued PRs I have reviewed in this queue. I checked the load-bearing claims against the source rather than taking them, and every one holds:
And the placement is right: The core insight is the one worth keeping: the Lint is red, and it is yoursSame for In substance it is a false positive — const char *node_vars[CYP_MAX_VARS] = {0};
const char *edge_vars[CYP_MAX_EDGE_VARS] = {0};Worth knowing why the neighbouring One place the description claims slightly more than is true
As I read That does not change my view, because the remedy is already the first thing your error message says — "leave the name off the ones you do not use" — and an unnamed node takes no slot, so the fix is exact and local. But the claim as written is stronger than what the code supports, and I would rather the PR body said "cannot be answered for the names past the bound". If I have misread the ordering, tell me. AlsoYou are 3 commits behind The three tests are well chosen — anchoring on a label that matches nothing makes them instant and fixture-free, and pinning the reporter's case at both 10 and 35 declared names is what actually demonstrates the bug rather than the symptom. Noting that the wide case is refused for width rather than scope, so the message names the limit instead of Fix the lint, rebase, and I am happy to merge this. |
|
Heads-up on sequencing, and a reminder of the one outstanding item. Still outstanding here: the cppcheck And this conflicts with your own #1918. I verified it rather than assuming: merging The two are the same defect family — this refuses a query naming more pattern variables than a binding holds, #1918 refuses a I have suggested on #1918 that it goes first, purely because it needs nothing further from you while this one still owes the lint fix. That would leave this PR needing a small, mechanical test-file rebase — additive test cases, not competing logic. If you would rather this one landed first, say so and I will sequence it that way instead. My review from yesterday otherwise stands unchanged: approved on merit, and the reframing that made it convincing — that the old |
check_projection_scope models declared names in a fixed 32-entry array and skipped the check entirely when a query declared more. Nothing bounded how many variables a pattern could declare, so the out-of-scope refusal added by DeusData#1922 switched itself off on a wide query: the same undeclared name was refused at 10 declared names and quietly accepted at 35, answering a column of empty strings. Skipping was deliberate — a wrong refusal costs the caller a working query, which is worse than the silence. The premise was wrong. A binding holds CYP_MAX_VARS node variables and CYP_MAX_EDGE_VARS edge variables in plain arrays, and binding_set and binding_set_edge drop anything past those without a word, so a query naming more cannot be answered at all. Its extra names bind to nothing and project as blanks, which reads as "the graph holds no such data". The choice was never refuse-or-stay-quiet; it was refuse, or answer wrong. So bound the input. check_pattern_var_capacity counts distinct node and edge variables across every pattern — they share one binding, and an OPTIONAL MATCH pattern sits in the same array — and refuses beyond what a binding holds, naming the limit and how to get under it. Leaving the name off a node frees its slot, because an unnamed node takes none. Bounding the input rather than each consumer also makes three other silent drops unreachable: binding_set past 16, binding_set_edge past 8, and the column collection in execute_default_projection. Those would otherwise need an error path threaded up through every caller, since both binding setters return void. The two `< 0` branches in check_projection_scope stay, with comments saying they can no longer fire. They keep the guard standing if either bound ever moves. Fixes DeusData#1995 Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
45b9728 to
41e6492
Compare
|
The cppcheck fix is in — pushed about an hour ago, so it likely landed after you wrote this. It is the shape you named: /* Initialized because cppcheck cannot see that scope_holds reads only the
* node_n / edge_n entries already written, and reports the first call as a
* read of an uninitialized array. */
const char *node_vars[CYP_MAX_VARS] = {NULL};
const char *edge_vars[CYP_MAX_EDGE_VARS] = {NULL};I kept the comment because the code was already correct — Locally, On sequencing: #1918 first is fine, no need to change it. I will take the One thing that may help that decision — neither of #1918's reds is its code:
So #1918 needs a re-run rather than a change from me. |
|
You did not misread the ordering. I checked I have rewritten that paragraph in the description. It now says the query "cannot be Everything else you asked for is already pushed, as of my earlier comment: the initializers Still happy to take the |
The out-of-scope
RETURNrefusal added by #1922 stops working on a wide query. Sameundeclared variable, same clause, opposite answers — the only difference is how many names
the pattern declares:
check_projection_scopemodels declared names in a fixed 32-entry array.collect_declared_namesanswers-1when a query declares more, and the caller thenreturns
NULL— no check at all. Nothing bounds how many variables a pattern can declare,because
pat->node_countgrows on demand.Why the fix is a bound and not a bigger array
That
-1branch was deliberate, and its comment says why: a wrong refusal costs the callera working query, which is worse than the silence. The premise is what turned out to be
wrong.
A query naming more variables than a binding can hold cannot be answered for the names
past the bound. A
binding_tholds exactlyCYP_MAX_VARSnode variables andCYP_MAX_EDGE_VARSedge variables in plain arrays.binding_set/binding_set_edgeappend in call order and drop anything past those without a word. Names inside the bound
still bind correctly, so a query that declares more but projects only those does answer
correctly today, and this change refuses it too. Names past the bound are the ones that
cannot be answered: they bind to nothing and project as empty strings, which reads as "the
graph holds no such data". For those the choice was never "refuse a working query or stay
quiet" — it was "refuse it, or answer it wrong". The remedy the error names is exact for
both cases, because an unnamed node takes no slot.
check_pattern_var_capacityrefuses it, before any row is touched:Both ways out are real ones. An unnamed node takes no slot, so dropping a name the query
never uses is the cheap one. Splitting the
MATCHis deliberately not offered: everypattern in one query shares one binding, which is why the check counts across all of them.
Separate queries do work, because each gets a binding of its own.
One bound, four silent sites
Bounding the input rather than each consumer closes three further drops that were all the
same root cause:
check_projection_scopebinding_setbinding_set_edgeexecute_default_projectionThe last three would otherwise need an error path threaded up through every caller, since
both
binding_setandbinding_set_edgereturnvoid. Bounding the input removes thatneed.
The two
< 0branches incheck_projection_scopestay, with comments saying they can nolonger fire. They cost nothing and they are what keeps the function correct if either bound
ever moves.
Scope of the change
The check counts DISTINCT variables across every pattern, because they all land in one
binding: a multi-
MATCHquery shares one, and anOPTIONAL MATCHpattern sits in the sameq->patternsarray. It runs once perUNIONarm, in the loop that already runs the scopecheck.
Nothing real is caught by the bound. The widest pattern anywhere in this repository names
three variables, across
tests/,src/andinternal/.Tests
Three in
tests/test_cypher.c, each anchored on a label that matches nothing so they areinstant and depend on no fixture:
cypher_wide_pattern_refusedcypher_wide_edge_pattern_refusedcypher_scope_check_survives_wide_patternRETURN zzz.nameis refused at 10 declared names AND at 35All three verified failing before the change, each at the assertion that the query was
accepted, and passing after.
Note the wide case is refused for width, not for scope — the capacity check runs first, so
the message names the limit rather than
zzz. Either way the caller gets an error insteadof a column of blanks, which is what #1995 asks for. Through the built binary:
The 35-name line previously read
rows: 0 (cols: zzz.name).The bound lands where
binding_tends, and nothing narrower. On the same index, withevery name declared so scope is not in play:
and an ordinary query is untouched:
MATCH (a:Function)-[rel:CALLS]->(b:Function) RETURN a.name, b.name LIMIT 3still answers 3 rows.Fixes #1995