TML-2787: M:N slice 3 — nested writes through the junction#683
Conversation
|
Warning Review limit reached
More reviews will be available in 59 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds M:N nested mutation support to the SQL ORM client by routing junction/through relations through a new ChangesM:N Nested Mutations with Junction Tables
ExecutionMutationDefault ref namespace field
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
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
…no-op main moved under this branch: the enum refactor dropped PostgresEnumStorageEntry from sql-contract/types, and per-namespace resolution renamed ModelsOf -> NamespaceModelsOf(<ns>). Update the test contract-builder import and the junction-payload ModelNameForTable type accordingly. Record the namespace-scoped execution-default ref change as an incidental substrate diff in the end-user 0.13->0.14 upgrade notes (re-emit picks it up; no user action) so check:upgrade-coverage passes. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
These two integration tests append an execution-default to the contract at runtime (a tags.updated_at @updatedat default; a user_tags.created_at onCreate default). After the ref gained a required namespace, the structural validator rejects the namespace-less refs. Add namespace: "public". Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The flat sql-builder write path called applyMutationDefaults with no namespace, so a multi-namespace contract with two same-named tables could apply the wrong namespace's execution default (or none at all). The ORM path already threaded it everywhere; only the lane did not (F01). Forward `namespace: namespaceId` from buildParamValues/buildSetExpressions, and make MutationDefaultsOptions.namespace required so a forgotten namespace is a type error rather than a silent degrade to table-name-only matching (F02) — the runtime matcher now filters on namespace unconditionally. Regression test: two same-named `users` tables in `public`/`auth` with per-namespace execution defaults; each insert/update now picks up its own namespace's default and not the collision twin's. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The guard's remediation advice told users to write the related model (e.g. `Role`) directly, but the unfillable required column lives on the junction table (`user_roles`), not the related model. Writing `Role` would never populate the junction payload. Point the advice at the junction `through.table` instead, and pin the wording in the create/connect guard tests. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The cast-smuggling section listed `blindAs`, which does not exist in `@prisma-next/utils/casts` — the only exported helpers are `blindCast` and `castAs`. Reference the real helpers so the forbidden-pattern enumeration is accurate. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…resolution Table names are unique per namespace, not globally — the storage IR and runtime already key tables by namespace, and the low-level buildSqlContractFromDefinition path already accepts same-named tables in different namespaces. The DSL/emit path did not: - The table-uniqueness check keyed `tableOwners` by bare table name, so two models mapping `public.users` and `shadow.users` were rejected. Key it by `(namespace, table)`. - M:N junction resolution lost the junction's namespace: the lowered `through` node carried only a table name, and `buildThroughDescriptor` re-derived the namespace by bare table name (last-wins). With two `user_roles` junctions this emitted the wrong `through.namespaceId` for both. Carry the junction's namespace on the `through` node and resolve `through.namespaceId ?? defaultNamespaceId`. Value-identical for existing single-namespace contracts (no fixture drift). Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…ate-disconnect Two type-level corrections to the nested-write mutators, mirroring the runtime: - The junction-payload required-column gate (`HasRequiredJunctionPayload`) matched execution defaults by `(table, column)` only, dropping the junction namespace even though `ExecutionMutationDefault.ref` is namespace-scoped (runtime twin: the sql-builder/applyMutationDefaults namespace fix). In a multi-namespace contract an execution default for `public.user_roles.token` could make `shadow.user_roles.token` look optional, wrongly enabling create/connect on the shadow relation. Thread the junction's namespace through the payload-field helpers and include `ref.namespace` in `HasExecutionCreateDefault`. - `MutationCreateInput` typed junction `disconnect` as available, but `createGraph` rejects any nested disconnect during `create()`. Add a create/update context axis to `RelationMutator` so disconnect is unavailable in create input; it stays available in update input. Tested against a new emitted two-namespace fixture (`junction-namespaces`) where `public`/`shadow` share the `user_roles` junction but only `public` carries the execution default. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
`withMutationScope` only opened a transaction when the runtime exposed a top-level `transaction()`, but no runtime does — `transaction()` lives on a connection (`connection().transaction()`). So every ORM mutation, including multi-statement nested-write graphs, ran without a transaction: a failure after the first write left a partial write behind. Open a connection and run the whole graph inside its transaction (falling back to a bare scope only when the runtime exposes no connection, e.g. unit-test stubs). The integration harness is reworked to match: the `@prisma/dev` server is PGlite-backed (one concurrent connection), so the wrapper now drives a single `pg.Client` (not a pool) through the direct driver, and its `connection()` / `transaction()` keep recording executions so the per-test execution-count assertions still see reads and writes. New regression test: a nested M:N tag create that fails (duplicate pk) after the parent insert now rolls back the parent and the junction. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…n emitted fixture The execution-default junction-payload test hand-cloned the emitted contract and mutated `storage`/`execution` directly (dropping `user_tags.created_at`'s storage default and pushing an execution default by hand) — exactly the pattern the no-contract-data-patching rule forbids. Replace it with a dedicated emitted fixture (`execution-defaulted-tags`) authored through the DSL, where `user_tags.created_at` is NOT NULL with an execution-time onCreate default and no storage default. The test now builds its runtime/context from that emitted contract; the scenario comes from a shape the authoring surface can produce. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The sql-orm-client integration runtime now drives a single long-lived client so transactions hold one connection on the single-backend PGlite server. The dev server's default 1s idle timeout reaps that client during brief idle windows under full-suite load (a pool would reconnect; a lone client cannot), causing intermittent "connection terminated" flakes. Raise the idle timeout to 30s for these tests — no test holds the connection idle anywhere near that long. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…main Rebasing onto main picked up TML-2916 (un-namespaced PG models default to `public`, dropping the spurious empty `__unbound__` storage slot). Re-emit the slice's own fixtures (`junction-namespaces`, `execution-defaulted-tags`) so they match the new emitter output; storage hashes update accordingly. Lockfile records the `@prisma-next/sql-errors` dependency at the 0.14.0 workspace version. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
ffb6131 to
39562fd
Compare
Now that main is 0.14.0, the upgrade-coverage gate keys the in-flight transition to 0.14→0.15. Move the namespace-scoped execution-default-ref note out of the released 0.13-to-0.14 upgrade and into new 0.14-to-0.15 directories for both the user and extension-author skills. The change is an incidental substrate diff (re-emit picks up the new contract ref shape; no consumer action), so each ships `changes: []`. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…ack version The supabase example contract embeds the supabase extension pack version, which the 0.14.0 release bump left at the stale `0.13.0` — `fixtures:check` re-emits it to `0.14.0`. Re-emit so the committed contract matches; no schema change. 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>
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 at both the type level and runtime with a clear error (disconnect stays).Changes
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.)User ↔ RoleviaUserRole(user_id, role_id, level NOT NULL)(canonical CLI emit).createandconnecton a required-payload junction throw (connect also INSERTs a junction row it cant complete → DB NOT-NULL violation). Disconnect stays allowed.HasRequiredJunctionPayload+HasJunctionThroughcomposeLinkWritesDisabledandBareDisconnectDisabledonRelationMutator, disablingcreate/connectand baredisconnect()at the type level for required-payload junctions. The.d.tsemitter now carriesthrough(namespace-scoped), so the gate derives required-payload columns from the junction model's field types withoutany. Covered by type tests (junction-link-write-disable.test-d.ts).isUniqueConstraintViolationuses driver-normalizedSqlQueryError.sqlState; duplicate-connect wraps with a domain error naming the junction; shared-column conflicts detected on both INSERT and DELETE sides; metadata-length assertions fail fast; preflight detects duplicate resolved targets.MutationDefaultsOptionscarriesnamespace;ModelNameForTableandJunctionPayloadFieldNamesare namespace-aware; execution-default and storage-default columns are excluded fromrequiredPayloadColumns.withMutationScope.Integration tests (per the project standard)
mn-nested-write.test.ts— connect/create/disconnect on the pureUser.tagsjunction and the required-payloadUser.rolesjunction, bothcreate()andupdate()flows; duplicate-connect rejection; missing-target rejection; parent-insert-failure-after-connect-preflight (no partial writes); execution-defaulted junction payload column; baredisconnect()type error on junctions; whole-rowtoEqual, explicit.selectin most, ≥1 implicit/default selection.AC status
create+connecton required-payload junctions + type-level disable (create/connect→never, baredisconnect()→ type error on junctions). Both implemented and tested.Notes
connect-disabled-on-required-payload was a mid-flight spec correction (the original spec wrongly assumed connect was FK-pair-only safe). Duplicateconnecterrors deliberately rather than silently succeeding (intentional divergence from Prisma-classic implicit-M:Nconnect, which is idempotent viaON CONFLICT DO NOTHING). The reverseTag.users/Role.usersdirections are deferred (one-directional fixture, decision #3). Refs: TML-2787.Summary by CodeRabbit
create,connect, anddisconnect.