Parent
#311 (feat PRD: Existence tests — .exists() on reverse and M2M relations)
What to build
The tracer bullet for existence tests: bare .exists() on a reverse (BackRef) relation, end-to-end — proxy spelling through the wire IR to a correlated EXISTS on both database backends.
A reverse-spec map is derived once at the existing compile choke point (beside the forward relation specs), carrying reverse-FK facts relationship resolution already computes: related model, child FK column, one-to-one flag. The predicate proxy consults it ahead of the column fallback and returns a reverse-relation proxy exposing .exists() and nothing else — column access, comparison operators (including != None / == None), and in_ raise at build time with the supported spelling in the message (ADR-0007: reverse relations are tested, not traversed).
On the wire: a new recursive exists node kind carrying a correlation hop path (one hop for a reverse FK, reusing the join-hop vocabulary) and an inner condition tree (empty in this slice). QueryIR version bumps; a hand-authored golden vector pins the shape. The Rust render emits EXISTS (SELECT 1 FROM child WHERE child.fk = root.pk) correlated to the enclosing scope's alias; ~t.rel.exists() renders NOT EXISTS via the NOT node from #312.
Demo: both branches of #307's is_transfer filter —
Txn.where(lambda t: t.transfer_out.exists() | t.transfer_in.exists()) and its ~-negated twin — correct on SQLite and Postgres.
Acceptance criteria
Blocked by
Parent
#311 (
featPRD: Existence tests —.exists()on reverse and M2M relations)What to build
The tracer bullet for existence tests: bare
.exists()on a reverse (BackRef) relation, end-to-end — proxy spelling through the wire IR to a correlated EXISTS on both database backends.A reverse-spec map is derived once at the existing compile choke point (beside the forward relation specs), carrying reverse-FK facts relationship resolution already computes: related model, child FK column, one-to-one flag. The predicate proxy consults it ahead of the column fallback and returns a reverse-relation proxy exposing
.exists()and nothing else — column access, comparison operators (including!= None/== None), andin_raise at build time with the supported spelling in the message (ADR-0007: reverse relations are tested, not traversed).On the wire: a new recursive
existsnode kind carrying a correlation hop path (one hop for a reverse FK, reusing the join-hop vocabulary) and an inner condition tree (empty in this slice). QueryIR version bumps; a hand-authored golden vector pins the shape. The Rust render emitsEXISTS (SELECT 1 FROM child WHERE child.fk = root.pk)correlated to the enclosing scope's alias;~t.rel.exists()renders NOT EXISTS via the NOT node from #312.Demo: both branches of #307's
is_transferfilter —Txn.where(lambda t: t.transfer_out.exists() | t.transfer_in.exists())and its~-negated twin — correct on SQLite and Postgres.Acceptance criteria
.exists()works on one-to-one and to-many BackRefs, uniform spelling and rendering (always correlated EXISTS — no LEFT JOIN specialization)&/|, rootorder_by,limit~t.rel.exists()renders NOT EXISTS; bothfeatReverse-relation membership predicates: EXISTS from the root query (PRD from Pinch) #307is_transferbranches pass e2e on both backends.exists()— column access, comparisons,!= None,in_— with build-time errors naming.exists()(messages pinned by tests)existsnode asserted by Python emitter and Rust decoderBlocked by
featUniform negation 1/2:~on leaf predicates — the tracer bullet #312