TML-2941: through: explicit many-to-many junction authoring in PSL#873
TML-2941: through: explicit many-to-many junction authoring in PSL#873tensordreams wants to merge 4 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 📦
|
acd5bd6 to
a3fea80
Compare
| // An explicit `through:` names the junction directly: skip every other | ||
| // junction-shaped model so recognition and near-miss reporting are scoped | ||
| // to the authored junction. A bare list (no `through:`) scans all of them. | ||
| if (input.candidate.through !== undefined && junctionModelName !== input.candidate.through) { |
There was a problem hiding this comment.
through: only affects the junction scan here, but findJunctionFkPairs is reached only after pairMatches is empty in applyBackrelationCandidates. That means an explicit list field like tags Tag[] @relation(through: PostTag) is silently lowered as a direct 1:N if Tag also has any FK back to Post, ignoring the authored junction. through: should either short-circuit into the junction path before direct FK matching, or report a conflict when both a direct FK and the named junction are present.
c8e0273 to
94f373d
Compare
a3fea80 to
f2764a7
Compare
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…e M:N Parse a bare `through: Junction` on a navigable list field into ParsedRelationAttribute.through and thread it onto the backrelation candidate. When present, many-to-many recognition is scoped to the named junction model: findJunctionFkPairs skips every other junction-shaped model, reusing idColumnsAreExactlyFkPair and childColumnsInTargetIdOrder to validate the junction and lowering to the existing N:M + through descriptor the bare-list convention path emits. A one-end-declared M:N (through: on Post.tags, bare Tag.posts) resolves on both ends: the explicit end via the named junction, the bare inverse via the convention scan. A through: naming a non-junction-shaped model yields the junction near-miss diagnostic; a self-relation through: (multiple FK pairs from one junction) defers to the existing ambiguity diagnostic rather than silently picking. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Add the `mn-psl-through` PSL fixture — a User <-> Tag many-to-many spelled with the explicit `tags Tag[] @relation(through: UserTag)` keyword on the User end and a bare `users User[]` inverse on Tag (D4 unambiguous case). The interpreter recognises the M:N via the named junction and lowers both ends to `cardinality: 'N:M'` + `through`, producing a contract structurally identical to the bare-list `mn-psl` fixture. Wire the fixture into the sql-orm-client emit pipeline and add an integration parity test (`mn-psl-through-parity.test.ts`) that drives `db.orm.User.include('tags')` over the emitted contract on the PGlite harness — explicit-select and implicit-select cases, whole-row toEqual. This proves the explicit `through:` source drives the ORM include end-to-end through emit -> runtime, matching how the sibling's M:N tests are parameterised (PGlite only; this suite has no SQLite path). Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…ct (operator) Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
f2764a7 to
2fad31e
Compare
94f373d to
5bfb3ea
Compare
tensordreams
left a comment
There was a problem hiding this comment.
The through: parsing, allow-list extension, and named-junction scoping are clean and the lowering correctly reuses the existing junction-recognition primitives — the equivalence test, both-ends test, two-junction selection, non-junction near-miss, and self-referential ambiguity test are all solid, and the runtime parity fixture is well-structured. However, the spec's edge-case table explicitly lists "through: names a model with no FK to one side" as requiring an actionable diagnostic ("do not silently fall through"), but the implementation only records a near-miss when the named model links both sides; a through: naming a one-sided or non-existent model falls through to the generic PSL_ORPHANED_BACKRELATION_LIST, whose message never mentions through: or the named junction. Separately, a through: on a backrelation list that also matches a 1:N FK pair is silently dropped (no diagnostic). Neither one-sided/non-existent case is covered by tests. Typesafety and the no-bare-casts / no-any rules are respected (the only as casts are in test files, which are exempt).
| // An explicit `through:` names the junction directly: skip every other | ||
| // junction-shaped model so recognition and near-miss reporting are scoped | ||
| // to the authored junction. A bare list (no `through:`) scans all of them. | ||
| if (input.candidate.through !== undefined && junctionModelName !== input.candidate.through) { |
There was a problem hiding this comment.
[bug] Scoping recognition to the named junction is correct, but the near-miss is only recorded when the named model links both sides (the recording lives inside the parentFk/childFk double loop). When through: names a model that has an FK to only one side, or names a model that doesn't exist / declares no FKs, no near-miss is recorded and the candidate falls through to the generic PSL_ORPHANED_BACKRELATION_LIST — whose message ("has no matching FK-side relation ... use an explicit join model") never mentions through: or the named junction. I verified this: through: NoSuchJunction and a one-sided through: PostTag (FK to Post only) both emit PSL_ORPHANED_BACKRELATION_LIST.
The slice spec's edge-case table (projects/psl-relation-syntax/slices/02-through-explicit-mn/spec.md) explicitly lists "through: names a model that isn't junction-shaped (no FK to one side / @@id ≠ the two FKs)" as requiring an actionable diagnostic that reuses the near-miss reasons — "do not silently fall through." The @@id ≠ two FKs sub-case is handled (the existing test covers it); the "no FK to one side" sub-case is not. Consider recording a dedicated near-miss / PSL_THROUGH_JUNCTION_NOT_FOUND diagnostic when candidate.through is set and the named model isn't recognised as a junction for this candidate, so the message can name the through: value and point the user at the missing FK side.
| field, | ||
| targetModelName: field.typeName, | ||
| ...ifDefined('relationName', relationName), | ||
| ...ifDefined('through', through), |
There was a problem hiding this comment.
[question] When this candidate also matches a 1:N FK pair (applyBackrelationCandidates takes the matches.length === 1 path at psl-relation-resolution.ts:675), through is carried on the candidate but never consulted — the relation lowers as cardinality: '1:N' and the through: declaration is silently dropped. I verified: comments Comment[] @relation(through: NoSuchJunction) on a Post whose Comment has an FK back resolves to a 1:N with no diagnostic. through: is M:N-only per the spec, so a co-occurrence with a 1:N FK match is a user mistake (likely a typo'd junction name); silently honouring the 1:N over the explicit through: masks it. Worth at least a diagnostic when through is set on a candidate that resolves via the 1:N FK-pair path, or documenting that through: is ignored once a 1:N match exists.
| }); | ||
| }); | ||
|
|
||
| it('emits the junction near-miss diagnostic when through: names a non-junction model', () => { |
There was a problem hiding this comment.
[test] The non-junction near-miss test covers the @@id ≠ two FKs sub-case (model links both sides). Two related shapes are untested and currently regress to the generic PSL_ORPHANED_BACKRELATION_LIST:
through:naming a model that links only one side (FK to the candidate's model but not the target, or vice-versa) — the spec lists this as requiring an actionable diagnostic.through:naming a non-existent model.
Adding these would lock in whichever behaviour is decided for the [bug] above (a dedicated diagnostic vs. the generic orphaned message) and prevent a silent regression if the near-miss surface is later broadened.
Linked issue
Refs TML-2941. Second of the PSL: Directional Relation Syntax stack — stacked on
tml-2940-s1-from-to-fk-foundation(TML-2940); review/merge that first. Spec underprojects/psl-relation-syntax/slices/02-through-explicit-mn/.At a glance
through: UserTagrecognises the many-to-many via the named junction and lowers it to the sameN:M+throughcontract the runtime already consumes — byte-identical to the bare-list form.Summary
A navigable list field may name its junction explicitly with
@relation(through: Junction), rather than relying on convention-based discovery. Declared on one end; the opposite bare list resolves as the inferred inverse. This is the explicit-authoring path that the disambiguation work (PR 3) extends.Decision
Recognise an explicit
through: Junctionon a navigable list and lower it to the existingN:M+throughdescriptor. The runtime is unchanged — this PR only changes how that descriptor is authored. The bare-list convention recognition (a junction model present, discovered by shape) is preserved, so a one-end-declared M:N resolves on both ends.How it fits together
throughjoins the@relationargument allow-list (next tofrom/to); the resolver reads a bare model name intoParsedRelationAttribute.through.findJunctionFkPairs(packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts), a singlecontinuefilter narrows the junction search to the named model whenthrough:is set, reusing every existing junction-shaped validation and themanyToManyRelationNodemachinery — so the emitted descriptor is byte-identical to the convention path.through:is emitted and exercised through the ORMinclude.Behavior changes & evidence
through: Junctionlowers byte-identically to the bare-list form; a non-junction model gets an actionable diagnostic.packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts— evidence:packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.through.test.ts(toEqualon the emittedContract).through:-authored M:N drivesdb.orm.<Model>.include(...). evidence:test/integration/test/sql-orm-client/fixtures/mn-psl-through/contract.prisma+test/integration/test/sql-orm-client/mn-psl-through-parity.test.ts(PGlite, whole-rowtoEqual, explicit + implicit select).Notes for the reviewer
through:on a field that also has a direct FK match would be silently ignored — but a navigable list field is structurally excluded from FK-side matches, so this is unreachable by any sensible schema (it's the "never silently ignore an authored junction" guard, deferred). Disambiguation of self-referential / multiple-between-same-models M:N is PR 3.check:upgrade-coverage: this PR touchespackages/3-extensions/sql-orm-client(the integration fixture's emit wiring), so the gate will want achanges: []declaration under the in-flight upgrade cycle — see Skill update.Testing performed
pnpm --filter @prisma-next/sql-contract-psl test; the M:N integration parity suite on PGlite. Validated in the full-stack pass (build, typecheck:packages, fixtures:check, lint:deps, cast delta 0).Skill update
n/a for behaviour beyond the additive
through:authoring surface, runtime unchanged. This PR touchespackages/3-extensions/sql-orm-client, socheck:upgrade-coverageexpects achanges: []declaration in the in-flight…/upgrades/<cycle>/instructions.md. Flagged as a stack-wide follow-up (the four touching PRs share it); happy to add it on request.Checklist
through:recognition).TML-NNNN: …form.