feat(gfql/cypher): NOT-pattern AST plumbing (#1031 slice 2 phase 2a)#1233
Merged
feat(gfql/cypher): NOT-pattern AST plumbing (#1031 slice 2 phase 2a)#1233
Conversation
Top-level `WHERE NOT (pattern)` shapes (e.g. `WHERE NOT (n)-[:R]->()`) now parse cleanly and lift into `WhereClause.predicates` as `WherePatternPredicate(negated=True)` entries instead of tripping the legacy "cannot yet be mixed with generic row expressions" E108. This is the AST half of slice 2; the runtime half (anti-semi-join lowering) ships in a follow-up sub-PR. Changes: - `ast.py`: `WherePatternPredicate.negated: bool = False` field. Default keeps existing single-positive / multi-positive callers unchanged. - `parser.py::_split_top_level_and_pattern_leaves`: add top-level `not(pattern_atom)` case that strips the NOT and emits the inner pattern as a negated leaf. Returns a 4-tuple `(positive, negated, others, has_nested)`. Patterns nested deeper (under OR/XOR or double-NOT) still trip the legacy E108 reject so slice 4 / De-Morgan- NOT compositions stay deferred. - `parser.py::_build_where_with_pattern_lift`: accept both positive and negated leaves; emit one `WherePatternPredicate` per leaf with the matching `negated` flag. - `ast_normalizer::_rewrite_where_pattern_predicates_to_matches`: partition into positive (appended MatchClause as before) vs negated (passes through to lowering). - `lowering.py`: `WherePatternPredicate` checks now distinguish the two cases. Negated raises a scoped "Cypher WHERE NOT (pattern) anti-semi-join lowering is not yet supported" pointing at the engine-half follow-up. Positive case unchanged. Tests: - `test_parser.py`: replaces `test_parse_rejects_mixed_where_pattern_predicates_as_unsupported` parametrize with two parametrize blocks — OR cases (still rejected, slice 4) and NOT cases (now lift, new test `test_parse_lifts_top_level_not_pattern_to_negated_predicate`). - `test_lowering.py`: splits the legacy `_failfast_rejects_unsupported_mixed_variable_length_where_pattern_predicates` test — drops NOT cases, adds `_failfast_rejects_negated_pattern_until_slice2_lowering` locking the precise new error message. Verified: - 1585/1585 GFQL tests pass. - mypy clean on all 4 touched cypher modules. Out of scope (engine half / phase 2b+): - Anti-semi-join runtime lowering itself. Path C (row-pipeline anti-join via Let + NotIn) recommended in `plans/1031-slices-2-3-4/findings/slice-2-scope.md`. Will land as a follow-up PR. - Bound-aliases NOT-pattern (`MATCH (a)-[:R]->(b) WHERE NOT (b)-[:R]-(a)`). AST plumbing already handles it via the negated flag; runtime needs the same engine work. - IC10 benchmark unblock. Composes the engine work above with row-NOT (already supported via expr_tree). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
PR Review: #1233 — feat(gfql/cypher): NOT-pattern AST plumbing (#1031 slice 2 phase 2a)Branch: BlockersNone. ImportantNone. Suggestions
Human checks required
MethodologyPer RecommendationApprove and merge with |
This was referenced Apr 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slice 2 of #1031, phase 2a (AST half). Top-level
WHERE NOT (pattern)shapes now parse cleanly and lift intoWhereClause.predicatesasWherePatternPredicate(negated=True)entries. The runtime half (anti-semi-join lowering) ships in a follow-up sub-PR.Why split
Slice 2 needs both AST plumbing and a new lowering path. AST plumbing is small (~140 LOC, well-localized) and ships cleanly; lowering needs a new row-pipeline anti-join primitive. Splitting lets the AST land + tested in isolation, and gives the engine half a clear hook point.
Behavioral effect
MATCH (n) WHERE NOT (n)-[:R]->() RETURN nMATCH (n) WHERE NOT (n)-[:R*]->() RETURN nMATCH (n) WHERE (n)-[:R]->() OR n.id = 'z' RETURN nMATCH (n) WHERE (n)-[:R]->() AND (n)-[:T]->() RETURN n(slice 3)User-facing behavior unchanged — query still fails. But error is scoped to the engine gap, and the AST exposes a clean
negatedflag for the engine half to plug in.Changes
cypher/ast.pyWherePatternPredicate.negated: bool = Falsefieldcypher/parser.py::_split_top_level_and_pattern_leavesnot(pattern_atom)case; emit inner pattern as negated leaf. Returns 4-tuple(positive, negated, others, has_nested).cypher/parser.py::_build_where_with_pattern_liftWherePatternPredicateper leaf with matchingnegatedflagcypher/ast_normalizer::_rewrite_where_pattern_predicates_to_matchescypher/lowering.pyWherePatternPredicateraises scopedCypher WHERE NOT (pattern) anti-semi-join lowering is not yet supportedtests/test_parser.py_rejects_mixed_*: OR stays rejected; NOT now lifts via new_lifts_top_level_not_pattern_to_negated_predicatetests/test_lowering.py_rejects_negated_pattern_until_slice2_loweringlocking the precise new errorPatterns nested deeper (under OR/XOR or double-NOT) still trip the legacy E108 reject so slice 4 / De-Morgan-NOT compositions stay deferred.
Test plan
parser.py,ast.py,ast_normalizer.py,lowering.pyOut of scope
Let+NotIn) recommended inplans/1031-slices-2-3-4/findings/slice-2-scope.md; follow-up sub-PRMATCH (a)-[:R]->(b) WHERE NOT (b)-[:R]-(a)) — AST plumbing handles it via the negated flag; runtime needs the same engine workCloses
Partial — slice 2 phase 2a only. Engine half + slice 4 still under #1031.
Related
plans/1031-slices-2-3-4/findings/slice-2-scope.md— full scope + path comparison