Skip to content

Commit 9c59b57

Browse files
committed
docs(sql-orm-many-to-many): slice 2 spec + plan (TML-2786)
Filter slice: some/every/none EXISTS through the junction. 2 dispatches (filter code / integration tests). Reuses slice 1 User↔Tag fixture. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 4ee6af6 commit 9c59b57

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Slice 2: filter EXISTS through the junction — Dispatch plan
2+
3+
**Spec:** `projects/sql-orm-many-to-many/slices/02-filter-exists-through-junction/spec.md`
4+
**Linear:** [TML-2786](https://linear.app/prisma-company/issue/TML-2786)
5+
6+
Two dispatches: filter code (judgment) then integration tests (verification). No fixture dispatch — slice 1's `User ↔ Tag` is reused.
7+
8+
### Dispatch 1: filter EXISTS walks the junction
9+
10+
- **Outcome:** `some`/`every`/`none` on an M:N relation compile to a correctly-shaped EXISTS / NOT EXISTS that walks the junction (target JOIN junction correlated to parent on the junction side; composite-key AND-ed); FK filters unchanged. Unit-tested at the AST level.
11+
- **Builds on:** slice 0's `ResolvedRelation.through` (carried by `resolveModelRelations`).
12+
- **Hands to:** correctly-shaped M:N relation filters — the behaviour D2 verifies on the DB.
13+
- **Focus:** the M:N branch in `buildExistsExpr`/`buildJoinWhere` (`model-accessor.ts`); surface `through` onto the filter relation if it's dropped; unit tests for the EXISTS AST (some/every/none through junction). No integration tests here.
14+
15+
### Dispatch 2: filter integration tests (operator standard)
16+
17+
- **Outcome:** integration tests prove `.filter(u => u.tags.some/every/none(...))` returns the right users on PGlite, following the standard — whole-row `toEqual` on the filtered set, explicit `.select` in most, **≥1** implicit/default-selection case; `some`, `every`, `none`, and an empty-match edge covered.
18+
- **Builds on:** D1's filter code + slice 1's fixture/seed helpers (`seedUserTags`).
19+
- **Hands to:** the slice-DoD-satisfying M:N filter coverage.
20+
- **Focus:** new integration test file under `test/integration/test/sql-orm-client/`, PGlite via `withCollectionRuntime`; reuse the `seedTags`/`seedUserTags` helpers slice 1 added. Run via `cd test/integration && pnpm test test/sql-orm-client/<file>`.
21+
22+
## Handoff completeness
23+
24+
Slice-DoD reachable: correctly-shaped junction EXISTS (D1 unit) + filter behaviour on DB per standard (D2 integration) + FK filters unchanged (D1).
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Slice 2: relation filters (some/every/none) through the junction
2+
3+
_Parent project: `projects/sql-orm-many-to-many/`. Outcome: `.filter(u => u.tags.some/every/none(...))` emits an EXISTS that walks the junction._
4+
5+
## At a glance
6+
7+
`db.orm.User.filter((u) => u.tags.some((t) => t.name.eq('x')))` (and `.every` / `.none`) must produce an EXISTS subquery that walks the **junction** for M:N relations. Today `buildJoinWhere` (`model-accessor.ts`) reads only `relation.on.localFields/targetFields`, so an M:N filter would emit a wrong-shape EXISTS that skips the junction. This slice adds the junction hop, reusing slice 0's `through` descriptor.
8+
9+
## Chosen design
10+
11+
**Add an M:N branch to the EXISTS builder.** `createRelationFilterAccessor``buildExistsExpr``buildJoinWhere` (`model-accessor.ts`). When the resolved relation carries `through`:
12+
13+
- **`some(pred)`**`EXISTS (SELECT 1 FROM target JOIN junction ON junction.childColumns = target.targetColumns WHERE junction.parentColumns = parent.anchor AND <pred>)`.
14+
- **`none(pred)`**`NOT EXISTS (… AND <pred>)`.
15+
- **`every(pred)`**`NOT EXISTS (… AND NOT (<pred>))` (no related row that fails the predicate), mirroring the existing FK `every` shape.
16+
17+
The parent correlation moves to the **junction** side (`junction.parentColumns = parent.anchor`); the target is reached via the junction join (`junction.childColumns = target.targetColumns`); composite keys AND-ed. The child predicate (`<pred>`) is unchanged — it still applies to the target columns.
18+
19+
The relation passed to `buildJoinWhere` comes from `resolveModelRelations`, which slice 0 extended with `through`; **confirm** the filter path's relation type carries `through` (if it uses a relation shape that drops it, plumb it through — same one-field surfacing slice 1 did for `IncludeExpr`).
20+
21+
## Coherence rationale
22+
23+
One reviewable story: "M:N relation filters walk the junction." The `some`/`every`/`none` cases share the single junction-EXISTS shape; they're one coherent change to `buildJoinWhere`/`buildExistsExpr`, not separable.
24+
25+
## Scope
26+
27+
**In:** the M:N branch in `buildExistsExpr`/`buildJoinWhere` (`model-accessor.ts`) for `some`/`every`/`none`; surfacing `through` onto the filter path's relation if needed; unit tests (EXISTS AST through junction); integration tests per the standard.
28+
29+
**Out:** include reads (slice 1, done); nested writes (slice 3); non-relation filters; any `through` shape change (slice 0 owns it). No fixture change — reuse slice 1's `User ↔ Tag`.
30+
31+
## Pre-investigated edge cases
32+
33+
| Edge case | Disposition | Notes |
34+
|---|---|---|
35+
| Composite-key junction | AND across all column pairs in both the junction→parent correlation and junction→target join | slice 0 arrays |
36+
| `every` semantics | `NOT EXISTS (… junction … AND NOT(pred))` — mirror the existing FK `every`, just through the junction | don't invent a new shape |
37+
| Relation type may drop `through` | If the filter path's resolved-relation type doesn't carry `through`, surface/plumb it (one field), don't approximate | grounding for the implementer |
38+
39+
## Slice-specific done conditions
40+
41+
- [ ] `.some/.every/.none` on an M:N relation emit a correctly-shaped EXISTS/NOT EXISTS that joins through the junction (composite-key AND-ed); unit test asserts the AST.
42+
- [ ] Integration tests (PGlite) per the standard: whole-row `toEqual` on the filtered result set; explicit `.select` in most; **≥1** implicit/default-selection case; cover `some`, `every`, `none`, and an empty-match edge.
43+
- [ ] FK relation filters unchanged (existing tests pass).
44+
45+
## Open Questions
46+
47+
1. **`through` availability on the filter relation.** Working position: `resolveModelRelations` already carries `through` (slice 0); the filter path reuses it directly. If grounding shows otherwise, plumb the one field (no design change).
48+
49+
## References
50+
51+
- Parent project: `projects/sql-orm-many-to-many/spec.md` (§ Cross-cutting — integration-test standard).
52+
- Slice 0 `ResolvedRelation.through`; slice 1 fixture (`User ↔ Tag`).
53+
- Linear: [TML-2786](https://linear.app/prisma-company/issue/TML-2786)

projects/sql-orm-many-to-many/trace.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@
4646
{"event_id":"b5530bd5-2ada-44bc-b65e-880aaff74187","schema_version":"1","ts":"2026-06-01T18:53:04.232Z","project_run_id":"sql-orm-many-to-many","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"88c9c264-8edc-4584-b440-7d51eeae37e4","round_id":"a405f9a4-2407-4328-93f9-476c31d88c0e","brief_byte_length":4062,"brief_content_hash":"c0f7aa67226f403d7c10b578156ae5b5dd8141de924ed80e3b0e1aeec917ae3e","brief_disposition":"initial"}
4747
{"event_id":"7f4f9ac3-eaf8-41df-b5df-cc72314030de","schema_version":"1","ts":"2026-06-01T19:14:43.687Z","project_run_id":"sql-orm-many-to-many","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"88c9c264-8edc-4584-b440-7d51eeae37e4","round_id":"a405f9a4-2407-4328-93f9-476c31d88c0e","verdict":"satisfied","findings_filed":0,"wall_clock_ms":1299311}
4848
{"event_id":"10edc1f4-e070-4dc6-b0ac-bf8eaf2565a7","schema_version":"1","ts":"2026-06-01T19:14:44.090Z","project_run_id":"sql-orm-many-to-many","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"88c9c264-8edc-4584-b440-7d51eeae37e4","result":"completed","wall_clock_ms":1299697}
49+
{"event_id":"c766ebb8-dfa9-4f3a-b033-3ce51a3594cc","schema_version":"1","ts":"2026-06-01T19:18:04.849Z","project_run_id":"sql-orm-many-to-many","orchestrator_agent_id":null,"event_type":"spec-authored","spec_path":"projects/sql-orm-many-to-many/slices/02-filter-exists-through-junction/spec.md","spec_kind":"slice","byte_length":4110,"edge_cases_count":3,"open_questions_count":1,"dod_items_count":3}
50+
{"event_id":"c358128a-8d02-451d-b2d6-ee791793231e","schema_version":"1","ts":"2026-06-01T19:18:05.325Z","project_run_id":"sql-orm-many-to-many","orchestrator_agent_id":null,"event_type":"plan-authored","plan_path":"projects/sql-orm-many-to-many/slices/02-filter-exists-through-junction/plan.md","plan_kind":"slice","byte_length":2068,"dispatch_count":2,"slice_count":null,"dispatch_size_distribution":{"S":0,"M":2,"L":0,"XL":0},"open_items_count":0}

0 commit comments

Comments
 (0)