Skip to content

feat(sql-contract-psl): support composite primary keys#432

Open
jkomyno wants to merge 1 commit intomainfrom
ws5-psl-composite-primary-keys
Open

feat(sql-contract-psl): support composite primary keys#432
jkomyno wants to merge 1 commit intomainfrom
ws5-psl-composite-primary-keys

Conversation

@jkomyno
Copy link
Copy Markdown
Contributor

@jkomyno jkomyno commented May 7, 2026

Intent

Support greenfield SQL PSL schemas that model junction tables with composite primary keys. contract infer can already print composite keys as @@id([...]); this makes the PSL interpreter accept that same shape and lower it into the existing SQL contract primary-key node.

Change map

The story

  1. The interpreter now recognizes model-level @@id attributes as primary-key declarations instead of falling through to the unsupported model-attribute path.
  2. Composite key fields are resolved through the same field-to-column mapping logic used by other model constraints, so @map and @@map continue to determine storage names.
  3. Invalid composite-key declarations return targeted diagnostics for duplicate primary-key sources, nullable fields, and unknown fields.

Behavior changes & evidence

Compatibility / migration / risk

This is additive for supported greenfield PSL. Existing field-level @id behavior remains unchanged, and schemas that combine field-level @id with model-level @@id now fail explicitly instead of being reported through unsupported model-attribute diagnostics.

Follow-ups / open questions

  • Native scalar arrays, @updatedAt, and inline @db.* remain separate WS5 M2 slices.
  • Full local package tests still expose unrelated execution-default failures in interpreter.defaults.test.ts, composed-mutation-defaults.test.ts, and provider.test.ts.

Non-goals / intentionally out of scope

  • No relation inference changes.
  • No native scalar array support.
  • No @updatedAt or inline @db.* support.

Summary by CodeRabbit

  • New Features

    • Added support for model-level composite primary keys using @@id([fieldA, fieldB]) syntax in SQL PSL contracts.
    • Support for named primary key constraints via the map parameter.
    • Enhanced diagnostics for validation of primary key declarations.
  • Tests

    • Comprehensive test coverage for composite primary key validation and SQL constraint mapping.
  • Documentation

    • New design specification documenting composite primary key behavior.

Accept model-level @@id attributes in the SQL PSL interpreter, including mapped storage columns and mapped constraint names. Add diagnostics for duplicate field/model primary keys, nullable composite-key fields, and unknown key fields.

Add the task brief for the WS5 composite primary key slice.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 3095f2f1-b570-4b2e-90eb-e2da12926bd0

📥 Commits

Reviewing files that changed from the base of the PR and between dfef55a and 2657412.

📒 Files selected for processing (4)
  • LINEAR.md
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts

📝 Walkthrough

Walkthrough

This PR implements support for model-level composite primary keys (@@id) in the SQL PSL contract interpreter. It adds validation and mapping logic to resolve @@id attributes to storage table primary key definitions, with diagnostics for conflicts, nullable fields, and unknown field references.

Changes

Model-Level Composite Primary Keys Support

Layer / File(s) Summary
Specification
LINEAR.md
Defines WS5 feature scope: support @@id([fieldA, fieldB], map: "constraint_name") with field order preservation, @map/@@map resolution, and comprehensive diagnostics.
Primary Key Resolution
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
New resolveModelLevelPrimaryKey helper validates @@id attributes, rejects multiple declarations and field-level @id conflicts, disallows nullable fields, and returns a PrimaryKeyNode with mapped columns and optional constraint name.
Primary Key Integration
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
Reworked buildModelNodeFromPsl to derive primary key from field-level @id, model-level @@id, or variant models; updates missing-key diagnostic to allow variants.
Constraint Handling
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
Model attribute loop skips @@id during constraint generation to prevent treating model-level primary keys as unsupported attributes.
Test Helper
packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
Centralizes diagnostic assertion logic in expectDiagnosticForSchema helper, parsing schemas and verifying interpretation failures with matching diagnostic codes.
Validation Tests
packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
Tests validate error scenarios: combining field-level @id with @@id, nullable fields in @@id, and unknown field references.
Functional Test
packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
Verifies @@id([...], map: ...) correctly maps to storage contract with ordered composite columns and mapped constraint name.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Composite keys bloom in springtime's gentle light,
With @@id now dancing, the mapping's done right,
Fields mapped to columns, their order preserved,
Diagnostics catch errors—they get what they deserved!
From spec to the storage, the journey's complete,
A rabbit-approved feature, quite neat and quite sweet. 🌸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 PR title 'feat(sql-contract-psl): support composite primary keys' directly and clearly summarizes the main change: adding support for composite primary keys in the SQL contract PSL interpreter. The title accurately reflects the primary objective of the changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ws5-psl-composite-primary-keys

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 and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@432

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 2657412

Comment thread LINEAR.md
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.

accidentally committed?

const primaryKeyName = modelPrimaryKey?.name ?? fieldPrimaryKeyName;
const isVariantModel = model.attributes.some((attr) => attr.name === 'base');
if (primaryKeyColumns.length === 0 && !isVariantModel) {
if (primaryKeyColumns.length === 0 && !isVariantModel && modelPrimaryKeyAttributes.length === 0) {
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.

Super nitpicky nit: keep !isVariantModel at the end of the condition, the order reads a bit weird right now.

Or even extract const hasPrimaryKey = primaryKeyColumns.length > 0 || modelPrimaryKeyAttributes.length > 0 and use !hasPrimaryKey here.

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