TML-2787: M:N slice 3 — nested writes through the junction#683
TML-2787: M:N slice 3 — nested writes through the junction#683tensordreams wants to merge 11 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
size-limit report 📦
|
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
92d1741 to
25a3c7e
Compare
7cbb4a0 to
de78282
Compare
25a3c7e to
06fc270
Compare
de78282 to
3e1c908
Compare
|
Updated: the type-level |
06fc270 to
bc2d4b9
Compare
308b48d to
bb3e246
Compare
3982e13 to
b1dadcf
Compare
bb3e246 to
47cf59e
Compare
b1dadcf to
df900cd
Compare
47cf59e to
b04a59c
Compare
df900cd to
ee44053
Compare
b04a59c to
04522c0
Compare
ee44053 to
65d4fe3
Compare
04522c0 to
4d8dad2
Compare
65d4fe3 to
bd47c00
Compare
4d8dad2 to
16dc690
Compare
16dc690 to
f3a7a5c
Compare
bd47c00 to
40b8cbe
Compare
f3a7a5c to
b772b54
Compare
67fb93c to
9feb292
Compare
97f065f to
308befd
Compare
9feb292 to
facaadf
Compare
308befd to
e9d57ac
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/integration/test/sql-orm-client/mn-nested-write.test.ts (1)
306-317: ⚡ Quick winAssert no partial writes after guard rejections.
Line 306 and Line 334 only verify the thrown error text. Add post-failure DB assertions to guarantee the rejected nested write didn’t persist partial parent/junction changes.
Proposed test hardening
@@ await expect( users.create({ @@ }), ).rejects.toThrow(/required column.*`level`/); + + const userRows = await runtime.query<{ id: number }>('select id from users'); + const roleRows = await runtime.query<{ id: string }>('select id from roles'); + const junctionRows = await runtime.query<{ user_id: number; role_id: string }>( + 'select user_id, role_id from user_roles', + ); + expect(userRows).toEqual([]); + expect(roleRows).toEqual([]); + expect(junctionRows).toEqual([]); @@ await expect( users.create({ @@ }), ).rejects.toThrow(/required column.*`level`/); + + const userRows = await runtime.query<{ id: number }>('select id from users'); + const roleRows = await runtime.query<{ id: string }>('select id from roles'); + const junctionRows = await runtime.query<{ user_id: number; role_id: string }>( + 'select user_id, role_id from user_roles', + ); + expect(userRows).toEqual([]); + expect(roleRows).toEqual([{ id: ROLE_ADMIN }]); + expect(junctionRows).toEqual([]);Also applies to: 334-345
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/integration/test/sql-orm-client/mn-nested-write.test.ts` around lines 306 - 317, The test currently only asserts the thrown error from users.create with the nested roles.create and doesn't verify the DB state; after the expect(...).rejects.toThrow(...) add assertions that the attempted parent/junction records were not persisted (e.g. call users.findUnique({ where: { id: 1 } }) or users.findMany(...) and roles.findMany(...) and assert they return null/empty), and repeat the same post-failure DB checks for the other case referenced (the block around lines 334-345) so both rejected nested-write scenarios confirm no partial writes occurred.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/integration/test/sql-orm-client/mn-nested-write.test.ts`:
- Around line 306-317: The test currently only asserts the thrown error from
users.create with the nested roles.create and doesn't verify the DB state; after
the expect(...).rejects.toThrow(...) add assertions that the attempted
parent/junction records were not persisted (e.g. call users.findUnique({ where:
{ id: 1 } }) or users.findMany(...) and roles.findMany(...) and assert they
return null/empty), and repeat the same post-failure DB checks for the other
case referenced (the block around lines 334-345) so both rejected nested-write
scenarios confirm no partial writes occurred.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 39ff8f77-4bad-45e8-96a3-49cc35485a18
⛔ Files ignored due to path filters (9)
projects/sql-orm-many-to-many/learnings.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/dispatches/01-write-path.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/dispatches/02-required-payload-fixture.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/dispatches/03-runtime-disable.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/dispatches/04-integration-tests.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/dispatches/04-integration-tests.r2.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/plan.mdis excluded by!projects/**projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/spec.mdis excluded by!projects/**projects/sql-orm-many-to-many/trace.jsonlis excluded by!projects/**
📒 Files selected for processing (6)
packages/3-extensions/sql-orm-client/src/mutation-executor.tspackages/3-extensions/sql-orm-client/src/types.tspackages/3-extensions/sql-orm-client/test/junction-link-write-disable.test-d.tspackages/3-extensions/sql-orm-client/test/mutation-executor.test.tstest/integration/test/sql-orm-client/mn-nested-write.test.tstest/integration/test/sql-orm-client/runtime-helpers.ts
facaadf to
679770d
Compare
58b53c5 to
7001dbe
Compare
679770d to
302e804
Compare
7001dbe to
f159866
Compare
302e804 to
100b1d5
Compare
f159866 to
2828012
Compare
100b1d5 to
91b8cc4
Compare
| for (const [column, value] of parentPkValues.entries()) { | ||
| junctionRow[column] = value; | ||
| } | ||
| for (const [column, value] of targetPkValues.entries()) { |
There was a problem hiding this comment.
For composite M:N relations where the junction reuses a column on both sides, this merge lets the target row overwrite the parent value. The fixture added in this stack has exactly that shape (parentColumns: [tenant_id, parent_id], childColumns: [tenant_id, child_id]): connecting parent (tenant_id=7, id=1) to target (tenant_id=8, id=10) would insert { tenant_id: 8, parent_id: 1, child_id: 10 }, which no longer points at the parent row we just updated, or trips FK constraints when they exist. The junction merge should detect duplicate junction columns, require the values to match, and keep the same behavior for deletes; a mismatched shared-column test would catch this.
There was a problem hiding this comment.
On it. I will reject shared junction-column value mismatches when merging link values. 👍
tensordreams
left a comment
There was a problem hiding this comment.
Reviewed this PR separately against tml-2786-slice-2-filter (origin/tml-2786-slice-2-filter...origin/tml-2787-slice-3-write). I left one inline correctness comment on the junction-row merge for composite M:N relations with shared columns. Verification while reviewing: pnpm --filter @prisma-next/sql-orm-client test and git diff --check for this PR diff passed.
tensordreams
left a comment
There was a problem hiding this comment.
Reviewed this slice against projects/sql-orm-many-to-many/slices/03-nested-write-through-junction/spec.md and the stacked diff vs tml-2786-slice-2-filter.
Well-executed slice that conforms closely to its spec: partitionByOwnership gains the third junctionOwned bucket with a proper hasThrough type guard, junction writes run after the parent PK is known in both create() and update() flows, and the required-payload disable exists at both the type level (with genuine positive and negative .test-d.ts coverage) and at runtime with a clear error naming the columns and the escape hatch. Atomicity rides the pre-existing withMutationScope (transactional when the runtime provides transaction(), sequential fallback otherwise — a pre-existing characteristic, not a regression). Production code correctly uses blindCast with reasons (no bare as, no any); runtime-guard tests bypass the type gate with test-file-exempt casts; test conventions are observed; runtime-helpers.ts extends the existing shared integration helper rather than duplicating it.
One finding I'd want resolved (or explicitly deferred with a spec note) before merge: the undefined duplicate-connect behavior, since it's a first-class user-facing path of this feature. The rest are hardening at the edges — see inline comments.
| async function insertJunctionLink( | ||
| scope: RuntimeScope, | ||
| context: ExecutionContext, | ||
| through: JunctionThrough, | ||
| parentPkValues: Map<string, unknown>, | ||
| targetPkValues: Map<string, unknown>, | ||
| ): Promise<void> { | ||
| const junctionRow: Record<string, unknown> = {}; | ||
| for (const [column, value] of parentPkValues.entries()) { | ||
| junctionRow[column] = value; | ||
| } | ||
| for (const [column, value] of targetPkValues.entries()) { | ||
| junctionRow[column] = value; | ||
| } | ||
|
|
||
| const compiled = compileInsertCount(context.contract, through.namespaceId, through.table, [ | ||
| junctionRow, | ||
| ]); | ||
| await executeQueryPlan<Record<string, unknown>>(scope, compiled).toArray(); | ||
| } |
There was a problem hiding this comment.
major: insertJunctionLink compiles a plain INSERT (compileInsertCount, no ON CONFLICT), so connect-ing an already-connected row — or repeating the same criterion in one connect([...]) call — surfaces as a raw junction-PK unique-violation from the driver rather than either idempotent success or a domain error naming the relation. Duplicate-link behavior is currently undefined and untested (no unit or integration test connects the same pair twice). Please pick a behavior deliberately — idempotent insert (ON CONFLICT DO NOTHING semantics, matching Prisma's implicit-M:N connect) or a wrapped error naming the relation — and pin it with a test. If it's deferred to a later slice, a code comment + spec note would help.
There was a problem hiding this comment.
On it. I will define duplicate junction connect behavior and add coverage. 👍
| if (!mutation.criteria || mutation.criteria.length === 0) { | ||
| throw new Error( | ||
| `disconnect() nested mutation for relation "${relation.relationName}" requires criterion`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
minor: Type/runtime mismatch for bare disconnect(): the RelationMutator interface still exposes the zero-arg disconnect() overload for junction relations, and for child-owned (1:N) relations it means "disconnect all" — but the junction path unconditionally throws requires criterion. So t.disconnect() on an M:N relation compiles and always fails at runtime. Either implement disconnect-all for junction relations (DELETE FROM junction WHERE parentCols = parentPk, symmetric with the 1:N behavior) or gate the no-arg overload off at the type level like you did for create/connect. If you keep the throw, the message should say criteria are required for M:N relations — "requires criterion" reads like a missing-argument bug.
There was a problem hiding this comment.
On it. I will align the bare M:N disconnect type/runtime behavior in this PR. 👍
| TContract, | ||
| JunctionModel, | ||
| Through['parentColumns'][number] | Through['childColumns'][number] | ||
| >]: IsOptionalCreateField<TContract, JunctionModel, F> extends true ? never : F; |
There was a problem hiding this comment.
minor: The type gate and the runtime guard disagree on execution-time create defaults. HasRequiredJunctionPayload treats a junction field as optional when IsOptionalCreateField is true, which includes HasExecutionCreateDefault (extension onCreate defaults). The runtime's requiredPayloadColumns (collection-contract.ts:371) only checks storage-level col.default — and insertJunctionLink never calls context.applyMutationDefaults (unlike insertSingleRow, line 948). Net effect: a junction payload column with an execution-only create default passes the type gate but throws the runtime guard. Suggest making insertJunctionLink apply mutation defaults for the junction table and excluding execution-defaulted columns from requiredPayloadColumns, or excluding HasExecutionCreateDefault from the type gate's optionality check so both layers agree.
There was a problem hiding this comment.
On it. I will make required junction payload checks agree on execution-time create defaults. 👍
| type ModelNameForTable<TContract extends Contract<SqlStorage>, TableName extends string> = { | ||
| [M in keyof ModelsOf<TContract> & string]: ModelsOf<TContract>[M] extends { | ||
| readonly storage: { readonly table: TableName }; | ||
| } | ||
| ? M | ||
| : never; | ||
| }[keyof ModelsOf<TContract> & string]; | ||
|
|
There was a problem hiding this comment.
minor: ModelNameForTable resolves the junction model by scanning all models for a matching storage.table name, ignoring namespaces — a bare-table-name assumption the runtime side just removed (junction DML threads through.namespaceId everywhere after the TML-2841 rebase). Two models in different namespaces with the same table name would make this resolve to a union of model names, producing an unpredictable gate. The emitted through type carries namespaceId, so the fix is cheap: match on readonly storage: { readonly table: TableName; readonly namespaceId: Through['namespaceId'] } and pass the namespace through from HasRequiredJunctionPayload.
There was a problem hiding this comment.
On it. I will make the junction model lookup namespace-aware in the type gate. 👍
| for (let i = 0; i < relation.through.childColumns.length; i++) { | ||
| const junctionColumn = relation.through.childColumns[i]; | ||
| const targetColumn = relation.through.targetColumns[i]; | ||
| if (!junctionColumn || !targetColumn) { |
There was a problem hiding this comment.
minor: readJunctionParentValues/readJunctionTargetValues silently continue when through.parentColumns/childColumns and their paired arrays disagree in length. For insertJunctionLink that yields a NOT-NULL violation (noisy but safe); for deleteJunctionLink a skipped pair drops a WHERE predicate, widening the DELETE to unlink more rows than requested. Contract validation should make misalignment impossible, but since the failure mode here is silent data loss rather than an error, prefer throwing on length mismatch instead of skipping.
There was a problem hiding this comment.
On it. I will make the junction paired-column readers fail fast on metadata length mismatches. 👍
| // connect — create() parent flow | ||
| // =========================================================================== | ||
|
|
||
| it( |
There was a problem hiding this comment.
minor (coverage): Failure paths for junction connect/disconnect against a nonexistent target are untested. The runtime path exists (mutation-executor.ts:768-772, did not find a matching row) but neither this file nor mutation-executor.test.ts exercises it for the junction-owned bucket (the existing not-found tests cover the parent-owned path only). Please add at least one test — e.g. users.create({..., tags: (t) => t.connect({ id: 'missing' })}) asserting the rejection, ideally also asserting no partial junction row is left behind, which would double as coverage of the rollback behavior.
There was a problem hiding this comment.
On it. I will add missing-target connect/disconnect coverage and no-partial-write assertions. 👍
| const tagCriterion = { id: 'featured' } as { readonly id: NonNullable<TagCreate['id']> }; | ||
|
|
||
| test('nested create on a relation whose junction has a required payload column is a type error', () => { | ||
| type Input = MutationCreateInput<Contract, 'User'>; |
There was a problem hiding this comment.
nit: All gate assertions here go through MutationCreateInput. The gate also applies to MutationUpdateInput (both reach RelationMutator via the shared RelationMutationFields/RelationMutationCallback), but that's structural rather than asserted — a refactor that gives update its own callback type would silently lose the gate. Two cheap cases (mutator.connect forbidden, disconnect allowed, via MutationUpdateInput<Contract, 'User'>) would pin it.
There was a problem hiding this comment.
On it. I will add MutationUpdateInput coverage for the junction relation mutator gate. 👍
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>
…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>
91b8cc4 to
4ea74ff
Compare
|
On it. I will add post-failure DB assertions for the required-payload guard cases. 👍 |
Slice 3 (final) of the SQL ORM: Many-to-Many End to End project (Linear project). Nested
connect/disconnect/createthrough the junction + the required-payload safety rail.Overview
db.orm.User.update/create({ tags: (t) => t.connect/disconnect/create(...) })now routes to theUserTagjunction (INSERT / DELETE / target-insert+link), under bothcreate()andupdate(). TheN:M not supported yetguard is gone. Junctions with a required non-FK payload column cant be written through the sugar, socreateandconnecton them are disabled with a clear error (disconnect stays).Changes (5 commits)
74a778816— runtime junction write path:partitionByOwnershipgains ajunctionOwnedbucket (keyed onthroughpresence); connect→INSERT, disconnect→DELETE, create→target-insert+link, both flows, composite-key AND-ed; the rejection unit test flipped positive. (getRelationDefinitionsnow carriesthrough.)926bdc849— required-payload fixture:User ↔ RoleviaUserRole(user_id, role_id, level NOT NULL)(canonical CLI emit).3bccd80b3— runtime guard: nestedcreateon a required-payload junction throws.e6c641811— design correction (decision Remove SQL -> Runtime dependency #9): extended the guard toconnecttoo (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 pureUser.tagsjunction with whole-row readback viainclude(tags), bothcreate()+update()flows;disconnect; connect AND create onUser.rolesthrow the guard;disconnectonUser.rolesworks. Whole-rowtoEqual, explicit.selectin most, ≥1 implicit/default selection.AC status
create+connecton required-payload junctions: ✅ done + tested. Type-level disable: deferred — see below.⚠ Open decision (blocks marking this slice done)
The type-level
.createdisable cant be built as specified: the generatedcontract.d.tsrelation type carries onlyto/cardinality/on, notthrough— so a conditional type cant see which model is the junction or that it has a required column.requiredPayloadColumnsexists only at runtime. Honouring the type-level disable needs the contract.d.tstype emitter to carrythrough(a contract-surface change reaching into slice-0 territory). Options (full detail inwip/unattended-decisions.md#8):.d.tsemitter to emitthrough, then a follow-up adds the conditional-type disable + negative type test;requiredPayloadColumns/hasRequiredPayloadmarker into the typed relation;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 reverseTag.users/Role.usersdirections are deferred (one-directional fixture, decision #3). Refs: TML-2787.Summary by CodeRabbit
Release Notes
New Features
Improvements