Skip to content

TML-2787: M:N slice 3 — nested writes through the junction#683

Open
tensordreams wants to merge 23 commits into
mainfrom
tml-2787-slice-3-write
Open

TML-2787: M:N slice 3 — nested writes through the junction#683
tensordreams wants to merge 23 commits into
mainfrom
tml-2787-slice-3-write

Conversation

@tensordreams

@tensordreams tensordreams commented Jun 1, 2026

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.

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.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enabled nested create, connect, and disconnect operations for many-to-many relationships via junction tables.
    • Added compile-time type checking to prevent invalid operations on junction relations with required fields.
  • Improvements

    • Enhanced runtime validation with clear error guidance for missing required junction payload fields.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'TML-2787: M:N slice 3 — nested writes through the junction' accurately and specifically describes the main change: implementing many-to-many nested write operations that route through junction tables.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 156.34 KB (+0.79% 🔺)
postgres / emit 123.64 KB (+1.02% 🔺)
mongo / no-emit 76.92 KB (0%)
mongo / emit 71.01 KB (0%)
cf-worker / no-emit 183.82 KB (0%)
cf-worker / emit 147.7 KB (0%)

@pkg-pr-new

pkg-pr-new Bot commented Jun 1, 2026

Copy link
Copy Markdown

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/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@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: 1f24648

@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from 92d1741 to 25a3c7e Compare June 2, 2026 12:40
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from 7cbb4a0 to de78282 Compare June 2, 2026 12:40
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from 25a3c7e to 06fc270 Compare June 2, 2026 13:07
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from de78282 to 3e1c908 Compare June 2, 2026 13:07
@tensordreams tensordreams marked this pull request as ready for review June 2, 2026 13:08
@tensordreams tensordreams requested a review from a team as a code owner June 2, 2026 13:08
@tensordreams tensordreams changed the title TML-2787: M:N slice 3 — nested writes through the junction (DRAFT — type-level disable pending decision) TML-2787: M:N slice 3 — nested writes through the junction Jun 2, 2026
@tensordreams

Copy link
Copy Markdown
Contributor Author

Updated: the type-level .create/.connect disable is now implemented (escalation #8 resolved via the chosen approach — the .d.ts emitter now carries through, landed in slice 0; the type gate derives required-payload purely at the type level, no any). AC-2 is closed at both the runtime and type levels. The whole stack has also been rebased onto the latest origin/main (clean — no fixture re-emit cascade). Negative type test: junction-link-write-disable.test-d.ts. No longer a draft.

@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from 06fc270 to bc2d4b9 Compare June 3, 2026 08:51
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch 2 times, most recently from 308b48d to bb3e246 Compare June 3, 2026 11:32
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch 2 times, most recently from 3982e13 to b1dadcf Compare June 4, 2026 15:12
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from bb3e246 to 47cf59e Compare June 4, 2026 15:12
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from b1dadcf to df900cd Compare June 4, 2026 15:41
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from 47cf59e to b04a59c Compare June 4, 2026 15:41
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from df900cd to ee44053 Compare June 5, 2026 12:44
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from b04a59c to 04522c0 Compare June 5, 2026 12:44
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from ee44053 to 65d4fe3 Compare June 5, 2026 14:21
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from 04522c0 to 4d8dad2 Compare June 5, 2026 14:21
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from 65d4fe3 to bd47c00 Compare June 5, 2026 15:02
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from 4d8dad2 to 16dc690 Compare June 5, 2026 15:02
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from 16dc690 to f3a7a5c Compare June 8, 2026 10:19
@tensordreams tensordreams force-pushed the tml-2786-slice-2-filter branch from bd47c00 to 40b8cbe Compare June 8, 2026 10:19
@tensordreams tensordreams force-pushed the tml-2787-slice-3-write branch from f3a7a5c to b772b54 Compare June 8, 2026 11:19
@tensordreams

Copy link
Copy Markdown
Contributor Author

On it 👍 — re: the review-body finding "Duplicate-connect error message can be misleading": softening the connect unique-violation wrap so it no longer asserts the junction link is already present (a non-PK unique constraint on the junction could be the real cause); it will say a unique constraint on the junction was violated and that the link may already be present. Unit + integration message assertions updated accordingly.

@tensordreams

Copy link
Copy Markdown
Contributor Author

Done in c274f1e95 — re: the review-body finding "Duplicate-connect error message can be misleading": the connect unique-violation wrap now says the mutation 'violated a unique constraint on junction "

"; the junction link may already be present' instead of asserting the link already exists. Unit assertions (duplicate-key wrap + SQLITE_CONSTRAINT_PRIMARYKEY wrap) and the integration already-linked-tag test updated to the new wording.

@tensordreams

Copy link
Copy Markdown
Contributor Author

On it 👍 — re: the decision-log note: recording duplicate-connect-errors as an intentional divergence from Prisma-classic implicit-M:N connect (which is idempotent via ON CONFLICT DO NOTHING) in the slice 03 spec's decision surface.

@tensordreams

Copy link
Copy Markdown
Contributor Author

Done in 07379d4f8 — re: the decision-log note: the slice 03 spec (projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/spec.md) now records duplicate-connect-errors as an intentional divergence from Prisma-classic implicit-M:N connect (idempotent ON CONFLICT DO NOTHING): a note on the connect design bullet plus an edge-case row with the rationale (portable write path, surfaces caller bugs) and the revisit condition. The wording reflects the softened error message from A25c (c274f1e95).

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>
…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>
…sted-write test; re-emit fixtures with through blocks

Plain `string` constants failed the integration typecheck (tsc) because
Tag.id / Role.id are `Char<36>`.  Brand TAG_RUST, TAG_TS, ROLE_ADMIN,
ROLE_EDITOR at declaration (matching the pattern from create.test.ts).
Runtime values are unchanged.

Also commits the re-emitted contract.d.ts fixtures carrying the new
`through` blocks on the N:M relations (User.tags / User.roles).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…elations at the type level

Nested `.create` on an N:M relation whose junction carries a required
non-FK payload column now resolves its argument to `never`, surfacing a
compile-time error instead of only the runtime guard in
`mutation-executor.ts`. `connect`/`disconnect` typing is unchanged.

The derivation resolves the relation `through` to its junction model,
treats any non-join column as a payload column, and disables create when
any payload column is required (not nullable, no default). `User.roles`
(junction `user_roles` with required `level`) disables create; `User.tags`
(pure junction) keeps it.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Renames the `CreateDisabled` flag on `RelationMutator` to
`LinkWritesDisabled` and applies the same `never`-arg guard to both
`create` and `connect` overloads. `disconnect` is unaffected (it issues
a DELETE, not an INSERT, so no payload column is needed).

`HasRequiredJunctionPayload` is already threaded as the flag via
`RelationMutationCallback` — no change required there. The runtime
guard in `mutation-executor.ts` already blocks both operations; the
type system now matches.

Test file renamed to `junction-link-write-disable.test-d.ts`; the
former "connect remains available" positive test is replaced with a
`@ts-expect-error` assertion, and a new pure-positive disconnect test
preserves coverage of the allowed path.

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

The type-level .create/.connect disable (now in dist after the main rebase)
makes those calls compile errors; cast the args to `never` to still exercise
the runtime guard on a real DB (defense-in-depth). Surfaced by rebuilding
against origin/main.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…fter TML-2841 rebase

The write path gained namespace coordinates: insertSingleRow,
findRowByCriterion, toFieldName, compileInsertCount, compileDeleteCount
all take a leading namespaceId. Add namespaceId to the local
JunctionThrough, thread parentNamespaceId into applyJunctionOwnedMutation /
readJunctionParentValues, and use relation.relatedNamespaceId /
through.namespaceId for the related-row and junction-DML calls. Add the
namespaceId arg to the M:N write tests.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
updateFirstGraph now mirrors createGraph and runs
preflightJunctionOwnedCreateMutation over the junction-owned bucket
before the scalar-update block. Without it, an update()-flow junction
rejection (required-payload guard, metadata shape, unresolved connect
target) landed after the scalar UPDATE had already persisted; ordering
is the only partial-write protection on runtimes without transaction().

Pinned by a unit test asserting no UPDATE executes when the junction
guard fires, and an update()-flow integration test asserting the scalar
update does not survive a junction rejection.

Addresses PR 683 review thread r3404036584.

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

The junction-owned create bucket was the only one that did not recurse:
parent-owned and child-owned nested creates route their payloads through
createGraph, but the junction bucket called insertSingleRow directly, so
a junction-created target carrying its own relation mutation (type-legal
per RelationMutationCreate.data) passed through mapModelDataToStorageRow
untouched and died in normalizeInsertRows with an unknown-column error.

Swapping insertSingleRow for createGraph is drop-in (same returned row
shape readJunctionTargetValues needs) and buys bucket parity. Pinned by
a unit test where a junction-created Child carries an N:1 owner connect
that now resolves into the child INSERT.

Addresses PR 683 review thread r3404036572.

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

Duplicate criteria in a single connect([...]) (or two criteria resolving
to the same row) defeated the preflight: each criterion resolved
independently and passed, the parent INSERT landed, the first link
INSERTed, and the second threw the duplicate-link error — leaving the
parent plus the first link behind, the exact partial-write shape the
preflight exists to prevent.

The preflight now keys each resolved target tuple and fails fast on a
duplicate before any write, consistent with duplicate connect being a
domain error in this slice. Pinned by a unit test asserting the
rejection lands with zero INSERTs executed.

Addresses PR 683 review thread r3404036592.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Adds the create()-flow ordering edge case from review: a connect
preflight that resolves its target successfully, followed by a parent
INSERT failure (duplicate users pkey), must leave no junction rows and
no parent mutation behind. Ordering is the only partial-write protection
on the integration runtimes, so this pins the guarantee the rest of the
suite leans on.

Addresses the ordering finding in PR 683 review 4486308660.

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

Both the type gate and the runtime guard treat execution-defaulted junction
payload columns as satisfied, but the junction INSERT built the row from the
FK pairs only — the exempted case passed both gates and then hit a NOT NULL
violation on the database. Mirror insertSingleRow: merge
context.applyMutationDefaults({ op: create, table: through.table }) into the
junction row before compileInsertCount.

The reworked unit test builds its ExecutionContext from the patched contract
(the defaults applier is a closure over the contract the context was created
from, so spreading a patched contract over an existing context never sees the
new default) and asserts the junction row includes the defaulted column. A
new integration test drops the DB-side default on user_tags.created_at and
covers an execution-defaulted junction payload column end-to-end.

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

The constraint failed message arm matched every SQLite constraint class —
NOT NULL, FOREIGN KEY, and CHECK failures during a junction connect INSERT
were rewritten into the duplicate-link error, pointing users at a duplicate
that is not the problem. The arm was also redundant for genuine unique
violations: SQLite says UNIQUE constraint failed, which the unique
constraint arm already matches.

Drop the over-broad arm and recognize SQLITE_CONSTRAINT_PRIMARYKEY next to
SQLITE_CONSTRAINT_UNIQUE (better-sqlite3 reports junction-PK duplicates —
the common M:N shape — with the PRIMARYKEY code). Unit tests pin both
boundaries: a NOT NULL constraint failure passes through unwrapped, and the
PRIMARYKEY code is recognized on its own with a message no string arm
matches.

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

The shared-column conflict guard existed on the INSERT side only:
insertJunctionLink merges parent/target values through writeJunctionColumn
and throws on mismatch, while deleteJunctionLink emitted one predicate per
side — a mismatched shared column produced contradictory predicates
(tenant_id = 7 AND tenant_id = 8), so the DELETE matched nothing and
disconnect silently no-oped where connect throws.

deleteJunctionLink now builds the merged row via writeJunctionColumn and
derives the WHERE from it: the mismatched case throws the same conflict
error as connect (before any DELETE executes), and the values-equal case
drops the redundant duplicate predicate. Both pinned by unit tests.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The connect unique-violation wrap asserted "the junction link is already
present", but when the junction table carries unique constraints beyond its
PK the underlying violation may not be about the link being connected at
all — the message attributed every unique violation to a duplicate link.

Reword to state what is actually established: a unique constraint on the
junction was violated, and the link may already be present. Unit and
integration message assertions updated to the new wording.

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

DisconnectMutator reshaped disconnect for every relation, but nothing pinned
that the bare disconnect() overload stays accepted for plain 1:N relations —
if HasJunctionThrough ever mis-resolved toward true for non-junction
relations, 1:N disconnect-all would silently drop out of the typed surface.
A positive update-input test on User.posts pins it.

HasRequiredJunctionPayload was only exercised on the required-to-disabled
side. Three new positive tests pin the optional-payload arms of
IsOptionalCreateField in isolation: a junction whose only payload column is
nullable (user_tags.note), one whose only payload column carries a storage
default (user_tags.created_at), and one whose required payload column is
satisfied solely by an execution onCreate default (user_roles.level — the
type-level counterpart of the insertJunctionLink defaults fix). Verified
non-vacuous by temporarily removing the storage-default arm from
IsOptionalCreateField, which turns the storage-default pin red.

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

Duplicate M:N connect deliberately errors with a wrapped unique-violation
domain error where Prisma-classic implicit-M:N connect is idempotent
(ON CONFLICT DO NOTHING). Per round-3 review, record the divergence in the
slice 03 decision surface: a note on the connect design bullet plus an
edge-case row carrying the rationale (portable write path, surface caller
bugs) and the revisit condition.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
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