Skip to content

feat Existence tests 1/4: bare .exists() on reverse FK — the tracer bullet #314

Description

@0x054

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

  • Bare .exists() works on one-to-one and to-many BackRefs, uniform spelling and rendering (always correlated EXISTS — no LEFT JOIN specialization)
  • Each matching root returns exactly once; result stays root-shaped and composes with &/|, root order_by, limit
  • ~t.rel.exists() renders NOT EXISTS; both feat Reverse-relation membership predicates: EXISTS from the root query (PRD from Pinch) #307 is_transfer branches pass e2e on both backends
  • Reverse proxy rejects everything except .exists() — column access, comparisons, != None, in_ — with build-time errors naming .exists() (messages pinned by tests)
  • QueryIR version bumped; hand-authored golden vector for the bare 1-hop exists node asserted by Python emitter and Rust decoder
  • e2e tests follow the existing per-feature query test file style on the existing backend matrix

Blocked by

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureready-for-agentUnblocked and fully specified; an agent may claim it

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions