Skip to content

TML-2942: disambiguate relations by pointing, retire @relation(name:)#874

Open
tensordreams wants to merge 7 commits into
tml-2941-s2-through-explicit-mnfrom
tml-2942-s3-pointer-disambiguation
Open

TML-2942: disambiguate relations by pointing, retire @relation(name:)#874
tensordreams wants to merge 7 commits into
tml-2941-s2-through-explicit-mnfrom
tml-2942-s3-pointer-disambiguation

Conversation

@tensordreams

@tensordreams tensordreams commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Refs TML-2942. Third of the PSL: Directional Relation Syntax stack — stacked on tml-2941-s2-through-explicit-mn (TML-2941). Spec under projects/psl-relation-syntax/slices/03-pointer-disambiguation/.

At a glance

// self-referential M:N — disambiguated by pointing at the junction's FK field
model User {
  following User[] @relation(through: Follow.follower)
  followers User[] @relation(through: Follow.followee)
}

// 1:N with two relations to the same model — disambiguated by pointing at the FK field
model User {
  authoredPosts Post[] @relation(inverse: author)
  editedPosts   Post[] @relation(inverse: editor)
}

Disambiguation is by pointing at the relation field, not by a free-floating @relation(name:) string. name: is rejected at parse time — in both the SQL and Mongo family resolvers — with a PSL_LEGACY_RELATION_NAME diagnostic directing authors to inverse: or through: Junction.field.

Summary

Replaces the name-string disambiguation mechanism with directional pointers: through: Junction.relationField for ambiguous many-to-many (self-relations, or multiple M:N between the same models), and inverse: <fkField> for a 1:N back-relation with multiple candidates. @relation(name:) is rejected at parse time in both families (the clean-break decision, D1 reversed) — not merely dropped from output. The Mongo family resolver gains inverse: support (it previously relied on name: for disambiguation).

Decision

Three pieces, all in service of "disambiguate by pointing":

  1. Member-access value grammar in @prisma-next/psl-parser — an @relation argument value may be a qualified Foo.bar. (This also makes the to: Model.id qualifier work, resolving the S1-deferred edge case.)
  2. through: Junction.relationField resolves an ambiguous M:N by pinning the parent-side junction FK leg.
  3. inverse: <fkField> resolves a 1:N back-relation by pinning the owning FK field.

@relation(name:) (and the positional @relation("...") form) is rejected at parse time in both the SQL and Mongo resolvers with PSL_LEGACY_RELATION_NAME. The contract infer printer emits inverse: on disambiguated back-relations; the entire relationName plumbing is removed from the SQL contract pipeline. The Mongo family's inverse: support mirrors SQL's (with a PSL_INVERSE_FIELD_NOT_FK diagnostic).

How it fits together

  1. Grammar (packages/1-framework/2-authoring/psl-parser/src/parse.ts): a member-access expression parses Foo.bar, preserving both segments; the resolver receives the full dotted string and splits it.
  2. M:N disambiguation (contract-psl/src/psl-relation-resolution.ts): through.field pins the parent-side FK by declaring-field name, resolving the self-referential / multiple-M:N case that previously fell into the ambiguity diagnostic.
  3. 1:N disambiguation (same file): an inverse: branch selects the FK-side relation by declaring-field name, running before the ambiguity branches.
  4. Legacy rejection (both psl-relation-resolution.ts and packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts): @relation(name:) and positional @relation("...") produce PSL_LEGACY_RELATION_NAME; fields:/references: produce PSL_LEGACY_FIELDS_REFERENCES (carried from S1).
  5. Printer (packages/2-sql/9-family/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts): emits inverse: on a disambiguated back-relation instead of name:; the relationName plumbing is wholly removed.
  6. Mongo (psl-helpers.ts): gains inverse: reading + rejection of legacy name:/positional, mirroring SQL.

Behavior changes & evidence

  • through: J.field disambiguates a self-referential / multiple M:N that is otherwise PSL_AMBIGUOUS_BACKRELATION_LIST. contract-psl/src/psl-relation-resolution.ts — evidence: packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.through.test.ts (the ambiguous-without-the-qualifier control proves the qualifier is what resolves it).
  • inverse: <fkField> disambiguates a 1:N back-relation; inverse: ≡ legacy name: (byte-identical lowering). evidence: packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.inverse.test.ts.
  • Legacy @relation(name:) rejected in both SQL and Mongo; the contract infer printer never emits name:. evidence: packages/2-sql/9-family/test/psl-contract-infer/print-psl/print-psl.relations.test.ts (grep gate).
  • to: Model.id qualifier now works (deferred from S1, resolved by the member-access grammar). to: Post.idto: id, byte-identical.
  • Both forms drive the ORM (self-ref M:N + disambiguated 1:N). evidence: test/integration/test/sql-orm-client/self-ref-mn-through-parity.test.ts and …/disambiguated-1n-inverse-parity.test.ts (PGlite, asymmetric seeds so a mis-paired pointer fails).

Notes for the reviewer

  • Largest PR in the stack — unified by one theme (disambiguate by pointing), but it spans the parser, the resolver, the printer, and both families (SQL + Mongo). The integration tests use deliberately asymmetric data so a swapped pointer surfaces wrong rows.
  • The through: value split (junction = head, field = tail) deliberately does not route through stripModelQualifier, whose "keep the tail" semantics are correct for from:/to: columns but wrong for through:.
  • The relationName plumbing (the relationName field, its wiring through the contract-builder, its printer consumption) is wholly removed — no dangling reads remain.
  • The Mongo inverse: addition is the replacement for the Mongo family's previous name:-only disambiguation; the PSL_INVERSE_FIELD_NOT_FK diagnostic mirrors SQL's.
  • check:upgrade-coverage applies (touches packages/3-extensions/sql-orm-client) — see Skill update.

Testing performed

  • pnpm --filter @prisma-next/sql-contract-psl test, @prisma-next/psl-parser test, @prisma-next/family-sql test, @prisma-next/mongo-family test; the relation/M:N integration suite on PGlite. Validated after the rebase: pnpm typecheck:packages (129/129), pnpm --filter -next/sql-contract-psl test (23 files, 314 tests), pnpm fixtures:check (zero drift), and pnpm lint:deps (0 violations).

Skill update

This PR rejects @relation(name:) input (a breaking change for both SQL and Mongo PSL authors) and adds inverse: to Mongo. Touches packages/3-extensions/sql-orm-client, so check:upgrade-coverage expects a changes: [] declaration in the in-flight upgrade cycle — flagged as a stack-wide follow-up.

Checklist

  • All commits signed off (DCO).
  • Read CONTRIBUTING.md; scoped to one logical concern (pointer disambiguation + name: rejection).
  • Tests updated.
  • Title in TML-NNNN: … form.
  • Skill update section filled in.

@tensordreams tensordreams requested a review from a team as a code owner June 26, 2026 14:48
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ff661643-1f18-4251-bfba-3f22c0e8dc89

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2942-s3-pointer-disambiguation

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.

@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@874

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@874

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@874

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@874

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@874

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@874

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@874

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@874

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@874

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@874

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@874

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@874

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@874

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@874

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@874

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@874

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@874

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@874

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@874

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@874

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@874

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@874

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@874

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@874

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@874

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@874

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@874

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@874

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@874

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@874

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@874

@prisma-next/config-loader

npm i https://pkg.pr.new/@prisma-next/config-loader@874

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@874

@prisma-next/language-server

npm i https://pkg.pr.new/@prisma-next/language-server@874

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@874

prisma-next

npm i https://pkg.pr.new/prisma-next@874

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@874

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@874

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@874

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@874

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@874

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@874

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@874

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@874

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@874

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@874

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@874

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@874

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@874

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@874

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@874

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@874

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@874

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@874

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@874

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@874

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@874

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@874

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@874

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@874

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@874

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@874

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@874

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@874

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@874

commit: b52910d

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 160.36 KB (+0.01% 🔺)
postgres / emit 147.55 KB (0%)
mongo / no-emit 79.86 KB (0%)
mongo / emit 72.68 KB (0%)
cf-worker / no-emit 188.11 KB (-0.01% 🔽)
cf-worker / emit 173.58 KB (+0.01% 🔺)

@tensordreams tensordreams force-pushed the tml-2941-s2-through-explicit-mn branch from acd5bd6 to a3fea80 Compare July 1, 2026 10:56
@tensordreams tensordreams force-pushed the tml-2942-s3-pointer-disambiguation branch from 4aeb245 to f422aef Compare July 1, 2026 10:56
...ifDefined('referencesInferred', referencesInferred),
...ifDefined('through', through && through.length > 0 ? through : undefined),
...ifDefined('through', through),
...ifDefined('inverse', inverse !== undefined && inverse.length > 0 ? inverse : undefined),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accepts through: and inverse: on the same list relation and carries both into the candidate. Resolution then checks candidate.inverse first and continues after building a 1:N, so the through: half is silently ignored. Since these two arguments describe different relation shapes, please reject the combination in parseRelationAttribute (and add a negative test) instead of letting priority order decide.

@tensordreams tensordreams force-pushed the tml-2941-s2-through-explicit-mn branch from a3fea80 to f2764a7 Compare July 1, 2026 14:35
@tensordreams tensordreams force-pushed the tml-2942-s3-pointer-disambiguation branch from f422aef to b253568 Compare July 1, 2026 14:35
…n, retire name:)

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The attribute-argument-value grammar carried only the head identifier of a
dotted value: `through: Junction.field` parsed as the bare `Junction`,
dropping `.field`. Extend the identifier-leading value alternative so a
qualified `Identifier.Identifier` parses as a `QualifiedName` chain that
preserves every segment, reusing the `Dot` handling that type annotations and
call names already share.

The resolved argument value renders the node's full source, so
`getNamedArgument(attr, 'through')` returns the dotted string `"Foo.bar"` for
`through: Foo.bar`; the downstream resolver splits on `.`. Bare identifiers
(`through: Foo`) stay `Identifier` nodes and bracketed lists (`from: [a, b]`)
are unaffected.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Resolve self-relation and multiple-M:N-between-same-models ambiguity by
pinning the parent-side junction FK to the relation field named in a
qualified `through: Junction.relationField`. The S3 member-access grammar
now delivers the full dotted value to the resolver, so:

- `ParsedThrough` captures `{ junction, field? }`. `through:` is parsed by
  `parseThroughArgument` (junction = head, field = tail) rather than
  routed through `stripModelQualifier`, whose after-the-dot semantics are
  correct only for `from:`/`to:` (column is the tail), not `through:`
  (junction is the head). `field` is threaded onto
  `ModelBackrelationCandidate`.
- `findJunctionFkPairs` pins the parent-side FK by junction relation-field
  name (`declaringFieldName === field`), and reports an actionable
  `PSL_JUNCTION_THROUGH_FIELD_NOT_FK` near-miss when the named field is not
  a parent-side junction FK back to the candidate.
- Stale comments describing the head-only grammar are refreshed; the
  `to:` member-access test now asserts the tolerated-qualifier behaviour
  the built grammar makes live (`to: User.id` lowers like `to: id`).

A bare `through: J` still defers to `PSL_AMBIGUOUS_BACKRELATION_LIST` when
ambiguous; the bare-list convention path is unchanged.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
A one-to-many back-relation list field that matches multiple FK-side
relations on the target model is ambiguous and today requires
@relation(name:) on both sides. Add inverse: <fkFieldName> to pin the
back-relation to the FK-side relation whose declaring field it names —
the directional replacement for name: on the 1:N back side.

- Add inverse to the @relation arg allow-list; parse it as a bare
  relation-field name onto ParsedRelationAttribute.inverse and thread it
  onto the back-relation candidate.
- In FK-side matching, when the candidate carries inverse: F, select the
  FK-side relation whose declaring field is F, resolving the ambiguity
  that today needs name:.
- Diagnostic PSL_INVERSE_FIELD_NOT_FK when inverse: names a field that is
  not an FK-side relation back to the candidate.

name:-based disambiguation stays accepted as legacy input.

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

The contract infer printer disambiguated relations between the same pair of
models with a free-floating @relation(name: "...") string. Replace it with the
pointer forms: the FK side already carries from/to, and the back/list side now
emits inverse: <fkField> pointing at the FK-side relation field it pairs with.

inferRelations threads the FK-side field name onto the back-relation as
inverseOf whenever disambiguation is required (multiple FKs between the same
models, or a self-relation); buildRelationField emits inverse: from it and no
longer emits name: on either side.

Legacy @relation(name:) remains accepted as input and is left untouched by
format (auto-conversion to pointer form is a deferred follow-up).

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

Adds two PSL fixtures + integration tests proving the S3 pointer-disambiguation
forms drive the ORM end-to-end (PGlite, whole-row toEqual, explicit + implicit
select):

- disambiguated-1n-inverse: two relations between User and Post
  (Post.author / Post.editor, both N:1 FKs to User) with the User
  back-relations disambiguated by @relation(inverse: author/editor).
  include() over each back-relation returns the rows joined through the FK
  the inverse: pointer names (authoredPosts → author_id, editedPosts →
  editor_id).

- self-ref-mn-through: a self-referential M:N (User.following /
  User.followers through the Follow junction) disambiguated by
  @relation(through: Follow.follower / Follow.followee). include() over each
  end drives the self-join of the junction + target onto the same users
  table and returns the correct per-user-distinct rows.

Both fixtures are emitted through the real contract-emit pipeline (wired into
the sql-orm-client emit script) and deserialized at module load, so each test
also confirms the disambiguated contract round-trips the full sql contract
validation pipeline.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…sambiguate with inverse:/through: J.field

Complete the clean break begun in S1 (rejecting fields:/references:): the
positional @relation("x") and the named @relation(name: "x") forms are now
rejected as input in both the SQL and Mongo families, with a guiding
PSL_LEGACY_RELATION_NAME diagnostic pointing at the directional replacements.

Disambiguation is by pointing only: inverse: <fkField> for a 1:N back-relation
with multiple candidates, through: Junction.field for an ambiguous many-to-many.
The SQL family already had both pointer forms (S2/S3); the Mongo family gains
inverse: parsing and matching here (it has no junction-based M:N recognition, so
only the 1:N disambiguator applies). The relation-name string never reached the
emitted contract, so the name:->inverse:/through: migration of the test schemas
and the relation-backrelation-list parity fixture lowers byte-identically
(fixtures:check clean).

The legacy-name plumbing (relationName on ParsedRelationAttribute,
FkRelationMetadata, and the backrelation candidates) is removed in both families.
The two ambiguity diagnostics now recommend inverse:/through: instead of
@relation(name:).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
@tensordreams tensordreams force-pushed the tml-2942-s3-pointer-disambiguation branch from b253568 to b52910d Compare July 1, 2026 14:40
@tensordreams tensordreams force-pushed the tml-2941-s2-through-explicit-mn branch from f2764a7 to 2fad31e Compare July 1, 2026 14:40

@tensordreams tensordreams left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slice 3 lands the pointer-based disambiguation cleanly: the member-access grammar, inverse:/through: J.field recognition, actionable near-miss diagnostics, and the printer retiring name: are all correct and well-paired with parity fixtures. The implementation faithfully realises the settled design (design-notes D1 reversal / D2 — legacy name: is rejected with a guiding diagnostic). The findings below are a spec/doc inconsistency inside the PR's own artifacts and two test-coverage gaps rather than correctness defects: the back-relation lowering, junction leg pinning, and round-trip-via-ORM parity all hold.

| Self-referential M:N (two FKs from the junction to the same model) | the core `through: J.field` case — both ends pin their leg; this is what slice 2 deferred |
| `through: J.field` where `field` isn't a junction FK back to the candidate | actionable diagnostic (not silent) |
| `inverse:` naming a field that isn't an FK-side relation to the candidate | actionable diagnostic |
| legacy `@relation(name:)` schema | still parses; lowers as today; survives `format` unchanged (not auto-converted) |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[doc] This edge-case row (and the DoD tick on line 43, "legacy name: still parses and survives format") contradicts the settled design this slice implements. design-notes.md D1 was reversed on 2026-06-26 to a clean break ("legacy fields:/references: and @relation(name:) are rejected at parse time") and D2 states legacy name: "is rejected at parse time (per D1 clean break), not merely dropped from output." The implementation matches D1/D2 — parseRelationAttribute in both SQL (psl-relation-resolution.ts) and Mongo (psl-helpers.ts) emits PSL_LEGACY_RELATION_NAME and returns undefined, and tests assert rejection. Update the spec's edge-case disposition to "rejected with PSL_LEGACY_RELATION_NAME guiding authors to inverse:/through:" and drop the format-survival DoD, per .agents/rules/doc-maintenance.mdc.

expect(result).toContain('inverse:');
});

it('points each back-relation at the FK-side field it pairs with', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[test] The printer's inverse: emission (asserted here) and inferRelations's inverseOf wiring (asserted in relation-inference.test.ts) are each tested in isolation, but nothing re-parses the printed @relation(inverse: sender) PSL and asserts it lowers back to the same contract. A mis-paired inverseOf (e.g. the back-relation pointing at the wrong childRelFieldName) would pass both unit tests yet fail to round-trip. The MULTI_FK_BETWEEN_SAME_MODELS IR is the natural seed: print it, feed the printed PSL through interpretPslDocumentToSqlContract, and assert the lowered User back-relations match the original pairings (messagesauthor_id, messagesMessagerecipient_id). This is the print-side analogue of S5·M5's parser→resolver parity.

);
});

it('emits an actionable diagnostic when inverse: names a field that is not an FK-side relation', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[test] Two cases the PR context calls out for test completeness aren't covered here. (a) Two back-relation list fields pointing at the SAME inverse: (x Post[] @relation(inverse: author) and y Post[] @relation(inverse: author)): both currently resolve and emit two relation nodes over the same FK with no duplicate/collision diagnostic — the slice's own pre-investigated edge table says "a relation that gratuitously specifies inverse:/through: … lean accept", but two SEPARATE back-relations colliding on one FK is a different shape; a test pins the intended behaviour either way. (b) inverse: on a composite-FK back-relation (from: [a, b]): the disambiguator is name-based so it should resolve, but no composite-FK + inverse: test exercises it. Add both so the inverse: contract is fully bounded.

// back-relation: a bare relation-field name (no member-access grammar). It
// pins the back-relation to one of several relations linking the same pair of
// models, the directional replacement for a name-based disambiguator.
const inverseRaw = getNamedArgument(input.attribute, 'inverse');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] The new member-access grammar accepts Identifier.Identifier as a value for any argument, and inverse: takes the raw string here (inverseRaw.trim()). So inverse: Post.author round-trips through the resolver as the literal field name "Post.author", which can never match declaringFieldName, yielding PSL_INVERSE_FIELD_NOT_FK mentioning "Post.author". The spec describes inverse: as "a bare relation-field name (no member-access grammar)" — should the resolver validate bare-only and emit a clearer PSL_INVALID_ATTRIBUTE_ARGUMENT for a dotted inverse: value (mirroring how through: explicitly splits the head/tail in parseThroughArgument), rather than surfacing the confusion as a not-an-FK diagnostic?

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