Skip to content

feat(query): M2M existence tests — the two-hop correlation path#321

Closed
0x054 wants to merge 1 commit into
feat/existence-tests-315-scoped-existsfrom
feat/existence-tests-316-m2m-exists
Closed

feat(query): M2M existence tests — the two-hop correlation path#321
0x054 wants to merge 1 commit into
feat/existence-tests-315-scoped-existsfrom
feat/existence-tests-316-m2m-exists

Conversation

@0x054

@0x054 0x054 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Closes #316

Stacked on #320 (slice 2/4); base retargets as the stack merges.

Slice 3/4 of the existence-tests PRD (#311, ADR-0007): .exists() on many-to-many relations, bare, scoped, and ~-negated — spelled and behaving exactly like the reverse-FK form.

What ships

User.where(lambda u: u.tags.exists(lambda tag: tag.name == "admin"))

renders

SELECT ... FROM "user"
WHERE EXISTS (SELECT 1 FROM "tag_users" AS "x1_tags"
              INNER JOIN "tag" AS "x2_tags" ON "x1_tags"."tag_id" = "x2_tags"."id"
              WHERE "x1_tags"."user_id" = "user"."id"
                AND "x2_tags"."name" = 'admin')
  • The reverse-spec map learns M2M entries — join table, source column, target column, facts relationship resolution already computes on each side's descriptor (which orients the triple per side, so the declaring side and the related_name side spell identically).
  • Same node, two hops. On the wire the exists node carries a two-hop correlation path: join table first (correlated to the enclosing scope), then the target. The Rust render loop from the earlier slices handles it unchanged — zero new render mechanism, which is the design claim this slice set out to prove.
  • Inner lambda over the M2M target has full predicate power — operators, &/|/~, the cross-scope guard — all the feat Existence tests 2/4: scoped inner predicates — forward traversal, nesting, cross-scope guard #315 machinery, untouched.
  • Hand-authored golden vector for the 2-hop path, asserted from the Python emitter and the Rust decoder; a Rust render test pins join-table-FROM, inner-join-target, and last-hop-alias qualification for the inner tree.

Acceptance criteria (#316)

  • Bare, scoped, and ~-negated .exists() on an M2M relation, e2e on both backends
  • Each matching root returns exactly once regardless of how many join-table rows match
  • Inner lambda over the M2M target has full predicate power, matching the reverse-FK slice
  • Hand-authored golden vector for the 2-hop exists path asserted by Python emitter and Rust decoder
  • No new Rust render mechanism introduced — the existing hop loop covers the 2-hop path

Testing

  • cargo test: 196 + 28 (ferro-schema-ir) passed, including the 2-hop render test and vector round-trip
  • pytest --db-backends=sqlite,postgres: 1668 passed (full matrix, local Postgres)
  • ty check and ruff check: clean

t.tags.exists(...) works on many-to-many relations, bare, scoped, and
~-negated, spelled and behaving exactly like the reverse-FK form
(ADR-0007: one verb at every cardinality and relation kind). The same
exists node carries a two-hop correlation path — join table first,
correlated to the enclosing scope, then the target — and the existing
Rust render loop covers it unchanged: no new render mechanism, proving
the design's claim that M2M is test surface, not a second mechanism.

- build_reverse_specs maps M2M descriptors to join-table specs; each
  side's descriptor already orients source_col/target_col for that
  side, so both the declaring and the related_name side spell
  identically.
- The proxy builds hops from the spec: join_table.source_col against
  the root PK, then target on join_table.target_col = target.pk. The
  inner lambda resolves over the M2M target with full predicate power
  (operators, combinators, negation, cross-scope guard) — the #315
  machinery, untouched.
- Hand-authored golden vector pins the 2-hop shape, asserted from the
  Python emitter and the Rust decoder; a Rust render test pins
  join-table-FROM + inner-join-target + last-hop-alias qualification.
- e2e on both backends: bare (each root exactly once regardless of how
  many join rows match), scoped, negated, both sides of the relation,
  root-predicate/count composition, and the proxy's single-verb error
  surfaces.

Refs #311, #316
@0x054

0x054 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #323 (slices 2–4 combined into one rebase-mergeable PR; content identical).

@0x054
0x054 deleted the branch feat/existence-tests-315-scoped-exists July 18, 2026 15:41
@0x054 0x054 closed this Jul 18, 2026
@0x054
0x054 deleted the feat/existence-tests-316-m2m-exists branch July 18, 2026 15:41
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