Skip to content

Commit d711adf

Browse files
committed
docs(sql-orm-many-to-many): expand project with follow-on slices 4-6
Spec + plan amended for the follow-on scope (demos + authoring completeness): slice 4 SQLite demo examples (done, TML-2790), slice 5 PSL M:N authoring (TML-2794, framework gap — may promote to its own project), slice 6 PG demo examples + dual-mode reconciliation (TML-2795, blocked by slice 5). Slice specs + provisional dispatch plans for each. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 883309e commit d711adf

7 files changed

Lines changed: 217 additions & 1 deletion

File tree

projects/sql-orm-many-to-many/plan.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
## At a glance
77

8-
Four slices: one **foundation slice** (slice 0) that makes M:N a validatable contract shape and surfaces the shared `through` descriptor, then a **three-way parallel fan-out** — read, filter, write — each consuming slice 0's hand-off and independent of the others.
8+
**Runtime core (slices 0–3, complete):** one **foundation slice** (slice 0) makes M:N a validatable contract shape and surfaces the shared `through` descriptor, then a **three-way parallel fan-out** — read, filter, write — each consuming slice 0's hand-off.
9+
10+
**Follow-on (slices 4–6, added 2026-06-02):** demo examples + authoring-surface completeness — SQLite demo examples (done), PSL M:N authoring (a framework gap surfaced while adding the demos), and PG demo examples (blocked by the PSL work). See [§ Follow-on slices](#follow-on-slices--authoring-completeness--demos).
911

1012
## Composition
1113

@@ -45,3 +47,28 @@ Four slices: one **foundation slice** (slice 0) that makes M:N a validatable con
4547
## Sequencing rationale
4648

4749
Slice 0 is a hard gate, not a stylistic choice: until the contract validates an M:N relation and the resolver surfaces `through`, slices 1–3 have no validatable integration fixture to test against and nothing to read `through` from. Once slice 0 lands, the three consumers touch disjoint files (`query-plan-select.ts` / `model-accessor.ts` / `mutation-executor.ts`) and share only the read-only `ResolvedRelation.through` field — no write-write contention — so they parallelise cleanly. They are sequenced after 0 purely by data dependency, not by reviewer pacing.
50+
51+
## Follow-on slices — authoring completeness + demos
52+
53+
Added 2026-06-02 after the runtime core (0–3) shipped: while adding M:N **demo examples** we found the navigable M:N API is authorable **only via the TS contract builder** (`rel.manyToMany`), not PSL — so the PG demo (PSL-emitted) can't yet show it. These slices close that gap. Each is its own slice spec under `slices/`.
54+
55+
- **Slice `04-sqlite-demo-examples`** — Linear: [TML-2790](https://linear.app/prisma-company/issue/TML-2790)**DONE.**
56+
- **Outcome:** the SQLite demo (`examples/prisma-next-demo-sqlite`, TS-authored) demonstrates the full M:N API: `Post ↔ Tag` via `PostTag`, with include / `some`/`none`/`every` filter / nested `connect`/`disconnect`/`create` ORM modules + CLI commands + seed, smoke-tested end-to-end.
57+
- **Builds on:** slices 0–3 (the runtime M:N feature).
58+
- **Hands to:** a worked reference for the M:N ORM API (the PG demo, slice 6, mirrors it once unblocked).
59+
60+
- **Slice `05-psl-many-to-many-authoring`** — Linear: [TML-2794](https://linear.app/prisma-company/issue/TML-2794)**planned.**
61+
- **Outcome:** PSL can author a navigable M:N relation — the interpreter lowers a junction (explicit `@@id([a,b])` join model and/or implicit `Tag[]` list) to `cardinality:'N:M'` + a `through` descriptor, parity with the TS builder.
62+
- **Builds on:** slice 0's contract `through` shape (the lowering target).
63+
- **Hands to:** PSL-authored M:N → unblocks the PG demo (slice 6).
64+
- **Focus:** `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (today emits only `N:1`/`1:N`) + the PSL→RelationNode lowering; recognise/collapse the junction into a `through` relation. **Likely large — re-check slice-INVEST at pickup; may warrant promotion to its own project.**
65+
66+
- **Slice `06-pg-demo-examples`** — Linear: [TML-2795](https://linear.app/prisma-company/issue/TML-2795)**planned, blocked by slice 5.**
67+
- **Outcome:** the PG demo (`examples/prisma-next-demo`) demonstrates the M:N API (mirroring slice 4), AND its pre-existing dual-mode contract drift (stale TS source / missing TS-builder discriminator authoring) is reconciled.
68+
- **Builds on:** slice 5 (PSL M:N authoring — the PG demo emits from PSL); slice 4 (the example shape to mirror).
69+
- **Hands to:** M:N demonstrated in both demos; dual-mode green.
70+
- **Focus:** add `Post ↔ Tag` M:N to the PSL source + example modules/CLI/seed/tests; resolve dual-mode (`test:dual-mode` is currently red on the TS leg — fix the TS source or drop it). The dual-mode-drift half is independent of slice 5 and can start first.
71+
72+
### Sequencing (follow-on)
73+
74+
Slice 4 (done) and the **dual-mode-drift half of slice 6** are independent and could have run anytime after the core. Slice 5 (PSL authoring) gates the **M:N-examples half of slice 6**. Note this pushes the project to **7 slices** — past the 1–4 sweet spot; slice 5 in particular is framework-scoped and may be better promoted to its own project at pickup (flagged above).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Slice 4: SQLite demo M:N examples — DONE
2+
3+
_Parent project: `projects/sql-orm-many-to-many/`. Linear: [TML-2790](https://linear.app/prisma-company/issue/TML-2790). Status: **complete** (branch `tml-2790-mn-demo-examples`)._
4+
5+
## At a glance
6+
7+
Demonstrate the M:N ORM API end-to-end in the **SQLite** demo (`examples/prisma-next-demo-sqlite`, TS-authored — the only demo that can author M:N today, since the PG demo emits from PSL). Worked reference for include / filter / nested-write through a junction.
8+
9+
## Chosen design (as shipped)
10+
11+
`Post ↔ Tag` (+ reverse `Tag.posts`) M:N via a **pure** `PostTag` junction (`postId`/`tagId`, composite PK, no payload), authored in `prisma/contract.ts` with `rel.manyToMany(() => Tag, { through: () => PostTag, from, to })`. Emitted contract carries `cardinality:'N:M'` + `through`. ORM client modules:
12+
13+
- `get-post-tags.ts``.include('tags', t => t.select('id','label')…)`.
14+
- `get-posts-by-tag-filter.ts``.where(p => p.tags.some/none/every(t => t.label.eq(...)))`.
15+
- `connect-post-tags.ts` / `disconnect-post-tags.ts``.update({ tags: t => t.connect/disconnect([{ id }]) })` + readback.
16+
- `create-post-with-tags.ts``.create({ …, tags: t => t.create([{ label }]) })`.
17+
18+
Wired as 6 CLI commands in `src/main.ts`; seed adds tags + junction rows. Smoke-tested end-to-end (SQLite is offline-runnable).
19+
20+
## Scope
21+
22+
**In:** the SQLite demo only — contract, emit, ORM modules, CLI, seed. **Out:** the PG demo (PSL can't author M:N — slice 6, blocked by slice 5).
23+
24+
## Slice-specific done conditions
25+
26+
- [x] Emitted contract has `cardinality:'N:M'` + `through`; `emit:check` + typecheck clean.
27+
- [x] include / `some`/`none`/`every` / connect / disconnect / create examples run end-to-end (smoke-tested via CLI).
28+
29+
## References
30+
31+
- Parent: `projects/sql-orm-many-to-many/spec.md`. Commits: `72ef8b793` (contract+emit), `883309ecc` (modules/CLI/seed).
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Slice 5: PSL M:N authoring — Dispatch plan (provisional)
2+
3+
**Spec:** `projects/sql-orm-many-to-many/slices/05-psl-many-to-many-authoring/spec.md`
4+
**Linear:** [TML-2794](https://linear.app/prisma-company/issue/TML-2794)
5+
6+
> **Provisional** — the dispatch breakdown depends on PSL-pipeline grounding not yet done (the PSL interpreter / lowering / diagnostic interaction). Firm up at pickup via `drive-specify-slice` + `drive-plan-slice`; **re-check whether this is a slice or a project** (it's framework-scoped). The breakdown below is the expected shape.
7+
8+
### Dispatch 1: PSL junction → `N:M` + `through` lowering
9+
10+
- **Outcome:** the PSL relation resolver/lowering recognises a junction (explicit `@@id([a,b])` join model — form 1) and emits a relation with `cardinality:'N:M'` + a populated `through` descriptor (composite-key-correct), parity with the TS builder. Unit-tested at the lowering level.
11+
- **Builds on:** slice 0's contract `through` shape (the target); the TS builder's `rel.manyToMany` lowering as the parity reference.
12+
- **Hands to:** PSL emits navigable M:N relations.
13+
- **Focus:** `psl-relation-resolution.ts` + the PSL→RelationNode lowering; the `PSL_ORPHANED_BACKRELATION_LIST` diagnostic (relax when a junction is recognised, without regressing legitimate explicit-junction 1:N use).
14+
15+
### Dispatch 2: PSL M:N fixture + ORM-API parity tests
16+
17+
- **Outcome:** a PSL-authored M:N fixture round-trips `validateContract`, and the M:N ORM API (include / `some`/`none`/`every` / nested write) works against it — parity with the TS-authored `User↔Tag` fixture.
18+
- **Builds on:** dispatch 1.
19+
- **Hands to:** proven PSL→ORM M:N parity → unblocks the PG demo (slice 6).
20+
- **Focus:** a PSL fixture mirroring the TS M:N fixture; integration tests (reuse the slice-1/2/3 M:N test patterns); `fixtures:check`.
21+
22+
### Dispatch 3 (conditional): implicit-list authoring
23+
24+
- **Outcome:** Prisma-style implicit `Tag[]`/`Post[]` M:N (no explicit junction) lowers to an implicit junction. **Only if Open Question 1 chooses to include it** — otherwise drop (form 1 alone satisfies the slice).
25+
26+
## Handoff completeness
27+
28+
Slice-DoD reachable: PSL emits `N:M`+`through` (D1) · ORM-API parity from a PSL contract (D2). D2's hand-off (PSL M:N parity) is what slice 6 (PG demo) needs.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Slice 5: PSL many-to-many authoring
2+
3+
_Parent project: `projects/sql-orm-many-to-many/`. Linear: [TML-2794](https://linear.app/prisma-company/issue/TML-2794). Status: **planned** (not started)._
4+
5+
> **Sizing flag:** this is framework-scoped (the PSL interpreter), not an `sql-orm-client` change. It may fail slice-INVEST *Small* and warrant **promotion to its own project**. Re-check at pickup (`drive-triage-work`); the dispatch plan below is provisional pending PSL-pipeline grounding.
6+
7+
## At a glance
8+
9+
PSL can't author a navigable M:N relation. The PSL relation resolver emits only `cardinality:'N:1'`/`'1:N'`; `'N:M'` + `through` comes only from the TS builder (`rel.manyToMany`). PSL routes M:N to explicit junction models (`PSL_ORPHANED_BACKRELATION_LIST`). This slice teaches PSL to lower a junction to a navigable `N:M` + `through`, so PSL-authored schemas get the same M:N ORM API (and the PG demo, slice 6, becomes possible).
10+
11+
## Chosen design (intent — to be firmed at pickup)
12+
13+
Teach the PSL interpreter / lowering to recognise a many-to-many shape and emit a relation with `cardinality:'N:M'` + a `through` descriptor (`{ table, parentColumns, childColumns, targetColumns }`) — the exact shape slice 0 put in the contract and `sql-orm-client`'s `resolveThrough` consumes. Two candidate authoring forms (decide at spec-firm-up):
14+
15+
1. **Explicit junction model** — an `@@id([a, b])` join model with two FK relations (the shape PSL currently emits as three `1:N`/`N:1` relations) is recognised and *additionally* surfaced as a navigable `N:M` on each side. Lowest-magic; matches what the diagnostic already steers authors toward.
16+
2. **Prisma-style implicit list**`tags Tag[]` / `posts Post[]` with no explicit junction, lowered to an implicit junction table. More ergonomic, more interpreter work.
17+
18+
Primary surface: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (today emits `N:1` at ~line 255, `1:N` at ~line 322 — no `N:M`/`through`) + the PSL→RelationNode lowering + the `PSL_ORPHANED_BACKRELATION_LIST` diagnostic (relax/replace when the junction is recognised).
19+
20+
## Scope
21+
22+
**In:** PSL relation resolution + lowering to emit `N:M` + `through`; the M:N PSL diagnostic; PSL-authored M:N fixtures/tests; round-trip through `validateContract`.
23+
24+
**Out:** `sql-orm-client` runtime (already done, slices 0–3 — consumes `through` regardless of author); the TS builder (already emits `through`); the PG demo (slice 6).
25+
26+
## Pre-investigated edge cases
27+
28+
| Edge case | Disposition |
29+
|---|---|
30+
| Composite-key junctions | `through.{parentColumns,childColumns,targetColumns}` are arrays — lowering must handle multi-column FKs (parity with the TS builder) |
31+
| Self-referential M:N (e.g. User↔User followers) | confirm the lowering handles same-model both-sides |
32+
| Existing `PSL_ORPHANED_BACKRELATION_LIST` consumers | relaxing the diagnostic must not regress schemas that legitimately want explicit-junction 1:N modelling |
33+
34+
## Slice-specific done conditions
35+
36+
- [ ] A PSL schema with a junction (form decided at firm-up) emits a relation with `cardinality:'N:M'` + populated `through`, round-tripping `validateContract`.
37+
- [ ] ORM-API parity: `include` / `some`/`none`/`every` / nested write work from a PSL-authored M:N contract (a PSL fixture mirroring the TS `User↔Tag` fixture).
38+
- [ ] `fixtures:check` green; the M:N diagnostic behaves correctly (no false `PSL_ORPHANED_BACKRELATION_LIST` on a recognised junction).
39+
40+
## Open Questions
41+
42+
1. **Authoring form** — explicit-junction recognition (form 1) vs implicit-list (form 2) vs both. Working position: **start with explicit-junction recognition** (form 1) — smallest, matches the existing diagnostic's guidance; add implicit-list as a follow-up if desired.
43+
2. **Project vs slice** — likely promote to its own project. Working position: re-triage at pickup.
44+
45+
## References
46+
47+
- Parent: `projects/sql-orm-many-to-many/spec.md` (§ Follow-on scope). Slice 0's contract `through` shape is the lowering target.
48+
- `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`; the TS builder's `rel.manyToMany` lowering (`contract-lowering.ts`) as the parity reference.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Slice 6: PG demo M:N examples + dual-mode — Dispatch plan (provisional)
2+
3+
**Spec:** `projects/sql-orm-many-to-many/slices/06-pg-demo-examples/spec.md`
4+
**Linear:** [TML-2795](https://linear.app/prisma-company/issue/TML-2795)
5+
6+
> **Provisional** — half B depends on slice 5 (PSL M:N authoring); the dual-mode-reconciliation approach (half A) is an open decision. Firm up at pickup.
7+
8+
### Dispatch 1: dual-mode reconciliation (independent — can run before slice 5)
9+
10+
- **Outcome:** `pnpm test:dual-mode` is green — either both PSL and TS emit legs pass, or the TS contract source is removed and the demo is cleanly PSL-only (per Open Question 1).
11+
- **Builds on:** nothing in this project (it's a pre-existing demo bug).
12+
- **Hands to:** a demo whose contract source(s) are consistent — a clean base for the M:N example additions.
13+
- **Focus:** `examples/prisma-next-demo` dual-mode setup (`prisma-next.config.ts*`, `prisma/contract.ts`, `src/prisma/contract.prisma`, the `emit:psl`/`emit:ts`/`test:dual-mode` scripts).
14+
15+
### Dispatch 2: PG demo M:N examples (blocked by slice 5)
16+
17+
- **Outcome:** the PG demo demonstrates the M:N API — `Post ↔ Tag` M:N in the PSL source, with include / `some`/`none`/`every` filter / nested `connect`/`disconnect`/`create` ORM modules + CLI commands + seed + integration tests (per the project's integration-test standard), mirroring slice 4.
18+
- **Builds on:** slice 5 (PSL authors M:N → the PSL source can express it) + dispatch 1 (consistent contract source) + slice 4 (the example shape).
19+
- **Hands to:** M:N demonstrated in both demos.
20+
- **Focus:** PSL `Post ↔ Tag` M:N + re-emit; ORM modules mirroring slice 4's; CLI/seed/tests; regenerate migration refs per the demo's current `fixtures/` convention.
21+
22+
## Handoff completeness
23+
24+
Slice-DoD reachable: `test:dual-mode` green (D1) · PG M:N examples per standard (D2). D1 is independent (start anytime); D2 waits on slice 5.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Slice 6: PG demo M:N examples + dual-mode reconciliation
2+
3+
_Parent project: `projects/sql-orm-many-to-many/`. Linear: [TML-2795](https://linear.app/prisma-company/issue/TML-2795). Status: **planned**; M:N-examples half **blocked by slice 5** (TML-2794)._
4+
5+
## At a glance
6+
7+
Bring the M:N demo examples to the **PG** demo (`examples/prisma-next-demo`), mirroring the SQLite demo (slice 4), and reconcile the demo's pre-existing **dual-mode contract drift**. The PG demo emits from PSL, so its M:N examples can't exist until PSL can author M:N (slice 5).
8+
9+
## Chosen design
10+
11+
Two separable halves:
12+
13+
**A. Dual-mode reconciliation (independent — can start before slice 5).** The PG demo is nominally dual-mode (PSL `src/prisma/contract.prisma` + TS `prisma/contract.ts`, expected to emit identically). They're out of sync: the committed contract matches the **PSL** emit; `emit:ts` diverges (drops the discriminated `Task`/`Bug`/`Feature` hierarchy, `displayName`, typed `Address`) → `test:dual-mode` is red on the TS leg. Root cause: the **TS builder can't author the discriminator/`@@base` hierarchy**. Reconcile by one of: (i) add TS-builder discriminator/inheritance authoring (framework feature — bigger), (ii) drop the TS contract source and make the demo PSL-only, or (iii) another agreed approach. Decide at pickup.
14+
15+
**B. M:N examples (blocked by slice 5).** Once PSL authors M:N, add a `Post ↔ Tag` M:N relation to the PSL source (`src/prisma/contract.prisma`) + example ORM modules (include / `some`/`none`/`every` filter / nested `connect`/`disconnect`/`create` via the callback mutator) + CLI commands + seed + integration tests — mirroring slice 4's SQLite modules.
16+
17+
## Scope
18+
19+
**In:** the PG demo only — dual-mode reconciliation (A) + M:N examples on the PSL source (B); integration tests matching the demo's `test/` pattern (`withDevDatabase`).
20+
21+
**Out:** the PSL M:N authoring mechanism itself (slice 5); the SQLite demo (slice 4, done); TS-builder discriminator authoring if reconciliation chooses to drop the TS source instead.
22+
23+
## Pre-investigated edge cases
24+
25+
| Edge case | Disposition |
26+
|---|---|
27+
| Dual-mode `emit:check` only diffs the **default (PSL)** config | `test:dual-mode` (run-suite-against-each-emit) is the real dual-mode gate; reconciliation must make BOTH legs green or remove the TS leg |
28+
| The integration-test standard (whole-row, explicit-select, ≥1 implicit) | applies to the PG M:N examples too (project cross-cutting requirement) |
29+
| PG demo migration fixtures (reorganised on `main` under `fixtures/`) | M:N relation will add migration refs — regenerate per the demo's current convention |
30+
31+
## Slice-specific done conditions
32+
33+
- [ ] `test:dual-mode` green (both legs pass, or the TS source is removed and the demo is cleanly PSL-only).
34+
- [ ] PG demo demonstrates the M:N API (include / filter / nested write) — mirroring slice 4 — via CLI commands, seeded, with integration tests per the standard.
35+
36+
## Open Questions
37+
38+
1. **Dual-mode reconciliation approach** — add TS-builder discriminator authoring (i) vs drop the TS source / PSL-only (ii). Working position: lean (ii) PSL-only unless TS-builder discriminator authoring is wanted for its own sake — but this is the operator's call (it touches the demo's dual-mode story).
39+
40+
## References
41+
42+
- Parent: `projects/sql-orm-many-to-many/spec.md` (§ Follow-on scope, § Cross-cutting integration-test standard). Slice 4 (`TML-2790`) is the example shape to mirror; slice 5 (`TML-2794`) is the blocker for half B.

0 commit comments

Comments
 (0)