Skip to content

feat(gfql/cypher): NOT-pattern AST plumbing (#1031 slice 2 phase 2a)#1233

Merged
lmeyerov merged 1 commit intomasterfrom
m4-1031-slice-2-not-pattern
Apr 29, 2026
Merged

feat(gfql/cypher): NOT-pattern AST plumbing (#1031 slice 2 phase 2a)#1233
lmeyerov merged 1 commit intomasterfrom
m4-1031-slice-2-not-pattern

Conversation

@lmeyerov
Copy link
Copy Markdown
Contributor

Summary

Slice 2 of #1031, phase 2a (AST half). Top-level WHERE NOT (pattern) shapes now parse cleanly and lift into WhereClause.predicates as WherePatternPredicate(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

Shape Pre-PR Post-PR
MATCH (n) WHERE NOT (n)-[:R]->() RETURN n E108 "cannot yet be mixed with generic row expressions" parses, lifts, then E108 "anti-semi-join lowering is not yet supported"
MATCH (n) WHERE NOT (n)-[:R*]->() RETURN n same same scoped error
MATCH (n) WHERE (n)-[:R]->() OR n.id = 'z' RETURN n rejected (slice 4) unchanged
MATCH (n) WHERE (n)-[:R]->() AND (n)-[:T]->() RETURN n (slice 3) works unchanged

User-facing behavior unchanged — query still fails. But error is scoped to the engine gap, and the AST exposes a clean negated flag for the engine half to plug in.

Changes

File Change
cypher/ast.py WherePatternPredicate.negated: bool = False field
cypher/parser.py::_split_top_level_and_pattern_leaves Add top-level not(pattern_atom) case; emit inner pattern as negated leaf. Returns 4-tuple (positive, negated, others, has_nested).
cypher/parser.py::_build_where_with_pattern_lift Accept positive + negated leaves; emit one WherePatternPredicate per leaf with matching negated flag
cypher/ast_normalizer::_rewrite_where_pattern_predicates_to_matches Partition into positive (appended MatchClause) vs negated (pass-through to lowering)
cypher/lowering.py Negated WherePatternPredicate raises scoped Cypher WHERE NOT (pattern) anti-semi-join lowering is not yet supported
tests/test_parser.py Splits _rejects_mixed_*: OR stays rejected; NOT now lifts via new _lifts_top_level_not_pattern_to_negated_predicate
tests/test_lowering.py Adds _rejects_negated_pattern_until_slice2_lowering locking the precise new error

Patterns 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

  • 1585 GFQL tests pass; no regressions
  • mypy clean on parser.py, ast.py, ast_normalizer.py, lowering.py

Out of scope

  • Anti-semi-join runtime lowering — path C (row-pipeline anti-join via Let + NotIn) recommended in plans/1031-slices-2-3-4/findings/slice-2-scope.md; follow-up sub-PR
  • Bound-aliases NOT-pattern (MATCH (a)-[:R]->(b) WHERE NOT (b)-[:R]-(a)) — AST plumbing handles it via the negated flag; runtime needs the same engine work
  • IC10 benchmark unblock — composes the engine work with row-NOT (already supported via expr_tree)

Closes

Partial — slice 2 phase 2a only. Engine half + slice 4 still under #1031.

Related

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>
@lmeyerov
Copy link
Copy Markdown
Contributor Author

PR Review: #1233 — feat(gfql/cypher): NOT-pattern AST plumbing (#1031 slice 2 phase 2a)

Branch: m4-1031-slice-2-not-pattern @ d8c24544a
Base: master @ 4253ef6e6
Files: 7 (parser.py, ast.py, ast_normalizer.py, lowering.py, test_parser.py, test_lowering.py, CHANGELOG.md)
LOC: +137 / −38

Blockers

None.

Important

None.

Suggestions

  1. [Architecture, trivial] Lowering's scoped error (anti-semi-join lowering is not yet supported) doesn't reference the findings doc. Future engine contributors hit the error → grep the message → find the inline code comment which DOES cite plans/1031-slices-2-3-4/findings/slice-2-scope.md. One extra grep-hop vs zero. Cosmetic.
  2. [Testing/Correctness, optional] No direct tests for NOT NOT (pattern) (double-NOT) or NOT (pattern AND pattern) (NOT-around-AND-tree). Walker correctly rejects these via _has_pattern_descendant fallthrough; adding tests would document the boundary between phase 2a and slice 4.

Human checks required

  1. Approve and merge after CI green.
  2. Confirm Closes #1031 is not set — engine half + bound-aliases NOT-pattern + slice 4 remain.
  3. Coordination signal on Cypher/GFQL WHERE cannot mix generic row predicates with pattern predicates #1031 thread post-merge: "slice 2 phase 2a (AST half) merged; engine half (anti-semi-join lowering) is the next sub-PR."

Methodology

Per ~/Work/graphistry/.agents/skills/review/SKILL.md. Single wave (justified by tight scope, AST-only plumbing, full test parity, structural correctness verifiable from code). 10 dimensions. Adversarial in adversarial/wave-1.md. Convergence on Wave 1.

Recommendation

Approve and merge with --admin once CI is green. No defects. Trivial SUGGESTIONs not blocking. The PR sets up clean engineering scope for the runtime follow-up.

@lmeyerov lmeyerov merged commit e1707bf into master Apr 29, 2026
133 checks passed
@lmeyerov lmeyerov deleted the m4-1031-slice-2-not-pattern branch April 29, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant