Skip to content

TML-2787: M:N slice 3 — nested writes through the junction (DRAFT — type-level disable pending decision)#683

Draft
tensordreams wants to merge 6 commits into
tml-2786-slice-2-filterfrom
tml-2787-slice-3-write
Draft

TML-2787: M:N slice 3 — nested writes through the junction (DRAFT — type-level disable pending decision)#683
tensordreams wants to merge 6 commits into
tml-2786-slice-2-filterfrom
tml-2787-slice-3-write

Conversation

@tensordreams
Copy link
Copy Markdown
Contributor

Slice 3 (final) of the SQL ORM: Many-to-Many End to End project (Linear project). Nested connect/disconnect/create through the junction + the required-payload safety rail.

⚠ DRAFT — not ready to merge. Runtime-complete, but the type-level .create disable (your non-negotiable, in-slice requirement) is deferred pending your decision — it needs a contract-.d.ts change. See Open decision below + wip/unattended-decisions.md #8. I did not make that contract-surface change unilaterally.

Stacked PR. Base tml-2786 (#680) → tml-2785 (#679) → tml-2784 (#678) → tml-2597 (#673) → tml-2729 (#667) → main. Review/merge bottom-up.

Overview

db.orm.User.update/create({ tags: (t) => t.connect/disconnect/create(...) }) now routes to the UserTag junction (INSERT / DELETE / target-insert+link), under both create() and update(). The N:M not supported yet guard is gone. Junctions with a required non-FK payload column cant be written through the sugar, so create and connect on them are disabled with a clear error (disconnect stays).

Changes (5 commits)

  • 74a778816 — runtime junction write path: partitionByOwnership gains a junctionOwned bucket (keyed on through presence); connect→INSERT, disconnect→DELETE, create→target-insert+link, both flows, composite-key AND-ed; the rejection unit test flipped positive. (getRelationDefinitions now carries through.)
  • 926bdc849 — required-payload fixture: User ↔ Role via UserRole(user_id, role_id, level NOT NULL) (canonical CLI emit).
  • 3bccd80b3 — runtime guard: nested create on a required-payload junction throws.
  • e6c641811design correction (decision Remove SQL -> Runtime dependency #9): extended the guard to connect too (connect also INSERTs a junction row it cant complete → DB NOT-NULL violation), flipped the unit test, finished the 10 write integration tests.

Integration tests (per the project standard)

mn-nested-write.test.ts — 10 tests, no skips: connect/create on the pure User.tags junction with whole-row readback via include(tags), both create()+update() flows; disconnect; connect AND create on User.roles throw the guard; disconnect on User.roles works. Whole-row toEqual, explicit .select in most, ≥1 implicit/default selection.

AC status

  • AC-1 — connect/disconnect/create route to junction DML, both flows, guard removed, rejection test flipped (unit + integration).
  • AC-3 — write integration tests per the standard.
  • AC-2 — partial. Runtime disable of create+connect on required-payload junctions: ✅ done + tested. Type-level disable: deferred — see below.

⚠ Open decision (blocks marking this slice done)

The type-level .create disable cant be built as specified: the generated contract.d.ts relation type carries only to/cardinality/on, not through — so a conditional type cant see which model is the junction or that it has a required column. requiredPayloadColumns exists only at runtime. Honouring the type-level disable needs the contract .d.ts type emitter to carry through (a contract-surface change reaching into slice-0 territory). Options (full detail in wip/unattended-decisions.md #8):

  • (a) extend the .d.ts emitter to emit through, then a follow-up adds the conditional-type disable + negative type test;
  • (b) emit a narrower requiredPayloadColumns/hasRequiredPayload marker into the typed relation;
  • (c) accept runtime-only (contradicts the in-slice non-negotiable).

I shipped the runtime guard and left this for you. Once you pick (a)/(b), a small follow-up dispatch completes the type-level disable.

Notes

connect-disabled-on-required-payload was a mid-flight spec correction (the original spec wrongly assumed connect was FK-pair-only safe). The reverse Tag.users/Role.users directions are deferred (one-directional fixture, decision #3). Refs: TML-2787.

Write slice: connect/disconnect/create through the junction + required-payload
.create disable (types+runtime). 4 dispatches (write path / required-payload
fixture / type+runtime disable / integration). Type-disable feasibility risk
pre-named with a halt.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Replace the partitionByOwnership N:M rejection guard with a junctionOwned
bucket. connect/disconnect/create over a through relation now resolve to
junction INSERT/DELETE (create = target-insert then link) in both the
create() and update() graph flows, after the parent PK is known. Composite
keys are AND-ed across all parent/child column pairs; disconnect stays
update()-only. Flip the rejection unit test to a positive junction-DML
assertion and add connect/disconnect/create coverage for both flows.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
… with required payload column; re-emit contract

Adds a Role model (id, name) and UserRole junction (userId, roleId,
level NOT NULL no-default) to the sql-orm-client integration fixture,
plus a User.roles manyToMany relation through UserRole. The required
non-FK column `level` in UserRole exercises the requiredPayloadColumns
path in resolveThrough, which D3 uses to disable direct .create for
junctions with required payload.

Re-emits contract.json + contract.d.ts for both the integration test
copy and the package-local copy (pgvector refs stripped). Change is
additive: existing models and User.tags relation are unchanged.

Verified: resolveThrough('user_roles') → requiredPayloadColumns=['level']
(NOT NULL ∧ no default ∧ not FK).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…umns exist

Throw a clear runtime error when a nested `.create()` targets an M:N
relation whose junction table has non-FK NOT-NULL columns with no default
(i.e. `requiredPayloadColumns` is non-empty). The error names the
relation, the offending column(s), and points to the junction model /
SQL builder as the supported alternative.

`connect` and `disconnect` are unaffected — they only touch the FK pair.
Pure junctions (no required payload) pass through the create path as before.

Adds `requiredPayloadColumns` to the local `JunctionThrough` interface and
copies it from the already-resolved `relation.through` in
`getRelationDefinitions`.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…finish write integration tests

Both `connect` and `create` write a bare junction row the M:N sugar can't
complete when the junction has required non-FK columns (e.g. `user_roles.level
NOT NULL`). Extend the runtime guard in `applyJunctionOwnedMutation` to cover
`connect` in addition to `create`; `disconnect` (DELETE path) is unaffected.

Guard message is unified: "Cannot `<op>` on relation `<rel>`: its junction
`<table>` has required column(s) `<col>` the relation API can't populate.
Use the `<Model>` model directly or the SQL builder."

Unit test that previously asserted connect-on-User.roles succeeds is flipped
to assert rejection. Integration tests are fully enabled (no it.skip); the new
test asserts connect on User.roles throws the guard, while pure-junction
(User.tags) connect/create/disconnect paths continue to work end-to-end.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…-2787)

Write slice artifacts. Spec corrected mid-flight (decision #9: connect also
disabled on required-payload junctions). Type-level .create disable deferred
(decision #8 — needs a contract .d.ts emitter change, operator decision).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 31213a66-acec-4db1-ab43-02b05ea2efcf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2787-slice-3-write

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

size-limit report 📦

Path Size
postgres / no-emit 136.83 KB (+0.39% 🔺)
postgres / emit 126.47 KB (+0.4% 🔺)
mongo / no-emit 75.69 KB (0%)
mongo / emit 70.68 KB (0%)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@683

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@683

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@683

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@683

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@683

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@683

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@683

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@683

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@683

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@683

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@683

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@683

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@683

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@683

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@683

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@683

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@683

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@683

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@683

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@683

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@683

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@683

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@683

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@683

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@683

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@683

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@683

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@683

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@683

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@683

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@683

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@683

prisma-next

npm i https://pkg.pr.new/prisma-next@683

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@683

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@683

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@683

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@683

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@683

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@683

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@683

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@683

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@683

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@683

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@683

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@683

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@683

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@683

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@683

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@683

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@683

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@683

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@683

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@683

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@683

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@683

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@683

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@683

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@683

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@683

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@683

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@683

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@683

commit: 7cbb4a0

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