TML-2943: implicit many-to-many via a synthesised model-less junction#875
TML-2943: implicit many-to-many via a synthesised model-less junction#875tensordreams wants to merge 3 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 |
@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/config-loader
@prisma-next/emitter
@prisma-next/language-server
@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: |
size-limit report 📦
|
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
When both navigable list ends are bare (no `through:`) and no junction model links the pair, the PSL interpreter now synthesises a model-less junction table (Prisma's implicit many-to-many) instead of emitting the orphaned-backrelation diagnostic. Detection lives in `applyBackrelationCandidates`: a bare list with no FK-side match, no authored junction, and no junction near-miss is deferred to `resolveImplicitManyToMany`, which pairs it with its mirror end (or resolves a self-referential list on its own) and emits the `N:M`/`through` descriptor on both ends. D5 precedence is preserved — a both-bare pair with an authored junction model is still recognised, never synthesised. Synthesis injects a junction `ModelNode` named `_<A>To<B>` (terminal model names ordered alphabetically) with foreign-key columns `A` and `B` (A references the first model's id, B the second), a composite `(A, B)` identity, and the two foreign keys; the contract assembler turns it into a storage table and fills the through descriptors' `targetColumns` from the terminal ids. The junction is a physical table only — filtered out of `roots` like an STI variant. Diagnostics: a terminal without a single-column `@id` (`PSL_IMPLICIT_MN_TARGET_NO_ID`), more than one implicit many-to-many between the same pair (`PSL_IMPLICIT_MN_AMBIGUOUS`), and a real table already named like the synthesised junction (`PSL_IMPLICIT_MN_NAME_COLLISION`). Migration DDL and runtime `include` integration are S4·M2. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
S4·M1 synthesises a model-less junction for an implicit many-to-many
(both navigable list ends bare, no junction model). This proves that
junction downstream: the migration system creates it, and the ORM
`include` walks it.
The `mn-psl-implicit` fixture authors `Post.tags Tag[]` / `Tag.posts
Post[]` with no junction model, emitted via the real pipeline for both
postgres and sqlite. The interpreter synthesises `_PostToTag` (composite
PK `(A, B)`, FK `A` → `posts.id`, FK `B` → `tags.id`) and lowers both
ends to `cardinality: 'N:M'` + `through` over it.
Migration DDL: the synthesised junction is a normal contract storage
table, so the migration planner creates it with no special-casing. A
migration test drives the real planner over each emitted contract against
an empty schema and asserts the `_PostToTag` `CREATE TABLE` (composite
primary key + the two foreign keys) is planned for postgres and sqlite.
Integration: an `include('tags')` test over the implicit M:N returns the
related rows (whole-row assertions, explicit + implicit select, PGlite),
walking the synthesised junction through a real emitted PSL contract with
no authored junction model.
Wires both emissions into the sql-orm-client emit script (so
`fixtures:check` regenerates them) and adds `@prisma-next/sqlite` to the
integration test package to emit the sqlite contract.
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
4aeb245 to
f422aef
Compare
02a463c to
1c404f0
Compare
Linked issue
Refs TML-2943. Fourth of the PSL: Directional Relation Syntax stack — stacked on
tml-2942-s3-pointer-disambiguation(TML-2942). Spec underprojects/psl-relation-syntax/slices/04-implicit-mn-synthesis/.At a glance
Both ends bare and no junction model → an implicit many-to-many. The synthesised junction is created by migration (postgres + sqlite) and walked by the ORM, exactly like an authored one.
Summary
Prisma's implicit many-to-many: when both navigable ends are bare lists and no junction model links them, the framework synthesises a model-less junction table (
_<A>To<B>, columnsA/B, composite PK) and lowers the relation toN:M+through. Migration creates the table; the ORM walks it.Decision
Detect the both-bare-no-junction case and synthesise the junction, preserving the precedence from earlier in the project: a
through:uses the named junction; both-bare with an authored junction model recognises it (PR 2's path); only both-bare without one synthesises. The synthesised junction mirrors Prisma's_AToB/A/Bconvention (terminal storage names, alphabetically ordered).How it fits together
packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts): bare lists with no FK match, no authored junction, and no near-miss are grouped by model pair; a mirrored pair (or a self-ref list) synthesises.contract-psl/src/interpreter.ts): inject a junctionModelNode—_<A>To<B>, columnsA/B(FKs to the two model@ids, descriptors copied so types match), composite(A, B)PK — and feed it through the existing contract-builder; filter it fromcontract.roots(the same mechanism STI variants use) so it isn't a queryable entity.includewalks the resultingthrough.Behavior changes & evidence
_PostToTag+N:M/through; precedence preserved.contract-psl/src/interpreter.ts,…/psl-relation-resolution.ts— evidence:packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.implicit-mn.test.ts(toEqualon the emittedContract+ the D5-precedence control + diagnostics for no-@id/ ambiguous / name-collision)._PostToTagon postgres and sqlite. evidence:test/integration/test/sql-orm-client/mn-psl-implicit-migration.test.ts(drives the real planners over the real emitted contract; assertsCREATE TABLE _PostToTag+ composite PK + both FKs on both targets).includewalks the synthesised junction. evidence:test/integration/test/sql-orm-client/fixtures/mn-psl-implicit/contract.prisma+…/mn-psl-implicit-parity.test.ts(PGlite, whole-row).Notes for the reviewer
through.tableto be a declared model, so a raw table would trip that guard. It's filtered fromroots, so it's more hidden than an explicit junction;db.orm._PostToTagremains reachable to exactly the degree STI variants are (rootsis metadata, not the query gate in the SQL family). That is a pre-existing, project-wide property — not a new leak. A truly-db.orm-unreachable junction is a separate ORM-surface change that would also tighten STI variants; flagged as an optional follow-up.@prisma-next/sqlitedev-dep, used solely to emit a real sqlite contract for the DDL assertion (layering-legal;lint:depsclean).check:upgrade-coverageapplies (touchespackages/3-extensions/sql-orm-client) — see Skill update.Testing performed
pnpm --filter @prisma-next/sql-contract-psl test; the migration DDL test + implicit-M:N parity on PGlite. Full-stack pass green (build, typecheck:packages, fixtures:check, lint:deps, cast delta 0).Skill update
n/a for behaviour beyond the additive implicit-M:N support. Touches
packages/3-extensions/sql-orm-client, socheck:upgrade-coverageexpects achanges: []declaration in the in-flight upgrade cycle — flagged as a stack-wide follow-up.Checklist
TML-NNNN: …form.