Skip to content

feat(sql): support id-less models#424

Merged
jkomyno merged 34 commits intomainfrom
feat/idless-models
May 8, 2026
Merged

feat(sql): support id-less models#424
jkomyno merged 34 commits intomainfrom
feat/idless-models

Conversation

@jkomyno
Copy link
Copy Markdown
Contributor

@jkomyno jkomyno commented May 5, 2026

Linear: TML-2380 — sql: add support for id-less models

Stacked on #422.

Intent

Make SQL PSL authoring round-trip database tables that do not have primary keys, while also lowering model-level composite @@id declarations into SQL storage primary keys. This keeps introspected SQL PSL emit-able without changing document-family behavior or adding runtime ORM APIs for id-less models.

The PR also closes five pre-existing silent-failure paths in the identity / constraint validation surface that the multi-agent review surfaced; see "Identity & constraint validation tightening" below.

Change map

The story

  1. The SQL PSL interpreter now treats "no identity declaration" as a valid SQL table shape and omits the storage primaryKey property instead of reporting PSL_MISSING_PRIMARY_KEY.
  2. Model-level @@id is parsed with the existing model-attribute helpers, maps field names through @map, preserves the optional map constraint name, and feeds the same SQL primary-key node used by inline @id.
  3. SQL emission structure validation now accepts model-backed tables without primaryKey while retaining validation for primary-key column references when a primary key is present.
  4. Identity & constraint validation is tightened so that five pre-existing silent-failure paths (multiple @@id blocks, duplicate columns in @@id/@@unique/@@index, nullable @id/@@id columns, multi-field inline @id) now produce explicit diagnostics rather than emit semantically broken contracts.

Identity & constraint validation tightening

In addition to enabling id-less models, the PR adds explicit PSL_INVALID_ATTRIBUTE_ARGUMENT diagnostics for five pre-existing gaps where the interpreter previously accepted invalid input and emitted a structurally or semantically broken contract:

  • Multiple @@id blocks on the same model: previously the second silently overwrote the first; now the duplicate is rejected.
  • Duplicate columns in @@id, @@unique, or @@index (e.g. @@id([email, email])): previously emitted columns: ['email', 'email'] — invalid SQL that fails at DDL time for @@id/@@unique and produces a wasteful no-op index for @@index. Now rejected at authoring time via the shared findDuplicateFieldName helper.
  • Nullable fields used as @id or in @@id: previously accepted, producing a NULLABLE primary-key column that the database would reject at DDL time; now rejected at authoring time with an explanatory diagnostic.
  • Inline @id on multiple fields (e.g. id1 Int @id together with id2 Int @id): previously silently lowered into a composite primary key whose name was dropped; now rejected with a hint to use @@id([...]) instead.

The conflict between inline @id and @@id on the same model also gains a single-clause diagnostic in the established voice ("cannot declare both field-level @id and model-level @@id").

Behavior changes & evidence

  • Id-less SQL models emit successfully: a model with no @id or @@id produces a table with no primaryKey while preserving columns, uniques, and model field mappings.
  • Composite @@id lowers to SQL primary keys: @@id([email, token]) emits primaryKey.columns in field order, and mapped fields plus map: "..." are reflected in the contract.
  • Emitter validation matches the optional storage shape: contracts whose model table intentionally omits primaryKey are valid, but existing validation still checks primaryKey.columns when present.
  • Round-trip: printer output → interpreter is locked by a split-snapshot test pair. The 9-family side already snapshot-tests printPsl(sqlSchemaIrToPslAst(ir)) for id-less and composite-PK tables; a new describe('round-trips printer output', ...) block in interpreter.test.ts parses those exact snapshot strings, so a drift on either side breaks one of the two suites.

Compatibility / migration / risk

  • Existing valid @id models continue to emit the same SQL primary-key shape — no change to storageHash or profileHash for any pre-existing valid schema (verified per ADR 010 canonicalization rules: id is still emitted via ifDefined('id', primaryKey), so absent values remain absent).
  • The id-less acceptance is a validation relaxation, aligning the PSL interpreter with the already-tolerant core contract type (storage.tables.<t>.primaryKey was already optional per ADR 172).
  • The five tightenings are user-visible changes for schemas that were previously accepted but produced semantically broken contracts. Any schema relying on the silent-failure behavior was already producing a malformed contract — these changes reject those schemas at authoring time with actionable diagnostics. No schema that produced a valid contract before is rejected now.
  • PSL_MISSING_PRIMARY_KEY is removed from the diagnostic set. It has no production references outside the test that was updated; this is a ContractSourceDiagnostic code (authoring time), not a RuntimeError envelope code, so ADR 027 stability rules do not apply.
  • Downstream lanes are unaffected: ORM, sql-builder, kysely-lane, relational-core, and sql-lane have zero references to model.id or table.primaryKey. The 9-family migrations / schema-verify / control-instance code paths that do reference table.primaryKey already optional-chain. pnpm lint:deps reports zero violations.
  • ORM single-PK invariant (non-goal: changing ORM ops that assume a PK fallback) is now documented in docs/architecture docs/subsystems/3. Query Lanes.md as an implicit invariant pending a future per-model capability gate (e.g. sql.ormRequiresPrimaryKey). Today the ORM lane has no code that depends on model.id, so id-less models remain compatible with composition; the doc records the constraint for future ORM-op authors.

Validation

  • pnpm --filter @prisma-next/sql-contract-psl test — 132 tests pass (was 118; +14)
  • pnpm --filter @prisma-next/sql-contract-emitter test — 103 tests pass
  • pnpm --filter @prisma-next/sql-contract-psl typecheck
  • pnpm --filter @prisma-next/sql-contract-emitter typecheck
  • pnpm test:packages — 110 tasks, all green
  • pnpm lint:deps — 0 dependency violations

Follow-ups / open questions

  • Per-model sql.ormRequiresPrimaryKey capability flag and ORM-lane composition gate (currently doc-only).
  • Delete projects/id-less-sql-models/ during project close-out per projects/README.md.

Non-goals / intentionally out of scope

  • Changing ORM operations that assume a single primary key fallback.
  • Adding Prisma Client-like APIs for id-less models.
  • Changing document or MongoDB PSL behavior.

Summary by CodeRabbit

  • Documentation

    • Clarified primary-key requirements for ORM operations and guidance for id-less models.
  • Bug Fixes

    • Stricter validation and clearer diagnostics for primary-key/@id/@@id conflicts, duplicates, and invalid usages.
    • Validation now accepts tables without a primary key where appropriate.
  • Tests

    • Expanded coverage for primary-key scenarios, composite ids, and nullable-field round-trips.
  • Tooling

    • Emit now notifies the dev server about updated artifact files to improve live updates.

jkomyno added 7 commits May 4, 2026 22:39
Adds phase-specific execution defaults to SQL TS authoring so generated IDs stay on-create-only while updatedAt can use both create and update phases.

Wires Prisma-compatible @updatedat lowering for Postgres and SQLite PSL, plus field.updatedAt() helpers backed by target-owned timestampNow generators. Empty update payloads skip onUpdate defaults so no-op updates do not advance updatedAt.
Reject @updatedat before relation fields are discarded so invalid PSL cannot be accepted silently. Keep the adapter mutation default descriptors contextually typed and narrow optional runtime generator hooks in tests so package typechecks remain green.
Resolves todos/056 (P1) and todos/057 (P1) from the PR #422 review.

The phased mutation-default shape `executionDefaults: { onCreate?, onUpdate? }`
was added in 682714e alongside the legacy single-slot `executionDefault?`. The
legacy slot is `{ onCreate: X }` in disguise, so carrying both forces a runtime
guard, a dynamic property-name label, and duplicated default+nullable validation
across every layer (FieldNode, ValueObjectFieldNode, AuthoringFieldPresetOutput,
ScalarFieldState, ResolvedField). It also sets a precedent that adding onDelete
later requires another conditional in the reconciler.

Changes:

- Drop `executionDefault` from FieldNode, ValueObjectFieldNode,
  AuthoringFieldPresetOutput, ScalarFieldState, AnyScalarFieldState, and
  ResolvedField.
- Every site that produced the singular form now wraps as
  `executionDefaults: { onCreate: X }`. Affects field.generated, the family-sql
  id helper presets (uuidv4, uuidv7, ulid, nanoid, cuid2, ksuid), and PSL
  `lowerDefaultForField`.
- Delete `resolveExecutionDefaultPhases` and the dynamic `executionDefaultProperty`
  branch from build-contract.ts. The loop body checks `field.executionDefaults`
  directly.
- Loosen the nullable check (todo 057): only fire when `onCreate` is set.
  An `onUpdate`-only mutation default is fine on a nullable field — it can
  legitimately start as NULL until first update. New regression test:
  `allows nullable fields with onUpdate-only executionDefaults`.
- PSL `psl-field-resolution.ts` now combines `loweredDefault.executionDefaults`
  (on-create from generated `@default(...)`) and `updatedAtExecutionDefaults`
  (both phases from `@updatedAt`) via `??`. The two are mutually exclusive —
  `lowerUpdatedAtAttribute` rejects `@updatedAt @default(...)`.

Net change: -33 lines across 13 files. One validator function gone, one
runtime guard gone, one dynamic property-name label branch gone.

Validation:
- pnpm typecheck (123/123 tasks)
- pnpm lint:deps (clean)
- @prisma-next/framework-components test: 131/131
- @prisma-next/sql-contract-ts test: 218/218
- @prisma-next/sql-contract-psl test: 115/115
- @prisma-next/family-sql test: 285/285
- @prisma-next/adapter-postgres test: 615/615
- @prisma-next/adapter-sqlite test: 110/110
…agnostic (todos 061, 066)

- Extract `rejectUpdatedAtOnNonScalar` helper in `psl-field-resolution.ts` and
  call it from the two upstream relation/list-field early-return sites instead
  of duplicating the inline diagnostic. Scalar-field rules continue to live in
  `lowerUpdatedAtAttribute` as the authoritative codec gate, while the new
  helper is the single owner of the relation/list rejection wording.
- Pass `targetId` into `lowerUpdatedAtAttribute` and rewrite the
  `PSL_INVALID_DEFAULT_APPLICABILITY` message raised when the
  `timestampNow` generator is absent. The message now points users at the
  responsible target adapter rather than surfacing the SPI-level concept of a
  mutation default generator. The diagnostic code is unchanged so existing
  matchers continue to work.
…ext creation (todo 063)

Surface RUNTIME.MISSING_MUTATION_DEFAULT_GENERATOR at createExecutionContext
time when the contract references generator ids the assembled stack does
not provide. Walks contract.execution.mutations.defaults across both
onCreate and onUpdate phases, deduplicates missing ids, and throws a
single structured error. Aligns with ADR 031/065's startup-time
capability negotiation: contracts naming a generator id whose runtime
implementation isn't shipped now fail fast at context construction
rather than at first applyMutationDefaults call.

The existing lazy fallback in computeExecutionDefaultValue
(RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING) is retained as
defense-in-depth — the new contract-time check supplements it, it does
not replace it.
…(todos 058, 059, 060, 064)

- 064: export `TIMESTAMP_NOW_GENERATOR_ID` constant and
  `ExecutionMutationDefaultPhases` type from canonical sources; replace
  duplicated literals/aliases across PSL/TS authoring and adapters.
- 059: add `MutationDefaultGeneratorDescriptor.buildPhases?`; PSL
  `lowerUpdatedAtAttribute` now delegates phase construction to the
  generator descriptor (no inline literal). PSL/TS authoring stay
  byte-equivalent for any future params-bearing generator.
- 058: centralize `timestampNow` wiring in `@prisma-next/family-sql` via
  `timestampNowControlDescriptor(applicableCodecIds)` and
  `timestampNowRuntimeGenerator()`. Postgres + SQLite adapters opt in
  rather than re-declare the body.
- 060: add `RuntimeMutationDefaultGenerator.applyOnEmptyUpdate?` and
  switch `applyMutationDefaults` from a global empty-update short-circuit
  to a per-generator opt-out. Default `false` preserves @updatedat RD2;
  future onUpdate generators can opt in to sentinel-write semantics.

Test counts (post-change):
- @prisma-next/framework-components: 133 tests
- @prisma-next/sql-contract-ts: 218 tests
- @prisma-next/sql-contract-psl: 115 tests (incl. ts-psl-parity unchanged)
- @prisma-next/family-sql: 285 tests
- @prisma-next/sql-runtime: 186 tests + 1 skipped (incl. new
  applyOnEmptyUpdate=true regression)
- @prisma-next/adapter-postgres: 615 tests
- @prisma-next/adapter-sqlite: 110 tests
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Review Change Stack

Warning

Rate limit exceeded

@jkomyno has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 56 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 5b7d371e-bd95-4cf6-b2b8-01e9548d6a2a

📥 Commits

Reviewing files that changed from the base of the PR and between ba6b20c and c1d6b15.

📒 Files selected for processing (6)
  • docs/architecture docs/subsystems/3. Query Lanes.md
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.ts
📝 Walkthrough

Walkthrough

Derive model primary keys from single inline @id or validated @@id lists; reject optional fields as ids; update attribute parsing and diagnostics; accept id-less tables in emitter validation; expand tests; notify Vite moduleGraph on emits; document ORM primary-key invariant.

Changes

Primary Key Support for ID-less SQL Tables

Layer / File(s) Summary
Attribute Parsing Infrastructure
packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts
parseAttributeFieldList now uses code and entityLabel; findDuplicateFieldName added; mapFieldNamesToColumns uses entityLabel for diagnostics.
Field-level @id Validation
packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
@id on optional fields emits PSL_INVALID_ATTRIBUTE_ARGUMENT and clears id semantics; ResolvedField.isId derived from post-validation flag.
Primary Key Derivation and Validation
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
Derive primaryKey from a single inline @id; parse/validate @@id/@@unique/@@index with duplicate detection and nullable-column checks; relation mapping uses entityLabel; emit model id via ifDefined('id', primaryKey).
Tests: Diagnostics, Contracts, Round-trips
packages/2-sql/2-authoring/contract-psl/test/*, packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
Add negative @@id diagnostics tests; add/extend contract tests for id-less and composite-PK models; update round-trip nullable-fields test to parse the second emit and assert nullable flags.
Emitter Structure Validation
packages/2-sql/3-tooling/emitter/src/index.ts, packages/2-sql/3-tooling/emitter/test/emitter-hook.structure.test.ts
Explicitly lookup StorageTable with assertDefined during validation and update tests to accept tables without primaryKey.
CLI Tests & Vitest Config
packages/1-framework/3-tooling/cli/test/control-api/client.test.ts, packages/1-framework/3-tooling/cli/vitest.config.ts
Introduce createMockEmitResult() and beforeEach() to reset mocked emit results; set fileParallelism: false in Vitest config.
Vite Plugin: Emit Invalidation
packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts, packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts
Plugin notifies server.moduleGraph.onFileChange for emitted JSON and DTS when server exists; tests assert onFileChange is called for both files.
ORM Lane Documentation
docs/architecture docs/subsystems/3. Query Lanes.md
Add invariant: ORM read/write ops assume a primary key; id-less tables are supported for DSL/PSL composition but ORM operations relying on canonical identity will fail at runtime until capability gating exists.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

In printer light the schemas hop and play,
Keys born from fields or left to fade away.
I nibble duplicates, reject the shy null,
Emit the files, wake Vite's moduleHull,
DSL gardens grow while ORM waits to sway. 🐰

🚥 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 title 'feat(sql): support id-less models' directly and clearly summarizes the main objective of the PR, which is to enable SQL models without primary keys while implementing composite @@id support and tightening validation.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/idless-models

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 5, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-telemetry

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: c1d6b15

jkomyno added a commit that referenced this pull request May 5, 2026
…69, 070)

Reject the four silent-acceptance gaps surfaced by the PR #424 review:
duplicate @@id blocks, duplicate fields inside an @@id list, and optional
fields used as inline @id or in an @@id list. Adds the missing diagnostic
tests for each new branch and for the previously-untested inline-vs-block
conflict.
jkomyno added a commit that referenced this pull request May 5, 2026
Lift the duplicate-field-name check that PR #424 added to the @@id branch
to @@unique and @@index, where the same gap existed: a list like
@@unique([email, email]) previously emitted columns: ['email', 'email']
and only failed at DDL time (CREATE UNIQUE INDEX rejects duplicates),
while @@index([email, email]) silently produced a wasteful index.

Extracted findDuplicateFieldName into psl-attribute-parsing alongside the
other field-list helpers; called from all three model-attribute branches
with the unified entityLabel diagnostic shape.
jkomyno added 2 commits May 5, 2026 17:04
… rows

The runtime contract already encoded @updatedat as a both-phase execution
mutation default (timestampNow generator on onCreate and onUpdate), but
only the create path threaded it through applyMutationDefaults. ORM
update, updateAll, updateCount, the upsert update branch, and the nested
update branch in updateFirstGraph all skipped the call, so a non-empty
update never advanced updatedAt. Wire applyMutationDefaults into every
update path. Empty payloads still short-circuit before the runtime call,
preserving the no-write-no-advance rule.

Bulk inserts (createAll/createMany) called applyMutationDefaults per
row, which made timestampNow generate a fresh Date per row and drift
within a single ORM operation. Add a stableAcrossRows generator flag
plus an acrossRowsCache parameter to applyMutationDefaults so bulk
operations reuse one generated value across every row that needs the
default. Mark timestampNow as stableAcrossRows: true. Per-row
generators (cuid, uuidv4, etc.) stay independent because the cache
only honors stableAcrossRows generators.

Adds runtime-side tests for the cache contract and orm-client tests
covering create, createAll, updateAll, updateCount, upsert (both
branches), and explicit-value preservation.
…ability

Add Milestone 4 to the created-updated-at-authoring plan covering the
two compatibility gaps surfaced by comparing against Prisma 6's
@updatedat semantics: ORM update paths skipped applyMutationDefaults,
and bulk inserts drifted timestampNow per row. Also record the
naming choice (stableAcrossRows / acrossRowsCache) under Resolved
Decisions so future readers see why it didn't end up as bulkStable.
jkomyno added a commit that referenced this pull request May 5, 2026
…69, 070)

Reject the four silent-acceptance gaps surfaced by the PR #424 review:
duplicate @@id blocks, duplicate fields inside an @@id list, and optional
fields used as inline @id or in an @@id list. Adds the missing diagnostic
tests for each new branch and for the previously-untested inline-vs-block
conflict.
jkomyno added a commit that referenced this pull request May 5, 2026
Lift the duplicate-field-name check that PR #424 added to the @@id branch
to @@unique and @@index, where the same gap existed: a list like
@@unique([email, email]) previously emitted columns: ['email', 'email']
and only failed at DDL time (CREATE UNIQUE INDEX rejects duplicates),
while @@index([email, email]) silently produced a wasteful index.

Extracted findDuplicateFieldName into psl-attribute-parsing alongside the
other field-list helpers; called from all three model-attribute branches
with the unified entityLabel diagnostic shape.
@jkomyno jkomyno force-pushed the feat/idless-models branch from 76aacd8 to 8ff2127 Compare May 5, 2026 15:17
jkomyno added 14 commits May 6, 2026 12:55
Reframe the project around a generic PSL field-preset dispatch path
(mirroring the existing TS field-preset registry), exposed as
temporal.createdAt() / temporal.updatedAt() in PSL and
field.temporal.createdAt() / field.temporal.updatedAt() in TS.
The Prisma-flavored @updatedat attribute is removed; codec
applicability stops being a user-facing diagnostic and becomes a
registry-coherence concern. Contract IR, runtime, and ORM-client
work from prior milestones survive untouched (consolidated into
"Inherited Foundation"). All open design questions resolved
inline; reviewer feedback applied where it didn't conflict with
prior decisions.
Adds a generic field-preset dispatch path to PSL, symmetric with the
existing type-constructor dispatch. The path resolves namespaced calls
in field-type position against authoringContributions.field, instantiates
the preset via the same instantiateAuthoringFieldPreset machinery TS
uses, and produces default / executionDefaults / id / unique / nullable
contributions on the resolved field.

Field presets resolve before type constructors (presets carry richer
semantics; the more complete answer wins). The temporal namespace is
added to the curated-namespaces exemption list, alongside db/family/
target — chosen for forward-compatibility with the JS/TS Temporal API.

Preset usage is constrained per spec FR7: optional preset (?), list
preset ([]), preset + @default, preset + @id, and preset + @updatedat
are all hard errors with stable diagnostic codes. PSL → typed-args
coercion happens in instantiatePslFieldPreset via mapPslHelperArgs;
instantiateAuthoringFieldPreset itself stays typed-input-only.

Test fixture proves genericness via a synthetic temporal.exampleField()
preset (not a temporal-specific code path).
…sion check

Adds defense-in-depth and user-facing diagnostics that close out Phase A
of the field-preset pivot:

PSL preset-misuse diagnostics:
- PSL_PRESET_NOT_OPTIONAL — `temporal.X()?` rejected (preset is a complete
  declaration; system owns the value, optional contradicts that).
- PSL_PRESET_NOT_LIST — `temporal.X()[]` rejected (presets aren't list
  elements).
- PSL_PRESET_AND_DEFAULT_CONFLICT — `temporal.X() @default(...)` rejected
  (preset already specifies its own defaults).
- PSL_PRESET_AND_ID_CONFLICT — `temporal.X() @id` rejected when the preset
  doesn't itself contribute id semantics.
- PSL_PRESET_AND_UPDATED_AT_CONFLICT — `temporal.X() @updatedAt` rejected
  (preset already specifies execution defaults).
- PSL_UNKNOWN_FIELD_PRESET — typo in a curated namespace surfaces a
  spelling-targeted hint instead of falling through to the generic
  PSL_UNSUPPORTED_FIELD_TYPE.

Compose-time registry collision check:
- createComposedAuthoringHelpers now rejects any path registered as both
  a field preset and a type constructor with a clear "Ambiguous authoring
  registry path" error. Belt-and-suspenders to RD9's runtime field-preset-
  first ordering — the error surfaces at composition, not at PSL
  resolution.

Tests:
- Five preset-misuse cases in interpreter.defaults.test.ts.
- Cross-registry collision case in authoring-helper-runtime.test.ts.

Validation gate (all green):
- @prisma-next/sql-contract-psl: 121 tests
- @prisma-next/sql-contract-ts: 219 tests
- @prisma-next/framework-components: 133 tests
- pnpm lint:deps: 716 modules, 0 violations
The original RD11 said to rename PSL_INVALID_TYPE_CONSTRUCTOR_ARITY →
PSL_INVALID_NAMESPACED_CALL_ARITY. Phase A revealed the assumption was
wrong: that code name doesn't exist. Type-constructor arity/arg errors
today emit PSL_INVALID_ATTRIBUTE_ARGUMENT (a code shared with genuine
attribute-arg errors like @id(badarg)). The honest rename to a
PSL_INVALID_NAMESPACED_CALL_ARGUMENT code would need to disambiguate
those cases too, which is a wider refactor than this project warrants.

Decision: field-preset arity/arg errors emit PSL_INVALID_ATTRIBUTE_ARGUMENT
for now, matching the existing type-constructor pattern. Honest rename
deferred to a follow-up.

This commit:
- Updates FR7 to enumerate diagnostic codes that match what's actually
  implemented, with notes on which cases are tested in Phase A vs deferred.
- Marks AC5 sub-items by implementation status (a, b, d, e, h, j shipped
  with tests; c, f, g, i deferred or covered indirectly).
- Adds AC5j for the transient PSL_PRESET_AND_UPDATED_AT_CONFLICT code that
  disappears in Phase C alongside the @updatedat attribute path.
- Rewrites RD11 to reflect the discovery and the deferral decision.
- Updates plan.md task list and Resolved Decisions consistently.
Moves the createdAt/updatedAt field-preset registrations from flat
target.authoring.field.{createdAt,updatedAt} to nested
target.authoring.field.temporal.{createdAt,updatedAt} for both Postgres
and SQLite targets. The TS surface follows: field.createdAt() /
field.updatedAt() → field.temporal.createdAt() / field.temporal.updatedAt().

PSL was already wired in Phase A — temporal.createdAt() /
temporal.updatedAt() now resolve through the field-preset dispatch path
once the registry move lands here.

Updates to keep parity with the new namespace:
- Test fixtures: contract-builder.dsl.helpers.test.ts,
  ts-psl-parity.test.ts (sqliteTimestampTargetPack,
  postgresTimestampTargetPack, sqlFamilyPack).
- Examples: react-router-demo and prisma-next-demo TS contracts.
- CLI init templates (TS template) + snapshot.
- Integration type tests.
- Doc comments in mutation-default-types.ts and timestamp-now-generator.ts.

PSL examples (.prisma) and the @updatedat attribute path are unchanged
in Phase B — they ship intact through Phase C, which deletes the
attribute and updates PSL examples to use temporal.updatedAt().

Validation gate (all green):
- pnpm test:packages: 110/110 turbo tasks
- pnpm lint:deps: 716 modules, 0 violations
Deletes the Prisma-flavored @updatedat attribute path that landed on
this branch in commit 682714e. The codec-applicability validation
that motivated the pivot is gone; users now write temporal.updatedAt()
in PSL and field.temporal.updatedAt() in TS, both consuming the same
field-preset registry shipped in Phases A+B.

What's deleted from psl-field-resolution.ts (~110 lines):
- 'updatedAt' entry in BUILTIN_FIELD_ATTRIBUTE_NAMES
- reportInvalidUpdatedAt, rejectUpdatedAtOnNonScalar (helpers)
- lowerUpdatedAtAttribute (the codec-applicability path)
- The relation-field @updatedat rejection branches in
  collectResolvedFields
- The @updatedat lowering merge with @default(generator())
- The PSL_PRESET_AND_UPDATED_AT_CONFLICT branch (no longer reachable
  once the attribute is gone)

Migration hint:
- New helper getRemovedAttributeHint() returns a per-attribute
  suggestion. Currently has one entry: 'updatedAt' → 'Use
  `temporal.updatedAt()` as a field-preset call instead.'
- The hint is appended to the standard PSL_UNSUPPORTED_FIELD_ATTRIBUTE
  message when the attribute name matches and the field doesn't
  already declare a temporal.* preset (suppression check).
- Implementation is the requested hardcoded if-branch — no map.
  Adding more migrations later means adding another branch.

Tests:
- Replaced four @updatedAt-attribute tests in interpreter.defaults
  with temporal.updatedAt()-preset tests (Postgres + SQLite, plus
  inline preset registrations matching the production target shape).
- Added two new tests: migration-hint emission, and hint suppression
  for already-migrated fields (the `temporal.updatedAt() @updatedAt`
  half-migrated case).
- Removed the obsolete PSL_PRESET_AND_UPDATED_AT_CONFLICT test.
- Updated the PSL parity test (sqlite + postgres) to use
  temporal.updatedAt() PSL syntax.

Diagnostic-code inventory (per RD11): grep confirmed
PSL_INVALID_DEFAULT_APPLICABILITY remains in use elsewhere (storage
default lowering); PSL_PRESET_AND_UPDATED_AT_CONFLICT is now
orphaned and removed.

Doc-comment updates in:
- packages/1-framework/1-core/framework-components/src/shared/mutation-default-types.ts
- packages/3-extensions/sql-orm-client/src/collection.ts

Spec/plan updates:
- AC4 marked complete; references PSL_UNSUPPORTED_FIELD_ATTRIBUTE
  (the actual code) instead of the spec's earlier PSL_UNKNOWN_ATTRIBUTE
  (which doesn't exist).
- AC5j superseded note: half-migrated fields now use the suppression
  path instead of a dedicated diagnostic.

Validation gate: 120 PSL tests pass, full lint:deps clean (716 modules,
0 violations). adapter-postgres (615) and target-mongo (447) pass
standalone; turbo-aggregate flake unrelated to this commit.

BREAKING CHANGE: PSL @updatedat is no longer a recognized attribute.
Migrate to `temporal.updatedAt()` field-preset syntax. The unknown-
attribute diagnostic includes the migration hint inline.
Update existing references to the renamed/removed surfaces:

- docs/products/psl/README.md — timestamp section now lists
  temporal.createdAt() / temporal.updatedAt() as the preset surface
  alongside @default(now()), and notes that @updatedat is no longer
  recognized (with the migration-hint diagnostic).
- packages/2-sql/2-authoring/contract-psl/README.md — Responsibilities
  bullet and "Supported timestamp authoring surface" section.
- packages/2-sql/2-authoring/contract-ts/{README.md,API.md} — bulk
  rename field.createdAt() / field.updatedAt() →
  field.temporal.createdAt() / field.temporal.updatedAt() across
  helper-vocabulary lists, code examples, and timestamp-helper notes.

Scope-bounded: only updates pre-existing references — no
restructuring, no new sections, no docs that weren't already
in the inventory.

Validation: 110/110 turbo tasks (adapter-postgres flake confirmed
unrelated by standalone re-run, 615 tests pass), lint:deps 716
modules / 0 violations.
…gets

Hoist the `temporal.{createdAt,updatedAt}` field-preset descriptors into a
single `temporalAuthoringPresets({ codecId, nativeType })` helper exported
from `@prisma-next/family-sql/control`. Postgres and SQLite each pass only
their target-specific codec/native-type pair; the rest of the descriptor
(default expression, generator id, both phases) is owned by the helper, so
PSL `temporal.updatedAt()` and TS `field.temporal.updatedAt()` lower to
byte-identical contracts across targets by construction.

Also drops the unused flat `dateTime` preset from SQLite authoring — it had
no callers in `packages/`, `examples/`, or tests, and was a parity wart
relative to Postgres' pre-existing flat `dateTime`. The Postgres flat
`dateTime` is left untouched since it predates this branch.
The `applyOnEmptyUpdate?: boolean` flag on `RuntimeMutationDefaultGenerator`
was added as a forward-compat hook for hypothetical future generators that
want sentinel-write semantics on empty update payloads. No production
generator opts in; the only caller was a self-test of the flag itself.

Per CLAUDE.md ("Don't design for hypothetical future requirements"), drop
the flag, the runtime branch in `applyMutationDefaults`, and the self-test.
Empty-update skip is now unconditional. Re-add when a real generator needs
sentinel semantics, with a test exercising real production code.

RD2 ("empty update payloads skip onUpdate defaults") is still enforced —
just simpler now.
`build-contract.ts` previously allowed nullable fields when only
`executionDefaults.onUpdate` was present (no `onCreate`), reasoning that
the column could start NULL until first update. That branch was a
forward-compat hook for hypothetical onUpdate-only presets — dead code with
respect to `temporal.updatedAt()`, which always sets both phases and is
also rejected at the PSL layer via `PSL_PRESET_NOT_OPTIONAL`.

Per CLAUDE.md ("Don't design for hypothetical future requirements"),
collapse the check to "nullable + any executionDefaults = error" and
update the error message accordingly. Drop the dedicated test that
asserted the permissive branch. Re-add the allowance when a real preset
(e.g. `temporal.lastModifiedAt()` with onUpdate-only and a nullable
column) needs it, alongside that preset's own tests.
- Replace Phase B parity-note for SQLite's flat `dateTime` preset with a
  consolidation note pointing at the new `temporalAuthoringPresets()`
  helper (the flat preset was dropped; the consolidation makes per-target
  drift structurally impossible).
- Update the "temporal.updatedAt() semantics" RD to reflect that the
  `nullable + onUpdate-only` allowance is no longer speculatively built
  in — a future onUpdate-only preset will introduce it alongside its
  own tests.
- Add a "YAGNI cuts at PR-close" RD recording the two forward-compat
  hooks removed before merge: `applyOnEmptyUpdate` opt-in and
  `nullable + onUpdate-only` allowance.
Resolves a single modify/delete conflict on `docs/products/psl/README.md`.
The file was deleted on main in commit b5c3381 ("elevate PSL authoring
to a top-level May workstream") for being "significantly stale,
unreferenced, and actively misleading." This branch had previously made
small edits to the same file as part of Phase D (spec AC9 / FR10 named it
as the canonical PSL doc).

Resolution: accept main's deletion. The temporal-preset writeup survives
in the package READMEs (`packages/2-sql/2-authoring/contract-psl/README.md`,
`packages/2-sql/2-authoring/contract-ts/{README.md,API.md}`), none of
which link back to the deleted file. A fresh canonical PSL doc is owned
by the May WS5 workstream and is out of scope for this PR.

`plan.md` records the AC9/FR10 adjustment.

Validation: full package test suite green post-merge (sql-contract-psl,
sql-contract-ts, sql-runtime, sql-orm-client) plus `pnpm lint:deps`.
…sting

Per CodeRabbit review on PR #422: `resolveAuthoringColumnDefaultTemplate`
in `framework-authoring.ts` was casting `value as ColumnDefaultLiteralInputValue`
without runtime validation. Since `resolveAuthoringTemplateValue` returns
`unknown` (it dereferences caller-provided `AuthoringArgRef` indices into
arbitrary user args), the cast would silently emit class instances or
non-JSON-serializable types into the contract.

Add an `isColumnDefaultLiteralInputValue` predicate to `contract/types.ts`
that recursively validates JSON primitives, plain arrays/objects of JSON
values, and `Date` instances. Use it in `framework-authoring.ts` to throw
a clear error when a resolved literal default doesn't match the input
shape — mirrors the pattern already used by
`resolveExecutionMutationDefaultPhase` with `isExecutionMutationDefaultValue`.

Drops the cast entirely; the predicate narrows `value` to the exact type.
…dule

Per CodeRabbit review on PR #422: `src/core/timestamp-now-generator.ts`
mixed control-plane (`timestampNowControlDescriptor`, `temporalAuthoringPresets`)
and runtime-plane (`timestampNowRuntimeGenerator`) implementations in a
single file. The runtime function pulled in `RuntimeMutationDefaultGenerator`
from `@prisma-next/sql-runtime`, dragging a runtime-plane import into a
file otherwise consumed only by control-plane callers.

Extract `timestampNowRuntimeGenerator` into a dedicated
`src/core/timestamp-now-runtime-generator.ts`. The control-plane file no
longer imports from `@prisma-next/sql-runtime`. `src/exports/runtime.ts`
re-exports from the new location.

No behavior change; `temporal.updatedAt()` and the per-target adapter
wiring continue to resolve the same descriptor + generator pair.
jkomyno added 6 commits May 7, 2026 10:27
Per CodeRabbit review on PR #422: the `stableAcrossRows` test compared
`.getTime()` across the three Dates observed in a 3-row bulk insert. That
assertion would still pass if the generator produced three distinct `Date`
instances within the same millisecond, defeating the point of the test.

Tighten to identity (`toBe`), so the test fails unless the cached `Date`
instance is reused across rows — which is exactly what `stableAcrossRows`
+ `acrossRowsCache` claim to do.
…69, 070)

Reject the four silent-acceptance gaps surfaced by the PR #424 review:
duplicate @@id blocks, duplicate fields inside an @@id list, and optional
fields used as inline @id or in an @@id list. Adds the missing diagnostic
tests for each new branch and for the previously-untested inline-vs-block
conflict.
…K invariant (todos 071, 072, 073, 074, 075)

Replace the three-variable (inlinePrimaryKey/modelPrimaryKey/span) plus
post-loop merge with a single primaryKey local; co-locate the inline-vs-block
and duplicate-@@id conflict checks inside the @@id branch. Reject inline
@id on multiple fields (PSL composite identity must use @@id), drop the
two avoidable as-SqlStorage casts in the new tests, add round-trip
companion tests that exercise the printer's snapshot output through the
interpreter, and document the implicit ORM single-PK invariant in
docs/architecture docs/subsystems/3. Query Lanes.md.
…el (todo 078)

Rename messagePrefix and contextLabel parameters on parseAttributeFieldList
and mapFieldNamesToColumns to entityLabel, matching the dominant naming
convention across psl-attribute-parsing, psl-column-resolution,
psl-authoring-arguments, and framework-components/control. Hoist the
"Model X @@<attr>" label to a single per-iteration local in the model
attribute loop instead of recomputing it 6 times across the @@id /
@@unique / @@index branches. No behavior change; diagnostic message text
unchanged.
Lift the duplicate-field-name check that PR #424 added to the @@id branch
to @@unique and @@index, where the same gap existed: a list like
@@unique([email, email]) previously emitted columns: ['email', 'email']
and only failed at DDL time (CREATE UNIQUE INDEX rejects duplicates),
while @@index([email, email]) silently produced a wasteful index.

Extracted findDuplicateFieldName into psl-attribute-parsing alongside the
other field-list helpers; called from all three model-attribute branches
with the unified entityLabel diagnostic shape.
@jkomyno jkomyno force-pushed the feat/idless-models branch from 8ff2127 to 405ea06 Compare May 7, 2026 08:33
@jkomyno
Copy link
Copy Markdown
Contributor Author

jkomyno commented May 7, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@saevarb saevarb left a comment

Choose a reason for hiding this comment

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

This looks good to me. 👍 I also ran it through my agent and it didn't really come up with anything substantive either.

Preserve literal temporal preset codec and native types so the integration type assertions keep exact target strings.

Stabilize the package tests by matching the nullable emitter round-trip timeout to the other TypeScript round-trip cases, isolating the CLI emitter mock baseline, and disabling CLI file parallelism under non-isolated Vitest.

Invalidate Vite generated contract artifacts after each successful emit so framework SSR modules reload fresh contract JSON during dev re-emits.
Copy link
Copy Markdown
Contributor

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

Looks good barring two small comments 👍🏻

Comment thread docs/architecture docs/subsystems/3. Query Lanes.md Outdated
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.

There are no corresponding changes to the Mongo PSL interpreter - should there be?

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.

That is on purpose! It would've bloated out the scope of this PR. I called it out in the PR description as a non-goal :)

Comment thread packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
Comment thread docs/architecture docs/subsystems/3. Query Lanes.md Outdated
Copy link
Copy Markdown
Contributor

@SevInf SevInf left a comment

Choose a reason for hiding this comment

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

Nice job, just a few comments.
I think we'll need a follow up to this to properly assess and gate all the places where ORM actually expects PK

Base automatically changed from feat/created-updated-at-authoring to main May 8, 2026 08:04
# Conflicts:
#	packages/1-framework/3-tooling/cli/src/commands/init/templates/code-templates.ts
#	packages/1-framework/3-tooling/cli/test/commands/init/__snapshots__/templates.test.ts.snap
#	packages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.ts
#	packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
#	packages/2-sql/2-authoring/contract-psl/test/interpreter.defaults.test.ts
#	packages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.ts
#	packages/2-sql/2-authoring/contract-ts/src/composed-authoring-helpers.ts
#	packages/2-sql/4-lanes/relational-core/src/query-lane-context.ts
#	packages/2-sql/5-runtime/src/sql-context.ts
#	packages/2-sql/5-runtime/test/sql-context.test.ts
#	packages/2-sql/9-family/src/core/timestamp-now-generator.ts
#	packages/2-sql/9-family/src/core/timestamp-now-runtime-generator.ts
#	packages/3-extensions/sql-orm-client/src/collection.ts
#	packages/3-extensions/sql-orm-client/test/collection-mutation-defaults.test.ts
#	packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts
#	packages/3-targets/6-adapters/postgres/src/exports/runtime.ts
#	packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts
#	packages/3-targets/6-adapters/sqlite/src/core/control-mutation-defaults.ts
#	packages/3-targets/6-adapters/sqlite/src/core/runtime-adapter.ts
#	packages/3-targets/6-adapters/sqlite/test/control-mutation-defaults.test.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts (1)

236-238: ⚡ Quick win

Use one object matcher for related nullable assertions.

These three assertions validate one logical shape and can be expressed as a single toMatchObject for consistency with repo test style.

💡 Suggested change
-      expect(id['nullable']).toBe(false);
-      expect(email['nullable']).toBe(true);
-      expect(name['nullable']).toBe(false);
+      expect({
+        id: id['nullable'],
+        email: email['nullable'],
+        name: name['nullable'],
+      }).toMatchObject({
+        id: false,
+        email: true,
+        name: false,
+      });

As per coding guidelines, "**/*.test.ts: Prefer object matchers (toMatchObject) over multiple individual expect().toBe() calls when checking 2 or more related values in tests".

🤖 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/3-tooling/emitter/test/emitter.roundtrip.test.ts` around
lines 236 - 238, Replace the three separate nullable assertions for id, email,
and name with a single object matcher assertion using toMatchObject: build an
object mapping each field to its nullable value (referencing the existing id,
email, name symbols) and assert that the container/object being tested matches
that shape with toMatchObject, keeping the same expected boolean values for
nullable on id (false), email (true) and name (false).
🤖 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.

Nitpick comments:
In `@packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts`:
- Around line 236-238: Replace the three separate nullable assertions for id,
email, and name with a single object matcher assertion using toMatchObject:
build an object mapping each field to its nullable value (referencing the
existing id, email, name symbols) and assert that the container/object being
tested matches that shape with toMatchObject, keeping the same expected boolean
values for nullable on id (false), email (true) and name (false).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b1699a2d-e5ea-4e37-ae62-b7227cab2d1f

📥 Commits

Reviewing files that changed from the base of the PR and between 405ea06 and ba6b20c.

📒 Files selected for processing (7)
  • packages/1-framework/3-tooling/cli/test/control-api/client.test.ts
  • packages/1-framework/3-tooling/cli/vitest.config.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/1-framework/3-tooling/cli/vitest.config.ts
  • packages/1-framework/3-tooling/cli/test/control-api/client.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts

@jkomyno
Copy link
Copy Markdown
Contributor Author

jkomyno commented May 8, 2026

Follow-up from the review summary: agreed that the ORM primary-key assumptions need a separate pass. I kept this PR scoped to the id-less contract/authoring work plus docs clarification. The remaining ORM work is to audit and gate primary-key fallback paths, not to block predicate-based find/update/delete on id-less tables.

@jkomyno jkomyno merged commit 57cdd5e into main May 8, 2026
16 checks passed
@jkomyno jkomyno deleted the feat/idless-models branch May 8, 2026 12:27
jkomyno added a commit that referenced this pull request May 8, 2026
Source content for the Linear ticket that this PR should land under.
Follow-up to TML-2380 (PR #424); covers the ORM-lane completion work
that builds on the authoring/emission support TML-2380 introduced.

Includes scope, out-of-scope, acceptance criteria, and references.
Maintainer will create the Linear ticket from this content and either
replace this file with the ticket URL or delete it post-creation.
jkomyno added a commit that referenced this pull request May 8, 2026
…tion

Linear ticket created: https://linear.app/prisma-company/issue/TML-2432
The Linear: line is now at the top of the PR description, matching the
convention used by the parent PR #424 (TML-2380).
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.

4 participants