Skip to content

fix(cypher): stop the early WHERE pruning rows it cannot judge yet - #2253

Merged
DeusData merged 1 commit into
mainfrom
distill/1245-cypher-partial-where
Oct 4, 2026
Merged

DeusData merged 1 commit into
mainfrom
distill/1245-cypher-partial-where

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Distilled from #1245 by @ahundt (upstream b835b5e), carried with Co-authored-by. Re-derived on main's cypher.c rather than cherry-picked — the file has moved ~9k lines since the branch point. #1245 stays open until its distills land.

The bug

WHERE runs twice: early on seed rows (only the first alias bound) to prune, and late (all aliases bound) to decide. Both passes used the same two-valued evaluator, which answers true for a comparison on an alias it cannot see — the permissive default the late pass relies on. NOT and XOR invert whatever they are given, so:

MATCH (a)-[:CALLS]->(b) WHERE NOT b.name CONTAINS "x"

evaluated NOT(true) on every seed and pruned every row. Zero results, no error, for a correct query.

The fix

The early pass now uses a three-valued evaluator: unbound alias → UNKNOWN; NOT of UNKNOWN stays UNKNOWN; AND/OR short-circuit only on a definite operand; XOR with any UNKNOWN is UNKNOWN. Only a definite FALSE prunes. The late pass is untouched, so anything that was correct before is unchanged.

Three deliberate differences from upstream: one shared leaf rule for the tree path and main's legacy flat AND/OR path; a c->variable == NULL guard (on main a literal-only condition like coalesce(0, 1) >= 2 carries no variable — upstream would have passed NULL to strcmp); the flat path keeps main's empty-list-is-true rule.

RED → GREEN

Before the fix, all four new tests fail with row_count == 0, expected 1:
cypher_exec_where_not_on_relationship_target, cypher_exec_where_mixed_alias_and, _or, _xor.
After: cypher 208 passed; cypher + mcp 526 passed, 4 skipped (the Windows-only skips).

eval_expr_partial recurses and is added to CBM_RECURSION_WHITELIST; scripts/check-nolint-whitelist.sh passes.

Formatting note: the whitelist macro was repacked by Homebrew clang-format 22 after the insertion lengthened a line. AlignEscapedNewlines and column packing are version-stable, but CI's clang-format 20 is the judge — if lint flags it, that is the only place to look.

Distilled from #1245 by Andrew Hundt (upstream b835b5e), re-derived on
main's cypher.c rather than cherry-picked -- the file has moved ~9k lines
since the branch point.

The WHERE clause runs twice: once early, on seed rows where only the
first alias is bound, to prune; and once late, when every alias is bound,
to decide. The early pass used the same two-valued evaluator as the late
one. That evaluator answers "true" for a comparison on an alias it cannot
see -- the permissive default that makes the late pass work -- but NOT and
XOR invert whatever they are given, so

    MATCH (a)-[:CALLS]->(b) WHERE NOT b.name CONTAINS "x"

evaluated NOT(true) on every seed and pruned every row. Zero results, no
error, for a query that is correct.

The early pass now uses a three-valued evaluator. A condition on an alias
that is not yet bound is UNKNOWN; NOT of UNKNOWN stays UNKNOWN; AND and OR
short-circuit only on a definite operand; XOR with any UNKNOWN operand is
UNKNOWN. Only a definite FALSE prunes. The late pass is unchanged and
still gives the final two-valued answer, so nothing that was correct
before changes.

Three deliberate differences from the upstream diff:

  - the unbound-alias rule lives in one leaf, eval_condition_partial, so
    the expression-tree path and main's legacy flat AND/OR path cannot
    drift apart;
  - that leaf guards c->variable == NULL. On main a condition whose left
    side is a function of literals, coalesce(0, 1) >= 2, carries no
    variable at all; the upstream version would have passed NULL to
    strcmp;
  - the flat path keeps main's rule that an empty condition list is true.

RED before the fix, all four with row_count == 0, expected 1:
cypher_exec_where_not_on_relationship_target,
cypher_exec_where_mixed_alias_and, ..._or, ..._xor. GREEN after: cypher
208 passed; cypher + mcp 526 passed, 4 skipped (the Windows-only skips).

eval_expr_partial recurses and is added to CBM_RECURSION_WHITELIST.

Co-authored-by: Andrew Hundt <ATHundt@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData added bug Something isn't working cypher Cypher query language parser/executor bugs priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Oct 1, 2026
@DeusData
DeusData force-pushed the distill/1245-cypher-partial-where branch from 458b0db to dc5293d Compare October 1, 2026 21:25
@DeusData
DeusData merged commit 8ab555e into main Oct 4, 2026
40 checks passed
DeusData added a commit to lorenzozanee/codebase-memory-mcp that referenced this pull request Oct 20, 2026
…rseded

The early WHERE pass in execute_single runs on seed rows where only the
first alias is bound, so a NOT on the target node or the relationship
pruned every row (DeusData#2250). This branch fixed that by skipping the early
pass until every alias the WHERE reads was bound.

While it waited for review, DeusData#2253 (8ab555e) landed the same fix on
main as a three-valued early pass: an unbound alias is UNKNOWN, NOT
keeps it UNKNOWN, and only a definite FALSE prunes. This branch's
regression tests pass on main's production code unchanged, so restore
src/cypher/cypher.c to the branch's base and keep the tests; the merge
of main that follows takes main's implementation.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit to lorenzozanee/codebase-memory-mcp that referenced this pull request Oct 20, 2026
Conflict in tests/test_cypher.c: both sides added WHERE NOT regression
tests right after cypher_exec_where_not. Kept both sets, main's four
DeusData#2253 tests first, then this branch's four, with their RUN_TEST lines in
the same order. src/cypher/cypher.c merged cleanly to main's version.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit to lorenzozanee/codebase-memory-mcp that referenced this pull request Oct 20, 2026
…eads

eval_condition_partial (from DeusData#2253) answers UNKNOWN for a condition whose
alias is not bound yet, but it only looked at c->variable. A function
condition is filed under its first argument's alias, so in

    MATCH (a)-[:CALLS]->(b) WHERE coalesce(a.no_such_prop, b.name) = 'x'

the seed row saw a bound a, read coalesce(NULL, <unbound b>) as a
definite mismatch and pruned every row before CALLS expansion: zero rows,
while the same predicate after WITH returned the right ones.

Check every function argument's alias as well, the way this branch's
original fix did (condition_bindings_available), inside main's
three-valued leaf.

RED before: cypher_exec_where_func_arg_on_unbound_target, row_count 0,
expected 1. GREEN after; cypher suite 237 passed.

Co-authored-by: lorenzozanee <wyz0707@proton.me>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cypher Cypher query language parser/executor bugs priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant