Skip to content

feat(query): uniform predicate negation — prefix ~ as a NOT wire node#318

Merged
0x054 merged 2 commits into
mainfrom
feat/uniform-negation
Jul 18, 2026
Merged

feat(query): uniform predicate negation — prefix ~ as a NOT wire node#318
0x054 merged 2 commits into
mainfrom
feat/uniform-negation

Conversation

@0x054

@0x054 0x054 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Implements PRD #310 across both slices in one PR: the ~ tracer bullet on leaf predicates (#312) and compounds, guard rails, and docs (#313).

What

One universal negation rule: prefix ~ negates any predicate node — leaf comparison or AND/OR compound.

Txn.where(lambda t: ~t.category_id.in_(ids))          # NOT IN — previously unspellable
User.where(lambda u: ~u.email.like("%@example.com"))  # NOT LIKE — likewise
User.where(lambda u: ~((u.active == True) | (u.email.like("%@ferro.dev"))))

renders as a faithful NOT (...) over the child:

SELECT ... FROM "user" WHERE NOT ("category_id" IN (...))
SELECT ... FROM "user" WHERE NOT ("active" = TRUE OR "email" LIKE '%@ferro.dev')

How

  • Python: QueryNode.__invert__ wraps any node in a NOT node; the truthiness guard (__bool__) now names ~ as the supported spelling. The mutate traversal guard and join registration see through NOT, so ~ can neither smuggle a join into update()/delete() nor drop a traversed child's join.
  • Wire: a new recursive not node kind beside leaf/compound{"node_kind": "not", "child": {...}}, any child, any depth. QueryIR version bumps to v6 (unconditional, single supported version; the Rust gate rejects v5 with the actionable one-wheel message). No per-operator negative forms anywhere (ADR-0008).
  • Rust: one recursion case in the condition builder emitting SeaQuery Cond::not over the rebuilt child. Double negation reaches Rust as two nested not nodes; SeaQuery's negation toggle renders the even nesting as the plain child — semantically exact under three-valued logic (NOT NOT x ≡ x for TRUE/FALSE/UNKNOWN alike).
  • Golden vectors: two hand-authored vectors pin the not-over-leaf and not-over-compound shapes, asserted byte-for-byte from the Python emitter and round-tripped by the Rust decoder; the existing query vectors move to v6.

Testing

  • End-to-end result-set tests (tests/test_negation.py), green on both backends: ~ over every leaf operator kind (==, !=, orderings, in_, like), ~ over AND/OR compounds, double negation, ~ mixed into &/| trees at multiple levels, NULL-row behavior under negation (three-valued semantics, matching !=), composition with order_by()/limit(), and ~ over a relation-traversing leaf.
  • Error paths: the truthiness-guard message pinned to name ~; NOT-wrapped traversal rejected on update()/delete().
  • Full suite: 1599 passed on sqlite+postgres; all Rust workspace suites green.

Docs

New "Negating Conditions" guide section with the central three-valued-logic note (real nullable-column model in Assignment + Annotated tabs, rendered SQL); ~/NOT row in the operators table; API reference states the universal rule; not_in() removed from the roadmap. All examples lambda-style and runnable under the docs test harness.

Closes #312
Closes #313

0x054 added 2 commits July 18, 2026 10:21
Prefix ~ negates any predicate node — leaf comparison or AND/OR
compound — via QueryNode.__invert__, carried on the wire as a new
recursive `not` node kind beside leaf/compound (QueryIR v6), and
rendered in Rust as a faithful SQL NOT (...) over the rebuilt child
(SeaQuery Cond::not). No per-operator negative forms are added:
~t.col.in_(ids) and ~t.col.like(p) close the pre-existing NOT IN /
NOT LIKE gap (ADR-0008).

- Two hand-authored golden vectors pin the not-over-leaf and
  not-over-compound wire shapes, asserted from the Python emitter and
  the Rust decoder; existing query vectors move to v6.
- The truthiness guard (QueryNode.__bool__) now names ~ as the
  supported negation spelling.
- The mutate traversal guard and join registration see through NOT, so
  negation can neither smuggle a join into update()/delete() nor drop
  a traversed child's join.
- End-to-end result-set tests on both backends: ~ over every leaf
  operator kind, compounds, double negation, mixed &/| trees,
  NULL-row behavior (three-valued logic), order_by/limit composition,
  and NOT over a traversed leaf.

Refs #310, #312, #313
- New "Negating Conditions" guide section: the one universal rule
  (~ negates any predicate; no per-operator negative forms), runnable
  lambda-style examples, and the central SQL three-valued-logic note
  with a real nullable-column model (Assignment + Annotated tabs) and
  the rendered NOT (...) SQL — negation excludes NULL rows, exactly
  like the existing != spelling.
- Operators table gains the ~ / NOT row; the guardrails bullet quotes
  the updated truthiness message naming ~.
- API reference states the universal ~ rule and links the NULL note.
- not_in() removed from the roadmap and the guide's "Not Yet
  Supported" list — ~t.col.in_(ids) is the spelling.

Refs #313
@0x054
0x054 merged commit 6061d66 into main Jul 18, 2026
7 checks passed
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.

feat Uniform negation 2/2: compounds, guard rails, docs feat Uniform negation 1/2: ~ on leaf predicates — the tracer bullet

1 participant