Skip to content

TML-2940: from/to FK relation syntax — clean break, reject legacy fields/references#872

Open
tensordreams wants to merge 16 commits into
mainfrom
tml-2940-s1-from-to-fk-foundation
Open

TML-2940: from/to FK relation syntax — clean break, reject legacy fields/references#872
tensordreams wants to merge 16 commits into
mainfrom
tml-2940-s1-from-to-fk-foundation

Conversation

@tensordreams

@tensordreams tensordreams commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Refs TML-2940. First of a five-PR stack for the PSL: Directional Relation Syntax project (plan anchor TML-2939); this one bases on main. Spec / design notes under projects/psl-relation-syntax/.

At a glance

// the only accepted FK syntax — `to:` omitted ⇒ target @id
user User @relation(from: userId)

Legacy @relation(fields:, references:) is rejected at parse time with a PSL_LEGACY_FIELDS_REFERENCES diagnostic directing authors to from:/to:. The format command no longer rewrites relation keywords (it cannot operate on now-invalid syntax). The repo's own schemas — SQL and Mongo families, including examples, fixtures, and tests — are migrated to from:/to: byte-identically (fixtures:check zero drift). A reusable downstream codemod is deferred (TML-2957).

Summary

PSL foreign-key relations gain a directional @relation(from:, to:) vocabulary — the sole accepted syntax. Legacy fields:/references: is rejected at parse time in both the SQL and Mongo family resolvers. The contract infer AST printer emits the canonical spelling. The repo's ~40 legacy sites (SQL + Mongo) are migrated in-stack; migration-history snapshots re-emit byte-identically (the migration hash is over migration.json/ops.json, not the .prisma source).

Design reversal: the original plan kept fields:/references: as backward-compatible input with a format-based auto-rewrite. The operator reversed that decision (2026-06-26) in favour of a clean break — the prototype is pre-1.0, breaking changes are acceptable. Decision D1 and the format canonicalisation pass (D3) were reversed; see projects/psl-relation-syntax/design-notes.md.

Decision

Ship from/to as the only FK relation syntax — read, print — with legacy fields:/references: rejected at parse time. Three faces:

  1. The resolver reads from/to with inference: omit to: ⇒ the target's @id; a single field is bare, composites bracketed. Legacy fields:/references: produces a PSL_LEGACY_FIELDS_REFERENCES diagnostic.
  2. contract infer emits from/to (the format rewrite pass was built and then removed when D1 was reversed — format does layout only, no relation-keyword rewrite).
  3. The repo's schemas are migrated to from/to (SQL and Mongo). A coupled topological-ordering surface in @prisma-next/psl-printer (hasFullRelation) was taught the canonical keys so inferred-model ordering survives.

The @relation attribute grammar is already generic (named args scanned by string), so no parser/grammar change to accept the new keywords.

How it fits together

  1. Resolver (packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts and packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts) reads from/to; legacy fields/references is rejected with PSL_LEGACY_FIELDS_REFERENCES. Omit-to: resolves the target @id at the call site (which holds the target model).
  2. Printer (packages/2-sql/9-family/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts) emits canonical keys; hasFullRelation in @prisma-next/psl-printer was taught the canonical keys so the FK dependency edge for topological model ordering survives.
  3. Repo migration — 29 SQL-family files and 15 Mongo-family files migrated fields:/references:from:/to:, a symmetric keyword swap. Migration-history snapshots re-emit byte-identically.

Behavior changes & evidence

  • @relation(from:, to:) authors FK relations; to: inference from the target @id. packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts — evidence: packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.from-to.test.ts (asserts toEqual on the emitted Contract).
  • Legacy fields:/references: rejected in both resolvers. psl-relation-resolution.ts (SQL) and psl-helpers.ts (Mongo) — evidence: interpreter.relations.from-to.test.ts (the rejection diagnostic is asserted).
  • contract infer emits canonical; no legacy keys survive. sql-schema-ir-to-psl-ast.ts — evidence: packages/2-sql/9-family/test/psl-contract-infer/print-psl/print-psl.single-dialect-gate.test.ts.
  • Repo migration byte-identical. fixtures:check shows zero contract drift across all migrated schemas (SQL + Mongo).

Notes for the reviewer

  • The PR contains both the original implementation (legacy aliased, format rewrite) and the rework (legacy rejected, format removed) — the built-then-deleted formatter commit (feat(psl-parser): canonicalise @relation FK keywords on format) is in the history. Consider squashing at merge so only the final state lands.
  • The hasFullRelation change in psl-printer is the one cross-package touch; without it, renaming the printer's keys silently drops the FK dependency edge used for topological model ordering.
  • A redundant Model. qualifier on to: (to: Post.id) works as of S3 (member-access value grammar); on S1 alone, it's bare-column only (the test assert the S1 boundary).
  • The deferred downstream codemod is TML-2957.

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 — green.
  • 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 introduces a breaking change to PSL syntax (fields:/references:from/to), affecting packages/3-extensions/ and all downstream PSL authors. check:upgrade-coverage expects a changes: declaration in the in-flight upgrade cycle — flagged as a stack-wide follow-up (the PRs touching packages/3-extensions/ share it). A user-facing docs note and the deferred codemod (TML-2957) are project close-out items.

Checklist

  • All commits signed off (DCO).
  • Read CONTRIBUTING.md; scoped to one logical concern (the from/to FK foundation + clean break).
  • Tests updated.
  • Title in TML-NNNN: … form.
  • Skill update section filled in.

Summary by CodeRabbit

  • New Features
    • Added support for the newer directional relation syntax in schema authoring, including simpler relation declarations and automatic inference in some cases.
  • Bug Fixes
    • Improved relation validation and error messages for incomplete or invalid relation definitions.
    • Better handling of relation ordering so referenced models are printed and resolved correctly.
  • Documentation
    • Updated examples and guides to use the new relation syntax throughout.

@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

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates Prisma relation attribute syntax from the legacy fields/references argument keys to canonical from/to keys across the PSL parser, printer, SQL and Mongo contract interpreters, tests, documentation, and example schemas, adding inference of to from a target model's @id when omitted.

Changes

Core relation parsing, lowering, and printing

Layer / File(s) Summary
Printer relation ordering
packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts, packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
hasFullRelation now checks from/to (accepting legacy fields/references aliases) for dependency-edge completeness; a new test verifies model ordering with canonical relation keys.
SQL relation parsing and lowering
packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts, .../src/interpreter.ts
parseRelationAttribute rejects legacy fields/references, parses from/to, sets referencesInferred when to is omitted, and adds resolveTargetIdFieldNames; the interpreter validates from, infers reference fields from target @id, and updates diagnostic messages.
SQL relation test suite migration
packages/2-sql/2-authoring/contract-psl/test/*.test.ts, test/fixtures.ts, README.md
Relation tests, namespace tests, diagnostics tests, provider tests, parity tests, README examples, and a new from-to test suite are updated/added to cover canonical syntax, inference, and diagnostics.
Mongo relation parsing and lowering
packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts, .../src/interpreter.ts, .../test/interpreter.test.ts
parseRelationAttribute and resolveTargetIdFieldNames are added/rewritten for from/to parsing and inference; the interpreter computes mapped local/target fields and updates orphaned-backrelation diagnostics; tests cover legacy rejection and inference.

Documentation updates

Layer / File(s) Summary
ADR and subsystem doc examples
docs/architecture docs/adrs/ADR 121...md, ADR 226...md, docs/architecture docs/subsystems/*.md, skills/prisma-next-contract/SKILL.md
Prisma schema examples in architecture docs and the skill doc are rewritten from fields/references to from/to.

Example projects and integration fixtures migration

Layer / File(s) Summary
Example and fixture schema updates
examples/*/contract.prisma, packages/1-framework/3-tooling/cli/.../code-templates.ts, test/integration/test/**/*.prisma, *.psl, *.test.ts
Relation directives across example apps, migration snapshots, CLI starter templates, and integration test fixtures are switched from fields/references to from/to.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • prisma/prisma-next#756: Both PRs modify cross-contract-space relation handling in packages/2-sql/2-authoring/contract-psl/src/interpreter.ts.
  • prisma/prisma-next#891: Both PRs modify the same SQL relation parsing pipeline (psl-relation-resolution.ts/interpreter.ts), with this PR extending it for from/to support.

Suggested reviewers: aqrln, wmadden

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adopting from/to foreign-key relation syntax and rejecting legacy fields/references.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2940-s1-from-to-fk-foundation

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@872

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/config-loader

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

@prisma-next/emitter

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

@prisma-next/language-server

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: dfd2de0

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 149.75 KB (0%)
postgres / emit 127.19 KB (0%)
mongo / no-emit 98.36 KB (0%)
mongo / emit 89.39 KB (0%)
cf-worker / no-emit 176.55 KB (0%)
cf-worker / emit 151.7 KB (0%)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts (1)

272-276: 🎯 Functional Correctness | 🔵 Trivial

Relies on explicit to: which breaks implicit relations.

The check hasFullRelation requires both from (or fields) and to (or references) to establish a FK dependency edge. While the sql-schema-ir-to-psl-ast generator currently emits to: explicitly (making the bug latent for this specific path), this logic fails if the AST allows implicit to: (defaulting to the target's @id), as stated in the PR objective.

Since the FK-owning side is uniquely identified by the presence of from/fields, the to check is unnecessary for detecting the edge and enforces a stricter requirement than the PSL spec allows.

Update the logic to key completeness solely on hasFrom:

- return hasFrom && hasTo;
+ return hasFrom;
🤖 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 `@packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts`
around lines 272 - 276, The relation completeness check in hasFullRelation is
too strict because it requires both sides of the FK edge, which breaks
implicit-to relations. Update the logic in ast-to-print-document’s
hasFullRelation so it treats the FK-owning side as sufficient by keying
completeness only on the presence of from/fields, while still allowing
to/references when explicitly provided.
🤖 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.

Inline comments:
In `@packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`:
- Around line 239-242: The relation argument resolution in
psl-relation-resolution.ts currently falls back from canonical names to legacy
aliases and silently ignores cases where both are present. Update the logic
around getNamedArgument in the relation parser to explicitly detect conflicts
when both from/fields or to/references are supplied, and reject the input with a
validation error instead of preferring one value. Use the existing relation
parsing flow in psl-relation-resolution.ts so the check happens before fromRaw
and toRaw are consumed.

---

Nitpick comments:
In `@packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts`:
- Around line 272-276: The relation completeness check in hasFullRelation is too
strict because it requires both sides of the FK edge, which breaks implicit-to
relations. Update the logic in ast-to-print-document’s hasFullRelation so it
treats the FK-owning side as sufficient by keying completeness only on the
presence of from/fields, while still allowing to/references when explicitly
provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: fbe45d1e-3b9b-4ccd-80e0-95802f33bb88

📥 Commits

Reviewing files that changed from the base of the PR and between 9532c5f and f30b436.

⛔ Files ignored due to path filters (6)
  • projects/psl-relation-syntax/README.md is excluded by !projects/**
  • projects/psl-relation-syntax/design-notes.md is excluded by !projects/**
  • projects/psl-relation-syntax/plan.md is excluded by !projects/**
  • projects/psl-relation-syntax/slices/01-from-to-fk-foundation/plan.md is excluded by !projects/**
  • projects/psl-relation-syntax/slices/01-from-to-fk-foundation/spec.md is excluded by !projects/**
  • projects/psl-relation-syntax/spec.md is excluded by !projects/**
📒 Files selected for processing (14)
  • packages/1-framework/2-authoring/psl-parser/src/format/canonicalize-relation.ts
  • packages/1-framework/2-authoring/psl-parser/src/format/format.ts
  • packages/1-framework/2-authoring/psl-parser/test/format/relation-keywords.test.ts
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.from-to.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.test.ts
  • packages/2-sql/9-family/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts
  • packages/2-sql/9-family/test/psl-contract-infer/print-psl/print-psl.naming-and-constraints.test.ts
  • packages/2-sql/9-family/test/psl-contract-infer/print-psl/print-psl.relations.test.ts
  • packages/2-sql/9-family/test/psl-contract-infer/print-psl/print-psl.single-dialect-gate.test.ts
  • packages/2-sql/9-family/test/psl-contract-infer/sql-schema-ir-to-psl-ast.test.ts

Comment thread packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts Outdated
@tensordreams tensordreams changed the title TML-2940: from/to FK relation syntax with legacy aliasing + format rewrite TML-2940: from/to FK relation syntax — clean break, reject legacy fields/references Jul 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts`:
- Around line 136-153: The relation-argument parsing in `psl-helpers.ts`
currently lets multiple positional args overwrite `relationName`, so mixed forms
like `@relation(from: a, b, to: id)` can be accepted incorrectly. Update the
`relationAttr.args` handling to detect and reject more than one positional
relation-name arg, especially when mixed with named args, and emit a diagnostic
instead of assigning the later value. Add a test around the relation parsing
path that covers `relationName` handling in the `@relation` argument loop and
verifies the new diagnostic for multiple positional or mixed positional/named
arguments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e337fb3d-7b78-4ae7-96f7-c8bb148ca470

📥 Commits

Reviewing files that changed from the base of the PR and between f30b436 and c8e0273.

⛔ Files ignored due to path filters (4)
  • packages/1-framework/3-tooling/cli/test/commands/init/__snapshots__/templates.test.ts.snap is excluded by !**/*.snap
  • projects/psl-relation-syntax/design-notes.md is excluded by !projects/**
  • projects/psl-relation-syntax/spec.md is excluded by !projects/**
  • projects/supabase-integration/example/prisma/schema.prisma is excluded by !projects/**
📒 Files selected for processing (51)
  • docs/architecture docs/adrs/ADR 121 - Contract.d.ts structure and relation typing.md
  • docs/architecture docs/adrs/ADR 226 - Cross-contract foreign-key references.md
  • docs/architecture docs/subsystems/2. Contract Emitter & Types.md
  • docs/architecture docs/subsystems/6. Ecosystem Extensions & Packs.md
  • examples/mongo-blog-leaderboard/src/contract.prisma
  • examples/mongo-demo/migrations/app/20260409T1030_migration/contract.prisma
  • examples/mongo-demo/migrations/app/20260415_add-posts-author-index/contract.prisma
  • examples/mongo-demo/src/contract.prisma
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260422T0720_initial/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260422T0742_migration/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260422T0748_migration/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260605T1145_mti_variant_link_columns/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260610T2216_set_priority_default/contract.prisma
  • examples/prisma-next-demo/src/prisma/contract.prisma
  • examples/react-router-demo/src/prisma/contract.prisma
  • examples/retail-store/migrations/app/20260513T0505_initial/contract.prisma
  • examples/retail-store/migrations/app/20260513T0507_add_product_category_index/contract.prisma
  • examples/retail-store/migrations/app/20260513T0508_backfill_product_status/contract.prisma
  • examples/retail-store/src/contract.prisma
  • examples/supabase/src/contract.prisma
  • packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/README.md
  • packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.namespaces.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.from-to.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.many-to-many.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/psl-ts-namespace-parity.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.ts
  • skills/prisma-next-contract/SKILL.md
  • test/integration/test/authoring/diagnostics/ambiguous-backrelation-list/schema.prisma
  • test/integration/test/authoring/diagnostics/unsupported-navigation-list-attribute/schema.prisma
  • test/integration/test/authoring/parity/callback-mode-scalars/schema.prisma
  • test/integration/test/authoring/parity/core-surface/schema.prisma
  • test/integration/test/authoring/parity/map-attributes/schema.prisma
  • test/integration/test/authoring/parity/relation-backrelation-list/schema.prisma
  • test/integration/test/authoring/side-by-side/mongo/contract.prisma
  • test/integration/test/authoring/side-by-side/postgres/contract.prisma
  • test/integration/test/cli.emit-command.additional.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-composite-pk.prisma
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/schema.parity.psl
  • test/integration/test/sql-orm-client/fixtures/mn-psl/contract.prisma
  • test/integration/test/sql-orm-client/fixtures/polymorphism/contract.prisma
✅ Files skipped from review due to trivial changes (4)
  • docs/architecture docs/subsystems/6. Ecosystem Extensions & Packs.md
  • packages/2-sql/2-authoring/contract-psl/README.md
  • docs/architecture docs/adrs/ADR 226 - Cross-contract foreign-key references.md
  • skills/prisma-next-contract/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.from-to.test.ts

Comment on lines 136 to 153
for (const arg of relationAttr.args) {
if (arg.kind === 'positional') {
relationName = stripQuotes(arg.value);
} else if (arg.name === 'name') {
relationName = stripQuotes(arg.value);
} else if (arg.name === 'fields') {
fieldsArg = arg;
} else if (arg.name === 'references') {
referencesArg = arg;
} else if (arg.name === 'fields' || arg.name === 'references') {
input.diagnostics.push({
code: 'PSL_LEGACY_FIELDS_REFERENCES',
message: `Relation field "${input.modelName}.${input.fieldName}" uses @relation(fields:/references:), which is no longer supported — use from:/to: instead`,
sourceId: input.sourceId,
span: arg.span,
});
return undefined;
} else if (arg.name === 'from') {
fromRaw = arg.value;
} else if (arg.name === 'to') {
toRaw = arg.value;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate relation parser/helper tests and any coverage for unbracketed composite directional args.
rg -n -C3 '`@relation`\(from:\s*[A-Za-z_][A-Za-z0-9_]*\s*,\s*[A-Za-z_][A-Za-z0-9_]*' .
rg -n -C3 'composites must be bracketed|bare single from|to without a from|parseRelationFieldArgument' packages/2-mongo-family/2-authoring/contract-psl

Repository: prisma/prisma-next

Length of output: 5882


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '108,210p' packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts
printf '\n--- TESTS ---\n'
sed -n '690,760p' packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
printf '\n--- SQL TESTS ---\n'
sed -n '196,260p' packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.from-to.test.ts

Repository: prisma/prisma-next

Length of output: 7631


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the parser/AST shape for `@relation` arguments to see whether a bare second value can appear as a positional arg.
rg -n -C2 'kind: '"'"'positional'"'"'|name === '"'"'from'"'"'|name === '"'"'to'"'"'|parseRelationAttribute' packages/2-mongo-family/2-authoring/contract-psl packages/2-sql/2-authoring/contract-psl

Repository: prisma/prisma-next

Length of output: 4805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the attribute parsing types and any diagnostics around malformed relation args.
ast-grep outline packages/2-mongo-family/2-authoring/contract-psl/src --view expanded | sed -n '1,220p'
printf '\n--- INVALID ARG TESTS ---\n'
rg -n -C3 'PSL_INVALID_ATTRIBUTE_ARGUMENT|parseIdentifierExpr|unknown field|composites must be bracketed|from:' packages/2-mongo-family/2-authoring/contract-psl/test packages/2-mongo-family/2-authoring/contract-psl/src

Repository: prisma/prisma-next

Length of output: 24341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the parser/grammar that constructs ResolvedAttribute args and whether commas can create positional args after named args.
rg -n -C3 'type ResolvedAttribute|interface ResolvedAttribute|kind: '"'"'positional'"'"'|name: .*from|attribute.*args|parse.*attribute' packages/2-mongo-family/2-authoring/contract-psl/src packages/2-mongo-family/2-authoring/contract-psl/test

Repository: prisma/prisma-next

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the attribute AST/parser types to see how mixed positional and named args are represented.
fd -a '.*' packages/2-mongo-family/2-authoring/contract-psl/src packages/2-mongo-family/2-authoring/contract-psl/test | sed -n '1,200p'
printf '\n--- ResolvedAttribute / parser references ---\n'
rg -n -C3 'interface ResolvedAttribute|type ResolvedAttribute|kind: '"'"'positional'"'"'|kind: '"'"'named'"'"'|args:' packages/2-mongo-family/2-authoring/contract-psl/src packages/2-mongo-family/2-authoring/contract-psl/test

Repository: prisma/prisma-next

Length of output: 1206


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the attribute parser/type definitions that produce ResolvedAttribute.args.
rg -n -C3 'ResolvedAttribute|ResolvedAttributeArg|kind: '"'"'positional'"'"'|kind: '"'"'named'"'"'|parse.*Attribute|attribute parser|args' packages . --glob '!**/dist/**' --glob '!**/build/**'

Repository: prisma/prisma-next

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 'interface ResolvedAttributeArg|type ResolvedAttributeArg|ResolvedAttributeArg =' . \
  --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/*.map' \
  | head -n 120

Repository: prisma/prisma-next

Length of output: 632


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow to parser sources only if the type is re-exported from there.
rg -n -C2 'ResolvedAttributeArg|ResolvedAttribute' packages -g '*psl-parser*' -g '*parser*' --glob '!**/dist/**' --glob '!**/build/**' | head -n 120

Repository: prisma/prisma-next

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 'ResolvedAttributeArg|ResolvedAttribute' packages -g '*psl-parser*' -g '*parser*' --glob '!**/dist/**' --glob '!**/build/**' | head -n 120

Repository: prisma/prisma-next

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' packages/1-framework/2-authoring/psl-parser/src/resolve.ts
printf '\n--- ATTRIBUTE PARSE SITES ---\n'
rg -n -C3 'ResolvedAttributeArg|positional|named|splitTopLevel|attribute args|parse.*attribute' packages/1-framework/2-authoring/psl-parser/src packages/2-mongo-family/2-authoring/contract-psl/src packages/2-sql/2-authoring/contract-psl/src | head -n 200

Repository: prisma/prisma-next

Length of output: 23664


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for grammar/tests covering mixed positional and named attribute args.
rg -n -C3 '`@relation`\(from:\s*[^,\]]+,\s*[^,\]]+|from:\s*[^,\]]+,\s*[^,\]]+,\s*to:|named.*positional|positional.*named|multiple arguments|single field may be bare' packages/1-framework/2-authoring/psl-parser packages/2-mongo-family/2-authoring/contract-psl packages/2-sql/2-authoring/contract-psl | head -n 200

Repository: prisma/prisma-next

Length of output: 15191


Reject mixed positional relation-name args
relationName is overwritten by every positional arg, so @relation(from: a, b, to: id) can treat b as the relation name instead of failing. Add a diagnostic/test for multiple positional args or mixed positional + named relation arguments.

🤖 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 `@packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts` around
lines 136 - 153, The relation-argument parsing in `psl-helpers.ts` currently
lets multiple positional args overwrite `relationName`, so mixed forms like
`@relation(from: a, b, to: id)` can be accepted incorrectly. Update the
`relationAttr.args` handling to detect and reject more than one positional
relation-name arg, especially when mixed with named args, and emit a diagnostic
instead of assigning the later value. Add a test around the relation parsing
path that covers `relationName` handling in the `@relation` argument loop and
verifies the new diagnostic for multiple positional or mixed positional/named
arguments.

Comment on lines +272 to +276
// Canonical `from:`/`to:` declare the FK dependency edge; legacy
// `fields:`/`references:` are accepted as an input alias for the same edge.
const hasFrom = named['from'] !== undefined || named['fields'] !== undefined;
const hasTo = named['to'] !== undefined || named['references'] !== undefined;
return hasFrom && hasTo;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why does this still have legacy names?

span: span(0),
};
const printed = printPslFromAst(ast);
expect(printed).toContain('@relation(from: [authorId], to: [id])');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why does it still have square brackets for the single-field case? Or is that implemented in the following PRs next in the stack?

Comment on lines +907 to +908
let references = relation?.references;
if (relation?.fields && relation.referencesInferred) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should fields,references and referencesInferred be renamed to fromFields, toFields and toFieldsInferred in this data structure?

declaringModel: pslModel.name,
fieldName: field.name,
targetModel: field.typeName,
...(relation.relationName !== undefined ? { relationName: relation.relationName } : {}),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

use ifDefined

Comment on lines +108 to +120
/**
* Parses a single `@relation` directional argument value (`from:`/`to:`). A
* single field may be bare (`from: userId`) or bracketed (`from: [userId]`);
* composites must be bracketed (`from: [a, b]`).
*/
function parseRelationFieldArgument(raw: string): readonly string[] | undefined {
const trimmed = raw.trim();
const entries = trimmed.startsWith('[') ? parseFieldList(trimmed) : [trimmed];
if (entries.length === 0 || entries.some((entry) => entry.length === 0)) {
return undefined;
}
return entries;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How come we have to care about this here? Parsing must happen in the parser. We use AST on this level, not raw strings.

@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.

Reviewed this slice. I did not find an additional distinct blocking issue beyond the existing unresolved threads already on this PR, mainly around the remaining legacy naming/parser-shape cleanup in the from/to migration.

@tensordreams tensordreams force-pushed the tml-2940-s1-from-to-fk-foundation branch from c8e0273 to 94f373d Compare July 1, 2026 14:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In
`@examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/contract.prisma`:
- Around line 3-8: The migration snapshot uses a Prisma enum for `UserRole`, but
`contract.prisma` in Prisma Next MongoDB does not support enums. Replace the
`UserRole` enum definition with a `String` field for `role` in the affected
model, and document the permitted values (`admin`, `author`, `reader`) in the
contract instead of encoding them as an enum. Locate the change by the
`UserRole` symbol and the `role` field in the migration snapshot.

In
`@examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/contract.prisma`:
- Around line 3-8: Remove the unsupported UserRole enum from the contract.prisma
snapshot and keep role modeled as a String field in the relevant schema instead.
Update the Prisma Next Mongo example so contract.prisma only uses supported
Mongo snapshot shapes, and if needed document the allowed role values outside
the schema rather than via an enum. Use the UserRole symbol in this snapshot to
locate and replace the enum definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: da270d36-bc55-4651-99a4-72c9d08dd6f0

📥 Commits

Reviewing files that changed from the base of the PR and between c8e0273 and 94f373d.

⛔ Files ignored due to path filters (1)
  • packages/1-framework/3-tooling/cli/test/commands/init/__snapshots__/templates.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • docs/architecture docs/adrs/ADR 121 - Contract.d.ts structure and relation typing.md
  • docs/architecture docs/adrs/ADR 226 - Cross-contract foreign-key references.md
  • docs/architecture docs/subsystems/2. Contract Emitter & Types.md
  • docs/architecture docs/subsystems/6. Ecosystem Extensions & Packs.md
  • examples/mongo-blog-leaderboard/src/contract.prisma
  • examples/mongo-demo/migrations/app/20260409T1030_migration/contract.prisma
  • examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/contract.prisma
  • examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/contract.prisma
  • examples/mongo-demo/src/contract.prisma
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260422T0720_initial/contract.prisma
  • examples/prisma-next-demo/src/prisma/contract.prisma
  • examples/react-router-demo/src/prisma/contract.prisma
  • examples/retail-store/migrations/app/20260513T0505_initial/contract.prisma
  • examples/retail-store/migrations/app/20260513T0507_add_product_category_index/contract.prisma
  • examples/retail-store/migrations/app/20260513T0508_backfill_product_status/contract.prisma
  • examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/contract.prisma
  • examples/retail-store/src/contract.prisma
  • examples/supabase/src/contract.prisma
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts
💤 Files with no reviewable changes (11)
  • packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts
  • examples/react-router-demo/src/prisma/contract.prisma
  • examples/supabase/src/contract.prisma
  • examples/retail-store/migrations/app/20260513T0508_backfill_product_status/contract.prisma
  • examples/retail-store/migrations/app/20260513T0505_initial/contract.prisma
  • examples/prisma-next-demo/src/prisma/contract.prisma
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/contract.prisma
  • examples/retail-store/src/contract.prisma
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • examples/retail-store/migrations/app/20260513T0507_add_product_category_index/contract.prisma
✅ Files skipped from review due to trivial changes (6)
  • examples/mongo-blog-leaderboard/src/contract.prisma
  • docs/architecture docs/subsystems/6. Ecosystem Extensions & Packs.md
  • docs/architecture docs/subsystems/2. Contract Emitter & Types.md
  • docs/architecture docs/adrs/ADR 226 - Cross-contract foreign-key references.md
  • examples/prisma-next-demo/migrations/app/20260422T0720_initial/contract.prisma
  • docs/architecture docs/adrs/ADR 121 - Contract.d.ts structure and relation typing.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/mongo-demo/src/contract.prisma
  • examples/mongo-demo/migrations/app/20260409T1030_migration/contract.prisma
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.prisma

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 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.

Inline comments:
In
`@examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/contract.prisma`:
- Around line 3-8: The migration snapshot uses a Prisma enum for `UserRole`, but
`contract.prisma` in Prisma Next MongoDB does not support enums. Replace the
`UserRole` enum definition with a `String` field for `role` in the affected
model, and document the permitted values (`admin`, `author`, `reader`) in the
contract instead of encoding them as an enum. Locate the change by the
`UserRole` symbol and the `role` field in the migration snapshot.

In
`@examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/contract.prisma`:
- Around line 3-8: Remove the unsupported UserRole enum from the contract.prisma
snapshot and keep role modeled as a String field in the relevant schema instead.
Update the Prisma Next Mongo example so contract.prisma only uses supported
Mongo snapshot shapes, and if needed document the allowed role values outside
the schema rather than via an enum. Use the UserRole symbol in this snapshot to
locate and replace the enum definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: da270d36-bc55-4651-99a4-72c9d08dd6f0

📥 Commits

Reviewing files that changed from the base of the PR and between c8e0273 and 94f373d.

⛔ Files ignored due to path filters (1)
  • packages/1-framework/3-tooling/cli/test/commands/init/__snapshots__/templates.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • docs/architecture docs/adrs/ADR 121 - Contract.d.ts structure and relation typing.md
  • docs/architecture docs/adrs/ADR 226 - Cross-contract foreign-key references.md
  • docs/architecture docs/subsystems/2. Contract Emitter & Types.md
  • docs/architecture docs/subsystems/6. Ecosystem Extensions & Packs.md
  • examples/mongo-blog-leaderboard/src/contract.prisma
  • examples/mongo-demo/migrations/app/20260409T1030_migration/contract.prisma
  • examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/contract.prisma
  • examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/contract.prisma
  • examples/mongo-demo/src/contract.prisma
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260422T0720_initial/contract.prisma
  • examples/prisma-next-demo/src/prisma/contract.prisma
  • examples/react-router-demo/src/prisma/contract.prisma
  • examples/retail-store/migrations/app/20260513T0505_initial/contract.prisma
  • examples/retail-store/migrations/app/20260513T0507_add_product_category_index/contract.prisma
  • examples/retail-store/migrations/app/20260513T0508_backfill_product_status/contract.prisma
  • examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/contract.prisma
  • examples/retail-store/src/contract.prisma
  • examples/supabase/src/contract.prisma
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts
💤 Files with no reviewable changes (11)
  • packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts
  • examples/react-router-demo/src/prisma/contract.prisma
  • examples/supabase/src/contract.prisma
  • examples/retail-store/migrations/app/20260513T0508_backfill_product_status/contract.prisma
  • examples/retail-store/migrations/app/20260513T0505_initial/contract.prisma
  • examples/prisma-next-demo/src/prisma/contract.prisma
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/contract.prisma
  • examples/retail-store/src/contract.prisma
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • examples/retail-store/migrations/app/20260513T0507_add_product_category_index/contract.prisma
✅ Files skipped from review due to trivial changes (6)
  • examples/mongo-blog-leaderboard/src/contract.prisma
  • docs/architecture docs/subsystems/6. Ecosystem Extensions & Packs.md
  • docs/architecture docs/subsystems/2. Contract Emitter & Types.md
  • docs/architecture docs/adrs/ADR 226 - Cross-contract foreign-key references.md
  • examples/prisma-next-demo/migrations/app/20260422T0720_initial/contract.prisma
  • docs/architecture docs/adrs/ADR 121 - Contract.d.ts structure and relation typing.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/mongo-demo/src/contract.prisma
  • examples/mongo-demo/migrations/app/20260409T1030_migration/contract.prisma
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.prisma
🛑 Comments failed to post (2)
examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/contract.prisma (1)

3-8: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace the Mongo enum with a string field.

Prisma Next Mongo contract.prisma files still do not support enums, so UserRole/role UserRole makes this migration snapshot invalid. Keep role as String and document the allowed values instead.

Suggested fix
-enum UserRole {
-  @@type("mongo/string@1")
-  Admin  = "admin"
-  Author = "author"
-  Reader = "reader"
-}
-
 model User {
   id      ObjectId `@id` `@map`("_id")
   name    String
   email   String
   bio     String?
-  role    UserRole
+  // Allowed values: "admin" | "author" | "reader"
+  role    String
   address Address?
   posts   Post[]
   @@map("users")
 }

Based on learnings, in Prisma Next MongoDB contract.prisma files Prisma enums are not supported yet; constrained values should stay as String fields with documented allowed values.

Also applies to: 22-22

🤖 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
`@examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/contract.prisma`
around lines 3 - 8, The migration snapshot uses a Prisma enum for `UserRole`,
but `contract.prisma` in Prisma Next MongoDB does not support enums. Replace the
`UserRole` enum definition with a `String` field for `role` in the affected
model, and document the permitted values (`admin`, `author`, `reader`) in the
contract instead of encoding them as an enum. Locate the change by the
`UserRole` symbol and the `role` field in the migration snapshot.

Source: Learnings

examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/contract.prisma (1)

3-8: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid Prisma enums in Mongo contract.prisma snapshots.

This file has the same unsupported UserRole enum shape. In Prisma Next Mongo schemas, role needs to remain a String; otherwise this example snapshot is not valid.

Based on learnings, in Prisma Next MongoDB contract.prisma files Prisma enums are not supported yet; constrained values should stay as String fields with documented allowed values.

Also applies to: 22-22

🤖 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
`@examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/contract.prisma`
around lines 3 - 8, Remove the unsupported UserRole enum from the
contract.prisma snapshot and keep role modeled as a String field in the relevant
schema instead. Update the Prisma Next Mongo example so contract.prisma only
uses supported Mongo snapshot shapes, and if needed document the allowed role
values outside the schema rather than via an enum. Use the UserRole symbol in
this snapshot to locate and replace the enum definition.

Source: Learnings

@tensordreams tensordreams force-pushed the tml-2940-s1-from-to-fk-foundation branch from 94f373d to 5bfb3ea 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 1 lands the canonical @relation(from:, to:) FK vocabulary end-to-end (SQL + Mongo resolver read with to:-inference, contract-infer printer, CLI templates, psl-printer topological detection) and migrates the repo's schemas. The implementation is sound: legacy fields:/references: is rejected with a guiding PSL_LEGACY_FIELDS_REFERENCES diagnostic in both family resolvers, to:-omission infers the target @id (single + composite), bare-vs-bracketed value forms lower identically, and there are good legacy-rejection / inference / single-dialect-gate tests. No any, bare-as-in-prod, or extension-in-import violations. The substantive issues are doc-staleness: the slice spec/plan/README still describe the rejected "legacy-as-input-alias + backward-compat byte-identical invariant + formatter rewrite" design (the design-notes D1 reversal in this same PR contradicts them), and the parseRelationFieldArgument docstring advertises a Model.-qualifier tolerance that the regression test disproves.


## At a glance

PSL foreign-key relations gain the canonical directional vocabulary `@relation(from:, to:)`. Legacy `@relation(fields:, references:)` keeps parsing as an **input alias** that lowers to the same contract; the toolchain only ever **emits** the canonical spelling. This slice is FK-only (1:N / N:1) — `through:`/`inverse:`/M:N are later slices. Its headline is the **backward-compat invariant**: legacy and canonical spellings lower to byte-identical contracts.

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 slice spec still describes the rejected design: "Legacy fields:/references: keeps parsing as an input alias" and the headline "backward-compat invariant: legacy and canonical spellings lower to byte-identical contracts". The implementation does a clean break — both resolvers reject legacy with PSL_LEGACY_FIELDS_REFERENCES (matching projects/psl-relation-syntax/design-notes.md D1 reversal and the project spec.md). The slice done-conditions (line 50 byte-identical legacy-vs-canonical; line 52 formatter idempotence on a legacy input) are untestable as written, and §Scope (line 32) still lists "CST format keyword-only canonicalisation" which isn't implemented (and is unnecessary under clean break). Please rewrite this spec/plan to the clean-break shape so the slice's stated DoD matches what landed.

# PSL relation syntax

Rework PSL's relation declaration to a directional `from:`/`to:`/`through:` vocabulary,
backward-compatible with `fields:`/`references:` (old spelling still parses; the PSL

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] Stale framing: "backward-compatible with fields:/references: (old spelling still parses; the PSL formatter rewrites it to the canonical form)". Under the D1 clean break landed in this slice, legacy does not parse and the formatter rewrite was dropped (see design-notes.md). Update to the directional-only/reject-legacy description.

* single field may be bare (`from: userId`) or bracketed (`from: [userId]`);
* composites must be bracketed (`from: [a, b]`).
*
* A redundant model qualifier on `to:` (`to: Post.id`) is stripped to its bare

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 docstring is internally contradictory and disagrees with the regression test. Line 54 claims to: Post.id is "stripped to its bare column name so it lowers identically to the unqualified spelling", but lines 57-58 then say the grammar drops .id and to: Post.id reaches the resolver as Post — so stripModelQualifier receives Post (no dot) and returns Post, not id. The test rejects a member-access to: value confirms to: User.id is rejected (unknown field "User.User"), not tolerated. design-notes.md's "tolerated and preserved verbatim" is also wrong here. Either drop the tolerance claim (and the dead lastIndexOf('.') branch, which only fires for the untested bracketed [Post.id] form) or add a test pinning the bracketed form's behaviour and reconcile the wording.

}

// An omitted `to:` references the target model's `@id`; an explicit
// `to:`/`references:` names the referenced fields directly.

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.

[nit] Comment says "an explicit to:/references: names the referenced fields directly", but legacy references: is now rejected by parseRelationAttribute with PSL_LEGACY_FIELDS_REFERENCES. Drop the references: mention so the comment reflects that only to: is a valid explicit form.

Scaffold the PSL Directional Relation Syntax project (sibling of
sql-orm-many-to-many). Captures the settled grammar design (decisions
D1-D5 from drive-discussion): directional from/to/through/inverse
relation vocabulary, backward-compatible with fields/references/name
(legacy input-only; toolchain emits canonical), with explicit junction
naming, pointer disambiguation retiring @relation(name:), implicit M:N
synthesis, and an arrow-path form.

Linear: TML-2939 (plan anchor); slices TML-2940..2944.

Signed-off-by: Alexey Orlenko's AI Agent <agent@prisma.io>
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…oundation)

Signed-off-by: Alexey Orlenko's AI Agent <agent@prisma.io>
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The PSL resolver reads the canonical directional `@relation(from:, to:)`
arguments for foreign-key relations (1:N / N:1). Legacy `fields:`/`references:`
are accepted as input aliases that lower to the identical resolved relation, so
a schema authored either way produces a byte-identical contract.

- `from:`/`to:` parse as generic named args (no grammar change); legacy keys
  alias to the same `{ fields, references }` result.
- `to:` may be omitted: the referenced columns default to the target model's
  `@id` (inline `@id` or `@@id([...])`), resolved at the FK call site which
  holds the target model. Omitting `to:` against a target with no `@id` is an
  error.
- A single field is bare (`from: userId`) or bracketed (`from: [userId]`);
  composites must be bracketed. `parseRelationFieldArgument` handles both
  without weakening the shared `parseFieldList` bracket contract used by
  `@@id`/`@@unique`/`@@index`.
- The both-or-neither diagnostic is adapted: `from:` may stand alone (references
  inferred); a `to:` (or legacy `references:`) without a `from:` stays an error.

FK relations only — `through:`/`inverse:`/M:N and `@relation(name:)` are
untouched.

Scope note: the spec's redundant-`Model.`-qualifier-on-`to:` tolerance is
deferred. The PSL expression grammar does not carry a member-access argument
value — `parseIdentifierExpr` consumes only the head identifier, so `to: Post.id`
reaches the resolver as `Post` (the `.id` is silently dropped, no diagnostic).
Accepting the qualifier requires a grammar change in @prisma-next/psl-parser,
out of this contract-psl-scoped dispatch. The resolver's qualifier strip is in
place for when the grammar carries the dotted value; a test pins the present
grammar boundary as a regression anchor.

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

M1 found the PSL expression grammar has no member-access value form, so
'to: Post.id' silently drops '.id'. The qualifier is redundant and the
member-access grammar is shared with S5 arrow-path; deferred there.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The CST formatter rewrites legacy @relation argument keys fields->from and
references->to, touching the keyword token only. Argument values, brackets,
the name: argument, and all comments/trivia are preserved; column alignment
is untouched.

The rename is a green-tree pre-pass (canonicalizeRelationKeywords): it walks
the parsed green tree, rebuilds only the affected @relation arg-key tokens,
and carries every other subtree by reference so emitDocument stays generic
and trivia is preserved structurally. The rename is scoped to @relation /
@@relation attribute argument keys by inspecting the attribute name, so a
fields field, a fields argument on @@index, or a @relation(name:) all survive
unchanged.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The @relation FK-keyword canonicalisation bound the rename to the first
identifier matching a rename target rather than to the key position, so a
bare single-field value spelled `fields`/`references` was rewritten when the
key did not itself migrate (e.g. `@relation(from: references, to: id)` became
`@relation(from: to, to: id)`).

Bind the rename to the key by position: an AttributeArg key exists only when
the arg carries a Colon, and is then its first Identifier child. Consider only
that identifier and stop, so the value position is never touched. Bracketed
values were already safe; this closes the bare-value collision.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The IR->PSL-AST printer used by `contract infer` now renders the
canonical directional vocabulary `@relation(from:, to:)` for FK
relations instead of the legacy `fields:`/`references:` keys, keeping
the explicit bracketed form (no aggressive inference) and leaving the
relationName `name:` argument untouched.

The psl-printer's FK-dependency topological sort reads the relation
keys to order models (referenced model before the FK owner). Teach
`hasFullRelation` to recognise the canonical `from`/`to` keys, with the
legacy keys accepted as an input alias, so the dependency edge is still
detected and model ordering stays stable.

Add a single-dialect gate asserting `contract infer` output carries no
legacy `fields:`/`references:` relation keys and does carry `from:`/`to:`,
and a printer regression covering canonical-key dependency ordering.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…rom/to only

`from`/`to` is now the only FK relation syntax. The resolver reads only
`from:`/`to:`, drops `fields`/`references` from the @relation argument
allow-list, and emits PSL_LEGACY_FIELDS_REFERENCES when a relation carries
`fields:`/`references:` — a clear, actionable rejection pointing at `from:`/`to:`.

The psl-parser formatter no longer rewrites relation keywords: the S1·M2
canonicalisation (`canonicalize-relation.ts`) and its test are removed, and
`format` reverts to `parse → emitDocument`.

S1's from/to unit tests replace the legacy≡canonical equivalence cases with a
rejection-diagnostic test (plus lone-`fields:`/lone-`references:` variants),
keeping the `from`/`to` inference, value-form, both-or-neither, and
self-referential coverage. The member-access `to:` test now pins the actual
behaviour: the qualifier drops at the grammar layer, so `to:` is treated as
omitted and the target `@id` is inferred.

Migrating the remaining legacy `.prisma` and test schemas is a separate
dispatch (S1b); the full repo suite is red until then.

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

S1a taught the SQL PSL resolver to accept only @relation(from:, to:) and to
reject the legacy fields:/references: spelling. Migrate every SQL-family
authored schema, integration fixture, and inline-PSL test to the from/to
vocabulary so they parse again.

from/to lowers byte-identically to fields/references, so only .prisma source
spelling changes — no contract.json, migration metadata, or hash drift
(verified via fixtures:emit + migrations:regen:examples producing zero
artifact diff). Mongo-family schemas keep fields/references: the Mongo
interpreter speaks only that vocabulary and would drop the relation under
from/to.

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

The `to: User.id` case is RED on a fresh tml-2940 build: the
member-access value grammar that makes `Foo.bar` parse as a qualified
argument value arrives in a later slice and is absent here. Without it,
`parseIdentifierExpr` reads only the head identifier `User`, the resolver
looks for a column `User` on the target model `User`, finds none, and
rejects the relation with PSL_INVALID_ATTRIBUTE_ARGUMENT.

Assert that real behaviour (failure + the unknown-field diagnostic)
instead of the inherited 'qualifier dropped, @id inferred' assertion,
which only held against a stale dist still carrying the grammar. The
case stays present as the anchor a later slice rewrites once the dotted
value is supported.

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

S1a/S1b taught the SQL PSL resolver to accept only @relation(from:, to:). The
Mongo family has its own PSL relation parser (contract-psl/psl-helpers.ts,
parseRelationAttribute) that read only fields:/references: and ignored from:/to:,
so Mongo schemas were left on the legacy vocabulary. Mirror the SQL change in the
Mongo interpreter:

- parseRelationAttribute now reads from:/to:, rejects fields:/references: with the
  PSL_LEGACY_FIELDS_REFERENCES diagnostic (the same open-string code SQL uses), and
  supports bare or bracketed values. Omitting to: infers the referenced column(s)
  from the target model's @id (resolveTargetIdFieldNames); a to: without a from: is
  rejected. The interpreter caller resolves the inferred references against the
  target model and routes legacy/invalid relations away from backrelation handling.
- Migrate every Mongo-family authored schema, migration snapshot, inline-PSL test,
  and the Mongo CLI init template from fields:/references: to from:/to:.

from/to lowers byte-identically to fields/references, so only .prisma source
spelling changes — no contract.json, migration metadata, or hash drift (verified:
migrations:regen:examples re-parses the migrated Mongo snapshots and re-emits the
start/end contracts with zero artifact diff). @relation(name:) is untouched.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…rences:) in .psl/.ts/.md to from/to

Finish the from/to migration for non-.prisma sites the .prisma-focused
passes missed: the SQL emit-command parity .psl fixture, the Postgres PSL
init template string (and its snapshot), and live authoring examples in
the SQL contract-psl README, the prisma-next-contract skill, two subsystem
docs, and ADR 121 / ADR 226 worked examples.

Deliberate legacy sites are left intact: resolver rejection tests and
diagnostic strings, parser/printer fidelity fixtures that never reach the
resolver, frozen release/upgrade history (CHANGELOG, releases, upgrade
recipe), project design records under projects/, and ADR 226 prose
explaining the legacy attribute grammar.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…legacy, no format rewrite

D1 reversed (operator decision, 2026-06-26): the directional vocabulary
(from/to/through/inverse) is the only accepted @relation syntax. Legacy
fields:/references: and @relation(name:) are rejected at parse time with
a guiding diagnostic. The format canonicalisation pass was removed.
The repo migration is byte-identical; a downstream codemod is deferred
(TML-2957).

Updates design-notes.md (D1, D3, principles, alternatives, trade-offs)
and spec.md (at-a-glance, non-goals, contract-impact, cross-cutting
requirements, transitional-shape, project DoD, open questions).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Two alignments with changes that landed on main after this branch was cut:

- The described-contracts infer test (main #919) asserts printed relation
  output; this branch retires @relation(fields:/references:) from the infer
  printer, so the expectation moves to the canonical from:/to: keys.
- The single-dialect gate test imported sql-schema-ir-to-psl-ast, which the
  schema-node tree restructure (main #894) dissolved; it now drives inference
  through the shared printPslFromFlat fixture helper.

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

The cross-space FK infer test asserts printed @relation output; the infer
printer emits canonical from:/to: keys on this branch, so the expectation
and the doc comment move off the legacy fields:/references: spelling.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
@tensordreams tensordreams force-pushed the tml-2940-s1-from-to-fk-foundation branch from 5bfb3ea to e989680 Compare July 8, 2026 14:58
…ycle

App authors and extension authors migrate @relation(fields:/references:) to
@relation(from:/to:); the legacy spellings are rejected at emission with
PSL_LEGACY_FIELDS_REFERENCES and contract infer prints the canonical keys.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
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.

2 participants