Skip to content

TML-3037: contract infer output round-trips through contract emit#998

Open
wmadden-electric wants to merge 25 commits into
mainfrom
tml-3037-contract-infer-output-round-trips-through-contract-emit
Open

TML-3037: contract infer output round-trips through contract emit#998
wmadden-electric wants to merge 25 commits into
mainfrom
tml-3037-contract-infer-output-round-trips-through-contract-emit

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Here is a fragment of packages/3-extensions/supabase/scripts/generate-contract.ts, on main, before this PR:

const DOUBLE_PLURALIZED_FIELD_NAMES: ReadonlySet<string> = new Set([
  'icebergNamespaceses', 'icebergTableses', 'identitieses', 'mfaAmrClaimses',
  'mfaChallengeses', 'mfaFactorses', 'oauthAuthorizationses', 'oauthConsentses',
  'objectses', 'oneTimeTokenses', 'refreshTokenses', 's3MultipartUploadsPartses',
  // …twenty in total
]);

We ship a script that repairs contract infer's output before our own contract emit will accept it. Alongside that list are tables of column omissions, default omissions, and index omissions — each with a careful comment explaining which part of our own pipeline rejects our own output.

This PR deletes 170 lines of that script and fixes what it was working around.

Why now

A user sent in a 260-line fix-inferred-contract.ts they run after every contract infer. Auditing it claim-by-claim against the source turned up nine defects. They had written our script independently, against a different database, without ever seeing ours.

Two people arriving at the same workaround is the finding. contract infer writes PSL that contract emit rejects, or that db verify then reports as drift — and no test anywhere did introspect → infer → emit, which is why all nine shipped. The e2e suite had FK-bearing tables but never ran them through infer; identity columns had no coverage at all.

So the first commit here is the instrument, not a fix: a round-trip journey against a live database carrying already-plural table names, 1:1 and 1:N and self-referencing FKs, both identity variants, serial, bounded and unbounded numeric, date, text[], jsonb, GIN and hash indexes, and an FK pointing out of scope. Every defect below was reproduced by it before being fixed, and each fix is the commit where its assertion flips green. projects/infer-emit-roundtrip/reproduction.md records the pre-fix state verbatim and is deliberately not updated.

What was broken

Back-relation names double-pluralized. pluralize() appended es to anything ending in s, so already-plural table names — which is most real schemas — came out sessionses. Now uses the maintained pluralize library. Closes TML-3024, whose bar was "the Supabase generator override is removed and the regenerated contract is unchanged by its removal". It is, and it is.

Infer printed a 1:1 back-relation emit couldn't parse. A bare profile Profile?. The uniqueness detection producing it was correct; the interpreter collected back-relation candidates only if (field.list). We had a snapshot test asserting we print PSL our own emitter rejects.

Decimal never worked on Postgres. This one isn't an infer bug at all. Decimal maps to pg/numeric@1 on the base-scalar path with no typeParams, so any schema with a plain amount Decimal field throws RUNTIME.CODEC_PARAMETERIZATION_MISMATCH at connect. NumericParams.precision was required while every sibling temporal codec's param is optional, and three other layers already handled a missing precision. Infer is simply the first thing that generates a Decimal — there isn't one in any .prisma file in this repo, which is why nobody hit it.

No pg/date codec existed. @db.Date carried codecId: null ("inherit from base"), and DateTime on Postgres is pg/timestamptz@1. Top-level rows survived because decode() is a passthrough over an already-parsed Date; .include() goes through json_aggdecodeJson(), which regex-checks for a full ISO timestamp and rejects "2024-01-15".

Non-btree indexes could never emit. Infer prints @@index(…, type: "gin"); the Postgres target registered zero index types, so emit threw unregistered index type "gin". Any real database has one.

Identity columns lost their default. The columns query joined pg_attribute but never selected attidentity. serial worked only because it sets a real nextval(...) default.

dbgenerated literal defaults drifted forever. Emit kept '{}'::jsonb as kind: 'function'; introspection parsed it to kind: 'literal'; resolvedDefaultsEqual compares kind first. db verify reported such columns not-equal permanently, even when the database matched exactly.

Dangling FKs dropped silently. Infer correctly drops an FK whose target is outside the introspected scope, but said nothing — so a user loses every auth.users relationship with no indication. It now says so, and points at the likely cause.

Two decisions worth your attention

Identity maps onto autoincrement(), symmetrically. The obvious fix — emit @default(autoincrement()) for an identity column — trades the emit break for a verify break: the contract would carry a function default while the live column reports none, and it would drift forever. So both sides move: infer emits it, and the normalizer resolves a live identity column to it.

At the contract's altitude autoincrement() means "the database generates this value", and identity and serial are both that. PSL has no syntax to distinguish them, so this PR does not model GENERATED ALWAYS vs BY DEFAULT. The accepted cost: a fresh db init from such a contract creates serial, not identity. That gap pre-exists — identity has never been authorable — and stays out of scope.

The Postgres target registers its own built-in index types (btree, hash, gin, gist, spgist, brin), via the same IndexTypeRegistry mechanism ParadeDB uses for bm25. That's the extension point used as designed: the target declares its own built-ins, extensions contribute types they own. build-contract.ts composes [target, ...extensionPacks], so they coexist. An unregistered type is still rejected — there's a test.

Breaking changes

Upgrade instructions are recorded in skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/ and the extension-author mirror.

  • @db.Date columns change codecId from pg/timestamptz@1 to pg/date@1 on the next emit, with no source change — so storageHash changes.
  • dbgenerated(...) literal defaults resolve differently on the next emit — storageHash changes.
  • Back-relation names change on a future contract infer re-run (sessionsessessions). These are public field names consumers type, which is exactly why this was worth fixing before more people ran infer.
  • Identity columns need an explicit default under db verify --strict only; non-strict verify filters an undeclared live default out entirely.

Index-type registration and the Decimal fix are not breaking: both previously threw unconditionally, so nothing could have depended on them.

Deliberately not fixed

@default(null) on a nullable column, and nullable list columns — two further round-trip gaps, documented in generate-contract.ts's own comments. Both real, both needing their own design. auth.users.phone is the one entry left in DEFAULT_OMISSIONS, and its comment now says it's the only reason the table still exists.

Also out: PSL constraint: false (FK-less relations are TS-DSL-only), identity DDL authoring, and per-index-method option validation.

One claim from the user's script resolved itself: the NUL-bytes-where-@-should-be workaround (replaceAll('\0', '@'), the first thing their script does). That was a real bug, and we fixed it separately before 0.15.0 was cut. It reads as unreproducible here because we tested against post-fix code — their script is pinned to a version that predates the fix, which also explains why it's the one line in the file with no explanatory comment and the one omission from their own re-verification list. Nothing to do here; the report back tells them to delete the line and to shout if it recurs on 0.15.0.

Alternatives considered

Eight separate PRs. Every defect is an instance of one class, they share one instrument and one acceptance bar, and the evidence they're worth fixing at all is collective. Split up, each reviewer sees a one-line change with no way to judge whether it's the right one-line change — and the pack script shrinking, which is the actual proof, wouldn't appear in any of them.

Make infer emit lists for 1:1 back-relations. Would have made emit pass by discarding real information. The contract already supports '1:1'; the gap was PSL-side only.

Relax the list-default check to permit storage function defaults. This is what the spec originally prescribed, and it's wrong — it admits tags DateTime[] @default(now()), whose DDL Postgres refuses, moving the error from authoring time to apply time. Nothing in the authoring layer can tell whether a function returns an array. Review caught it; the tell was that the implementation needed a special case for autoincrement() to plug one instance of the hole it had opened. The array default works anyway, because the literal-default fix means it never reaches that check.

Review trail

An Opus review pass before opening found five issues, three of which were this same failure repeating inside the PR's own diff — a symptom fixed instead of a class, and a test deleted rather than satisfied. All are addressed in the last four commits; the reproduction record and the spec's three correction blocks show where the plan was wrong and what the evidence said instead.

Refs: TML-3037
Closes: TML-3024

Summary by CodeRabbit

  • New Features

    • Added native PostgreSQL date support (YYYY-MM-DD) with correct encoding/decoding.
    • Improved PostgreSQL schema inference for identity columns, including @default(autoincrement()).
    • Added support for additional index types during schema inference and round-tripping.
  • Bug Fixes

    • Fixed one-to-one back-relation inference and pluralization to prevent malformed relation fields.
    • Improved handling of defaults (identity/generation, function defaults, and array literals) to enhance infer→emit→verify fidelity.
    • Added clearer diagnostics and warnings for unsupported fields and dangling foreign keys during inference.

Spec and plan for TML-3037. An external user's post-processing script,
audited claim-by-claim, surfaced eight defects — all instances of one
class: contract infer writes PSL that contract emit rejects, or that
db verify then reports as drift.

Our own extension-supabase generate-contract.ts is independently the
same workaround script, which is the evidence this is worth fixing at
the source rather than papering over per-consumer.

No test anywhere does introspect → infer → emit. That is why all eight
shipped, and building that instrument is the first dispatch.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…against a live database

`contract infer` output does not survive `contract emit`, and no test anywhere
drives introspect -> infer -> emit, which is why eight defects shipped. This
adds the instrument that reproduces all of them, and the failure record it
produced.

Two files, because the CLI path cannot observe runtime decode:

- `cli-journeys/infer-roundtrip-fidelity.e2e.test.ts` drives infer -> emit ->
  `db verify --schema-only` over a schema carrying already-plural table names,
  a 1:1 and a 1:N FK, both `GENERATED ... AS IDENTITY` variants plus a `serial`
  control, bounded and unbounded `numeric`, a `date`, array/jsonb defaults, GIN
  and `USING hash` indexes, and an FK pointing out of the introspected scope.
- `infer-roundtrip-runtime.integration.test.ts` builds a real `ExecutionContext`
  against an emitted contract and reads a `date` through `.include()` — the only
  way to see the numeric-connect and date-decode defects.

All ten assertions fail today; each asserts the fixed behaviour, so each flips
green as its fix lands. Each `it()` isolates one defect, repairing the unrelated
emit-blockers so a single run reports every defect independently.

`projects/infer-emit-roundtrip/reproduction.md` records, per defect, the command
that surfaces it and the verbatim error. Two findings correct the spec: the
unbounded-numeric crash arrives via the bare `Decimal` base scalar, not via
`@db.Numeric` with no args (infer never prints that attribute), so the affected
surface is every `Decimal` field on postgres; and only the to-many side of a
relation double-pluralizes, so this fixture reproduces `sessionses` but not
`identitieses`.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…gres

D1 reproduced the numeric crash and showed the spec had the mechanism
wrong. It does not arrive via @db.Numeric with no arguments; infer never
prints that. It arrives via the base-scalar path, where
control-mutation-defaults maps Decimal to pg/numeric@1 with no
typeParams — so a plain `amount Decimal` field crashes at connect
regardless of any attribute.

This is not an infer defect that happens to touch Decimal. Decimal has
never worked on postgres; infer is just the first thing that generates
one. No .prisma file in the repo declares a Decimal field, which is why
it went unnoticed.

The chosen fix is unchanged, but D4 must test the bare scalar — an
attribute-only test would pass while the defect ships.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
`pluralize()` appended `es` to any word ending in s/x/z/ch/sh, so an
already-plural table name like `sessions` doubled to `sessionses` in
inferred back-relation field names.

Replaced the hand-rolled suffix rule with the `pluralize` package
(real inflection: irregular/uncountable word lists plus ordered
regex rules, not a trailing-character heuristic), per TML-3024 and
the spec's preference for a maintained library over a hand-rolled
table. Verified it satisfies every row of the acceptance table,
including the two cases that pull in opposite directions: already
plural (sessions, identities, mfaAmrClaims unchanged) and
singular-but-s-ending (status -> statuses, bus -> buses, class ->
classes).

Added `@types/pluralize` as a dev dependency since the library ships
untyped. Import as a default import (`import pluralizeLib from
'pluralize'`), not a namespace import: pluralize assigns its whole
CJS module.exports in one statement, so cjs-module-lexer-based
interop (Node's native ESM loader, used when the CLI runs the built
dist/psl-infer.mjs) cannot statically detect `.plural` as a named
export and leaves the namespace with only `default`. A default
import always resolves to the full module.exports value, so
`pluralizeLib.plural` works reliably. Vitest hides this because Vite
pre-bundles CJS deps with esbuild, which copies the CJS exports'
own properties onto the namespace at runtime; confirmed the failure
and the fix against a real Node ESM import, not just the test
runner.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ults infer prints

The interpreter only collected back-relation candidates for list-typed
fields, so a bare, `@relation`-less model-typed field (the back side of
a 1:1) fell through to scalar resolution and died with
PSL_UNSUPPORTED_FIELD_TYPE. A model-typed, non-list field with no
`@relation` can only be the back side — the owning side always carries
`@relation(fields, references)` to declare its FK columns — so this is
never ambiguous. ModelBackrelationCandidate now carries `isList`, and
applyBackrelationCandidates resolves cardinality to `1:1` for a
singular candidate (`1:N` unchanged for list) and skips many-to-many
junction matching for singular candidates, since that concept only
applies to lists.

Separately, the list-default check conflated a storage-level SQL
default (`dbgenerated(...)`, `now()`, lowered as
`defaultValue.kind: "function"`) with a genuine per-element execution
default (`executionDefaults.onCreate`, e.g. `uuid()`), rejecting both
on list fields. Only the latter has no list semantics; Postgres accepts
`DEFAULT (expression)` on an array column regardless of `many`, and
buildColumnDefaultSql already renders it that way. The check now only
rejects `executionDefaults.onCreate` on a list field.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Permitting storage-level function defaults on lists (the defect-7 fix)
also let `Int[] @default(autoincrement())` through, which the postgres
DDL builder miscompiles: buildColumnTypeSql keys on the
`autoincrement()` sentinel to emit SERIAL and drops the array suffix
entirely. The old broad `kind === "function"` rule blocked that shape,
but for the wrong reason — it called every storage default an
execution default. Restoring it would undo defect 7, so this adds a
narrow guard instead: a sequence yields one scalar per row, which an
array column cannot be.

The check keys on the lowered `autoincrement()` sentinel rather than
the authored call name. Every target lowers the call to that exact
expression and keys its DDL rendering off it, so rejecting the
sentinel keeps the shape out of the DDL builders however it was
spelled.

Both sides of the line are tested: Int[] @default(autoincrement())
errors, String[] @default(dbgenerated("{}::text[]")) still lowers to a
storage function default. Scalar @default(autoincrement()) is
untouched.

Also regenerates the print-psl inline snapshot invalidated by the
pluralize fix: `child` now pluralizes to `children`, the correct
irregular plural, where the hand-rolled rule produced `childs`.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… date

`NumericParams.precision` was required while every sibling temporal codec
(`PrecisionParams`) declares its param optional. A bare `amount Decimal`
field — the base-scalar path `control-mutation-defaults.ts` maps every
postgres `Decimal` field through, with or without `@db.Numeric` — produced a
`pg/numeric@1` ref with no `typeParams` and crashed `createExecutionContext`
with `RUNTIME.CODEC_PARAMETERIZATION_MISMATCH`. Make `precision` optional on
`NumericParams`/`numericParamsSchema`, matching the sibling pattern;
`assertColumnCodecIntegrity` is untouched. A bounded `numeric(10,2)` still
carries and enforces its params.

No `pg/date@1` codec existed: `@db.Date` inherited `DateTime`s
`pg/timestamptz@1`. Top-level `SELECT` survived because `decode()` was a
passthrough over the driver-parsed `Date`, but `.include()` (`json_agg` ->
`decodeJson()`) rejected a bare `YYYY-MM-DD` string. Add a real `pg/date@1`
codec and point `@db.Date` at it. A Postgres `date` has no timezone, so the
codec canonicalizes its JS-level value as a `Date` at UTC midnight:
`decode()` re-derives the calendar date from the pg drivers local-midnight
`Date` via local getters and reconstructs it via `Date.UTC`; `encode()`
formats via UTC getters directly to `YYYY-MM-DD`, bypassing the pg drivers
own local-getter-based `Date` serialization (which would shift the day near
midnight under a negative UTC offset). `decodeJson`/`encodeJson` share the
same `YYYY-MM-DD` representation.

BREAKING: `@db.Date` columns now emit `codecId: "pg/date@1"` instead of
inheriting `"pg/timestamptz@1"`, which changes the contract hash and
therefore signed markers for any existing `@db.Date` column. Upgrade
instructions are dispatch D9s job, not covered here.

RT.01 (unbounded numeric via infer) and RT.02 (date via .include()) in
test/integration/test/infer-roundtrip-runtime.integration.test.ts now pass.
RT.02s top-level assertion is tightened from toBeInstanceOf(Date) to the
exact UTC instant, now that pg/date@1 owns the conversion and the round trip
no longer depends on the process timezone. Added RT.03, authoring
"amount Decimal" directly (no @db.Numeric, bypassing infer) so the
base-scalar fix is proven independently of infers own behavior.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…dangling FKs

The postgres target registered zero index types, so validateIndexTypes
rejected any @@index(..., type: "gin"/"hash") contract infer produced for a
non-default access method. Register the six access methods Postgres ships
(btree, hash, gin, gist, spgist, brin) via IndexTypeRegistry, following the
same wiring ParadeDB already uses for its own bm25 type. Options schemas stay
permissive; per-method option validation is a later slice. btree is
Postgres default access method already normalized to `undefined` by
introspection, so registering it does not change what gets emitted for an
ordinary index.

Separately, resolveForeignKeys correctly drops a foreign key whose target is
outside the introspected schema, but did so silently: the scalar column
survived with no comment and no warning, so a user loses every such
relationship (e.g. every auth.users FK in a Supabase database) with no
indication. Track dropped FKs per table and emit an explanatory comment on
the model, matching the existing missing-primary-key comment mechanism and
voice.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… both sides

`GENERATED ALWAYS AS IDENTITY` and `GENERATED BY DEFAULT AS IDENTITY` columns
report no `column_default` at all — Postgres tracks generation via
`pg_attribute.attidentity`, not a default expression — so infer emitted a
bare column with no default, and even once printed, the introspected side
had nothing to compare a `@default(autoincrement())` contract against.

Fixed symmetrically, on both sides:

- `SqlColumnIR` gains an `identity` boolean (introspection-only; a
  contract-derived column never sets it, since PSL has no identity
  vocabulary).
- The postgres columns query selects `a.attidentity` and threads it onto the
  column; the control adapter computes `resolvedDefault` from the identity
  flag when set, since there is no raw expression to parse.
- `parsePostgresDefault` takes an `identity` option that resolves straight to
  `autoincrement()`, independent of `rawDefault`.
- Postgres infer (`buildScalarField`) prints `@default(autoincrement())` for
  an identity column, the same as it already does for `serial`.

Deliberate boundary: at the contract's altitude, `autoincrement()` means "the
database generates this value" — identity and `serial` are both that, and
PSL has no syntax to distinguish `GENERATED ALWAYS` from `GENERATED BY
DEFAULT`. This maps both identity variants onto `autoincrement()` rather
than modelling the distinction. Consequence: a fresh `db init` from such a
contract creates a `serial` column, not an identity one — a pre-existing gap
(identity has never been authorable), unchanged by this fix.

Verified against a live database: a new introspection integration test
proves both identity variants thread through as `identity: true` with
`resolvedDefault: autoincrement()`, that `serial` is unaffected, and that a
plain column gets no default. The TML-3037 D1 instrument's IF.06 assertion
(both `GENERATED` variants print `@default(autoincrement())`; `serial`
keeps working) now passes, and a manual reduction of the same journey
(stripping only the two known non-identity default mismatches) shows `db
verify --schema-only` reports zero drift for the identity columns.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
D6 exposed a second instance: dbgenerated("'{}'::text[]") drifts
identically to the jsonb case. D1 could not reproduce it because emit
failed before verify ever ran; clearing the emit blockers uncovered it.

The defect is any literal default the authoring and introspection paths
resolve differently — kind:function on one side, kind:literal on the
other, and resolvedDefaultsEqual compares kind first. jsonb and text[]
are two instances; enumerate the rest from parsePostgresDefault rather
than from a list.

The chosen fix already covers the class, because reusing
parsePostgresDefault (rather than copying it) means a form it learns to
parse later is consistent on both sides automatically.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…aults, not just function text

Emit kept every `@default(dbgenerated("..."))` as `kind: 'function'`, even when the
raw SQL text was actually a literal (`'{}'::jsonb`, `'{}'::text[]`, NULL, true/false,
a bare number, a plain string). Introspection parses the same text through
`parsePostgresDefault` and gets `kind: 'literal'` for all of those forms.
`resolvedDefaultsEqual` compares `kind` before content, so any dbgenerated
literal drifted forever in `db verify`, even when the database matched the
contract exactly. jsonb and text[] were the two instances TML-3037 caught,
but the mismatch is generic to every literal form the normalizer recognizes.

Fix the class: `lowerDbgenerated` (adapter-postgres) now resolves the raw
text through the same `parsePostgresDefault` introspection already uses,
instead of keeping it as opaque text. A form the normalizer does not
recognize as a literal falls through to its own function fallback, so
genuine functions (`gen_random_uuid()`, `nextval(...)`, arbitrary
expressions) are unaffected.

`parsePostgresDefault` needs the column's native type to parse array and
jsonb literals correctly, so `DefaultFunctionLoweringContext` gains an
optional `nativeType` field, populated by the SQL-family authoring layer
(`lowerDefaultForField`) with the `[]`-suffixed form for list columns —
matching the format introspection's `resolvedNativeType` already uses.

Covers every literal form `parsePostgresDefault` handles (array, null,
boolean, numeric, string, jsonb-parsed-object, out-of-range bigint) and
proves three genuine functions stay functions, including a dbgenerated
nextval(...) and an enum-cast default whose qualified type name defeats
the string-literal pattern.

No fixture moved: the only existing dbgenerated usages in the repo
(gen_random_uuid(), enum casts, an interval expression) all already
resolve to `kind: 'function'` under the new path, unchanged.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ate contract

D2-D7 fixed the eight infer/emit round-trip defects TML-3037 set out to fix.
This dispatch is the proof: delete the workarounds from generate-contract.ts
and confirm the regenerated contract carries the fix instead of the patch.

DOUBLE_PLURALIZED_FIELD_NAMES + applyDoublePluralizationFix: deleted.
pluralize() (D2) is now correct for already-plural table names, so the
override and the fix cancel out exactly - no back-relation name changed in
the regenerated contract.prisma.

INDEX_OMISSIONS + applyIndexOmissions + indexOmissionNameOf: deleted. The
postgres target now registers hash as a built-in index type (D5), so
auth.one_time_tokens' two USING hash indexes come back declared as
@@index(..., type: "hash") instead of silently dropped.

DEFAULT_OMISSIONS: reduced from 7 entries to 1. The jsonb/array
dbgenerated(...) literal-default disagreement (D7) is fixed generically -
authoring and introspection now resolve the same literal to the same
`kind: 'literal'` shape via parsePostgresDefault - so the six
custom_oauth_providers/webauthn_credentials/iceberg_namespaces entries are
gone and those columns' defaults are declared again. Only auth.users.phone
remains: `DEFAULT NULL` on a nullable column round-trips to an explicit
`@default(null)`, which the interpreter rejects (PSL_INVALID_DEFAULT_VALUE) -
a different, still-open defect this slice did not fix.

COLUMN_OMISSIONS is untouched: nullable text[] columns still have no PSL
form. CONTRACT-FIDELITY.md's audit summary is updated to match.

Regenerated packages/3-extensions/supabase/src/contract/contract.prisma via
`contract:generate`. The diff is exactly the two classes above (jsonb/array
defaults restored, two hash indexes declared) plus the resulting
contract.json/contract.d.ts changes - nothing else moved.
reference-fixture-verify.integration.test.ts passes against the regenerated,
workaround-free contract.

Refs: TML-3037
Closes: TML-3024
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Four consumer-visible breaking changes from this slice need an upgrade
entry, in both the user-skill and the extension-author-skill packages
(both substrates hit the same postgres target/interpreter machinery):

- pg/date@1: an existing @db.Date column's codecId changes on the next
  contract emit, changing storageHash, with no source change required.
- dbgenerated literal defaults (jsonb/array/null/bool/numeric/string) now
  resolve to kind: 'literal' on both the authoring and introspection side;
  an existing dbgenerated(...) literal default's resolved shape changes on
  the next emit, changing storageHash.
- Identity columns (GENERATED ... AS IDENTITY) now resolve to
  autoincrement() on introspection too; db verify --strict newly reports
  drift for an identity column whose contract predates this fix and
  declares no default. Non-strict verify is unaffected (undeclared
  auxiliary defaults are tolerated below --strict).
- pluralize no longer double-pluralizes an already-plural table name in
  contract infer output; only affects a future infer run, not an
  already-generated .prisma file, but a consumer who re-infers and gets a
  renamed back-relation field must update the application/pack code that
  references it.

The extension-author entry additionally explains the packages/3-extensions/supabase diff itself (the D8 workaround-deletion regeneration): it demonstrates the dbgenerated-literal-defaults fix directly, plus two additive/non-breaking changes (postgres index-type registration, the Decimal base-scalar fix) that need no entry.

pnpm check:upgrade-coverage passes with these entries committed.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…-blind

D7's dbgenerated fix added a nativeType field directly to the shared
framework DefaultFunctionLoweringContext type, which lint:framework-vocabulary
correctly rejected: nativeType is explicit family vocabulary the framework
domain must not carry (per no-family-vocabulary-in-framework.mdc, which names
nativeType as the paradigm example).

Replace the concrete field with an opaque fieldContext?: unknown, the same
pattern ControlMutationDefaultEntry.signature already uses one property
over: the SQL authoring layer (contract-psl's lowerDefaultForField) populates
it with a plain object, and the postgres adapter's lowerDbgenerated narrows
it back via blindCast against a locally-declared shape, since core cannot
name a postgres-specific type and the authoring layer has no reason to
export one. Guards the case where fieldContext is entirely absent so no
other lower handler that never reads it breaks.

lint:framework-vocabulary is back at the committed threshold (836).

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…d comments

Per no-transient-project-ids-in-code.mdc, source and tests must never carry
transient project-plan IDs — only durable TML-#### references. This slice's
own reproduction.md and plan.md track defects/dispatches by D1-D9 and each
integration test by its own IF.NN / RT.NN row; those IDs leaked into the
actual test suite:

- Dispatch IDs (D1, D4, D5, D6) in doc-comment headers across
  index-types.test.ts, identity-column-introspection.integration.test.ts,
  infer-roundtrip-fidelity.e2e.test.ts, and
  infer-roundtrip-runtime.integration.test.ts.
- IF.01-IF.08 / RT.01-RT.03 test-case IDs prefixing every it() description
  and expect() message in the two new infer-roundtrip integration suites.

Removed all of them, keeping the durable TML-3037 references and rewording
the two prose mid-sentence D4 references in
infer-roundtrip-runtime.integration.test.ts so the paragraph still reads
correctly without the dispatch letter. Test behavior is unchanged - only
names/comments moved; both files re-run green.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…efault rejection

D3's relaxation of the list-default check rejected only
executionDefaults.onCreate on a list field, permitting any storage-level
function default through. With D7 landed, the case that motivated the
relaxation - dbgenerated("'{}'::text[]") - already resolves to
kind: 'literal' before this check runs (parsePostgresDefault
recognizes the array-literal pattern), so the relaxation's only live
effect was a hole: a genuine function default on a list
(DateTime[] @default(now()), an array-returning dbgenerated(...)) now
emits a contract whose DDL Postgres rejects at apply time instead of
being caught at authoring time.

Restores the original check: isListField && (loweredOnCreate ||
loweredFunctionDefault). This also makes the narrow
PSL_LIST_AUTOINCREMENT_UNSUPPORTED guard D3 added afterward redundant -
it existed only to re-plug the hole for one function (autoincrement());
the restored broad check covers it along with every other function-kind
default, so the guard and its error code are deleted. Restores the
now()-on-a-list rejection test D3 deleted and replaces the two
D3-added "accepts" tests (dbgenerated array literal, now()) and the
narrow autoincrement rejection test with the original three
rejects-an-execution-default tests (now, uuid, autoincrement).

Verified empirically via the CLI journey (infer -> emit -> db verify):
String[] @default(dbgenerated("'{}'::text[]")) still emits and
round-trips clean, confirming D7's literal resolution is what makes
this case work, not the now-reverted relaxation.

A genuine array-returning function default
(dbgenerated("ARRAY[gen_random_uuid()]")) stays rejected - pre-existing
behaviour, not a regression, and a candidate for a future authoring
feature rather than something to fix here.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…n too

The 1:1 back-relation guard added by D3 skipped every singular
model-typed field carrying @relation, under a comment claiming "it
declares fields/references" - which it never actually checked. Infer
prints a named singular back relation (@relation(name: "..."), no
fields/references) whenever two FKs connect the same table pair, or a
unique FK self-references its own table - relation-inference.ts sets
relationName whenever needsRelationName is true, independent of
whether the candidate is a list. That shape still hit
PSL_INVALID_RELATION_ATTRIBUTE ("requires fields and references
arguments"), because both the backrelation-candidate collector and the
owning-side FK-building loop keyed off "has @relation", not off
"declares fields/references".

Adds relationAttributeDeclaresOwningSide(), which checks for the
fields/references named arguments the owning side actually declares,
and uses it in both places: the collector now only skips a genuine
owning-side field, and the FK-building loop now skips a back-relation
field (named or bare) instead of trying to validate fields/references
it never has.

The single-FK-per-pair fixture couldn't see this - it never produced
a relationName. Extends the round-trip fixture with `accounts` (two
FKs to `users`, one unique) and a self-referencing `categories.parent_id
unique`, and adds two isolated tests proving both shapes now emit,
alongside the existing tests proving the genuine owning side (fields +
references) is still treated as such.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…t function rewrites

D7 made lowerDbgenerated() reuse parsePostgresDefault() so a literal
default (e.g. '{}'::jsonb) resolves the same way on both the
authoring and introspection sides. But parsePostgresDefault() also
rewrites some function forms for introspection's benefit - notably
nextval(...) -> autoincrement() - and the authoring side was adopting
those rewrites wholesale. dbgenerated("nextval('my_seq')") silently
lowered to autoincrement(), whose DDL renders SERIAL - creating a
fresh table_col_seq sequence and discarding the user's named one.

Authoring now adopts the parser's result only when it resolves to
kind: 'literal'; a kind: 'function' resolution (or no resolution)
keeps the original raw expression text unchanged. This keeps the class
fix intact for the case it was meant for - both sides still agree on
literals - while authoring no longer rewrites function expressions it
was only ever meant to read.

Corrects the "dbgenerated keeps genuine functions as functions" test:
it previously asserted the nextval() rewrite as the *expected*
behaviour ("normalized to autoincrement(), not demoted to a literal"),
which is exactly the bug. It now asserts the expression is preserved
verbatim. Also drops the transient "F13" dispatch-finding label from
that describe block's name, missed by the commit that swept the
others (23cb5d7).

A contract authored with dbgenerated("nextval('my_seq')") will still
drift against a live column under `db verify`, since introspection
maps a live nextval default to autoincrement() regardless - that
mismatch pre-dates this slice and is not chased here.

Corrects the upgrade note in both 0.15-to-0.16/instructions.md copies
(user-facing and extension-author): "nextval(...) is unaffected" was
true of `kind` but not of the expression text before this fix: the
note now explains authoring only adopts literal resolutions, so a
named sequence is preserved rather than silently becoming
autoincrement().

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…), not just no-throw

The include() date-decode test only checked that nothing threw and
discarded the row, so a decode that produced a *wrong* date would
still pass. That assertion is the only coverage for the defect
pg/date@1 was built for - the top-level select() path never had the
bug (decode() there is a passthrough over an already-parsed Date), so
this is the one path that actually exercises decodeJson()'s
YYYY-MM-DD handling.

Captures the include() result and asserts it equals the exact expected
row, matching the top-level assertion's style immediately above it.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Review caught that the prescribed remedy was unsound. Relaxing the list
check to permit storage function defaults admits DateTime[] @default(now()),
whose DDL postgres refuses — trading an authoring-time error for a
DDL-apply-time one. Nothing in the authoring layer can tell whether a
function returns an array, so the relaxation cannot be made safe.

The tell was in the implementation: it needed a special case for
autoincrement() to plug one instance of that hole.

Once defect 8 resolves literal defaults through parsePostgresDefault,
dbgenerated("{}::text[]") is a literal and never reaches the list check.
The check stays as it was and the case works anyway.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric requested a review from a team as a code owner July 16, 2026 17:06
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR improves Postgres infer-to-emit fidelity across relations, defaults, identity columns, dates, numeric types, indexes, and dangling foreign keys. It adds singular backrelation handling, library-based pluralization, a dedicated pg/date@1 codec, normalized default resolution, identity metadata propagation, and generated warnings for out-of-scope foreign keys. Supabase contract artifacts and upgrade instructions are regenerated, with new unit, integration, runtime, and end-to-end round-trip coverage.

Changes

Postgres contract fidelity

Layer / File(s) Summary
Relation ownership and backrelation resolution
packages/2-sql/2-authoring/contract-psl/src/*, packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.test.ts
Relation interpretation distinguishes owning sides and singular backrelations, preserves 1:1 versus 1:N cardinality, and validates orphaned candidates.
Backrelation names and native type resolution
packages/2-sql/9-family/..., packages/2-sql/2-authoring/contract-psl/src/*, packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts
Backrelation naming delegates to pluralize, while full db.* attributes can provide target codec identifiers.
Postgres codecs and index types
packages/3-targets/3-targets/postgres/src/core/*, packages/3-targets/3-targets/postgres/src/exports/codecs.ts, packages/3-targets/3-targets/postgres/test/*
Adds the pg/date@1 codec, supports unbounded numeric parameters, registers built-in Postgres index methods, and adds codec and index tests.
Identity introspection and inference
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts, packages/3-targets/3-targets/postgres/src/core/psl-infer/*, packages/2-sql/1-core/schema-ir/test/*
Identity metadata becomes resolvedDefault: autoincrement(), identity defaults and list literals are emitted during PSL inference, and dangling foreign keys produce model warnings.
Default normalization and schema projection
packages/2-sql/9-family/src/core/migrations/*, packages/3-targets/3-targets/postgres/src/core/default-normalizer.ts, packages/3-targets/3-targets/postgres/src/core/migrations/*
Contract-to-schema conversion accepts target default resolvers, and Postgres normalizes function-shaped defaults for schema comparison and planning.
Supabase contract regeneration
packages/3-extensions/supabase/contract/*, packages/3-extensions/supabase/scripts/generate-contract.ts
Removes index and pluralization workarounds and regenerates column defaults, hash indexes, contract declarations, JSON, Prisma schema, and storage hash metadata.
Round-trip validation and upgrade guidance
test/integration/test/*, projects/infer-emit-roundtrip/spec.md, skills/*/upgrades/0.15-to-0.16/*, scripts/lint-framework-vocabulary.config.json
Adds infer→emit→verify CLI and runtime coverage and documents the new date, identity, pluralization, default, numeric, and index behaviors.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • prisma/prisma-next#870: Both PRs modify SQL contract/schema IR handling and migration-related contract propagation.
  • prisma/prisma-next#921: The identity-column SqlColumnIR.children() coverage is directly related to resolved-default modeling.

Suggested reviewers: wmadden

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.11% 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: making contract infer output round-trip through contract emit.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-3037-contract-infer-output-round-trips-through-contract-emit

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 Jul 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/config-loader

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

@prisma-next/emitter

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

@prisma-next/language-server

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 9608d00

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 159.35 KB (+0.26% 🔺)
postgres / emit 132.76 KB (+0.23% 🔺)
mongo / no-emit 98.71 KB (0%)
mongo / emit 89.43 KB (0%)
cf-worker / no-emit 185.63 KB (+0.28% 🔺)
cf-worker / emit 156.19 KB (+0.21% 🔺)

@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: 4

🤖 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/3-targets/3-targets/postgres/src/core/codec-helpers.ts`:
- Around line 146-155: Update the date-decoding logic around ISO_8601_DATE and
the Date.UTC construction to validate the resulting UTC year, month, and day
against the parsed yearText, monthText, and dayText before returning. Reject
normalized dates such as 2024-02-31 and invalid months, while preserving the
existing invalid-date error behavior; ensure years below 100 are compared
against the parsed year rather than Date.UTC’s 1900-based remapping, and add
regression coverage for the specified cases.

In `@packages/3-targets/3-targets/postgres/src/core/codecs.ts`:
- Around line 100-109: Update NumericParams and numericParamsSchema so the
numeric options are either empty or include required precision with optional
scale; preserve the existing precision and scale bounds while rejecting
configurations that specify scale without precision. Ensure
pgNumericRenderOutputType receives the validated precision/scale shape and no
longer silently ignores scale.

In
`@packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts`:
- Around line 156-159: Update both ifDefined spreads in
packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts at
lines 156-159 and 251-255 to pass an object containing nativeType only when
nativeType is defined, otherwise pass undefined; this must omit fieldContext
entirely when nativeType is missing and avoid the nested ifDefined/blindCast
behavior.

In `@test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`:
- Around line 140-149: Strengthen the assertions in
test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts:140-149
by verifying the bare identities Identities? back-relation immediately after
runContractInfer and before fixListDefault/fixIndexTypes; at 240-248 assert the
inferred tags list default before emit; at 267-275 assert both inferred gin and
hash index declarations; at 297-305 match diagnostics against the dropped-FK
identifiers rather than preceding comments; and at 346-359 assert the inferred
JSONB literal default before emit and verify it afterward.
🪄 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: fc99cc0d-e24f-4809-a861-5dbc00f10d46

📥 Commits

Reviewing files that changed from the base of the PR and between 98cf9c0 and f103431.

⛔ Files ignored due to path filters (4)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/infer-emit-roundtrip/plan.md is excluded by !projects/**
  • projects/infer-emit-roundtrip/reproduction.md is excluded by !projects/**
  • projects/infer-emit-roundtrip/spec.md is excluded by !projects/**
📒 Files selected for processing (44)
  • packages/1-framework/1-core/framework-components/src/shared/mutation-default-types.ts
  • packages/2-sql/1-core/schema-ir/src/ir/sql-column-ir.ts
  • packages/2-sql/1-core/schema-ir/test/sql-column-ir.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • 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/src/psl-relation-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.relations.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.types.test.ts
  • packages/2-sql/9-family/package.json
  • packages/2-sql/9-family/src/core/psl-contract-infer/name-transforms.ts
  • packages/2-sql/9-family/test/psl-contract-infer/name-transforms.test.ts
  • packages/3-extensions/supabase/scripts/generate-contract.ts
  • packages/3-extensions/supabase/src/contract/CONTRACT-FIDELITY.md
  • packages/3-extensions/supabase/src/contract/contract.d.ts
  • packages/3-extensions/supabase/src/contract/contract.json
  • packages/3-extensions/supabase/src/contract/contract.prisma
  • packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts
  • packages/3-targets/3-targets/postgres/src/core/codec-ids.ts
  • packages/3-targets/3-targets/postgres/src/core/codecs.ts
  • packages/3-targets/3-targets/postgres/src/core/default-normalizer.ts
  • packages/3-targets/3-targets/postgres/src/core/descriptor-meta.ts
  • packages/3-targets/3-targets/postgres/src/core/index-types.ts
  • packages/3-targets/3-targets/postgres/src/core/psl-infer/infer-psl-contract.ts
  • packages/3-targets/3-targets/postgres/src/exports/codecs.ts
  • packages/3-targets/3-targets/postgres/src/exports/default-normalizer.ts
  • packages/3-targets/3-targets/postgres/test/codecs-class.test.ts
  • packages/3-targets/3-targets/postgres/test/codecs-class.types.test-d.ts
  • packages/3-targets/3-targets/postgres/test/codecs-runtime-and-helpers.test.ts
  • packages/3-targets/3-targets/postgres/test/codecs.test.ts
  • packages/3-targets/3-targets/postgres/test/default-normalizer.test.ts
  • packages/3-targets/3-targets/postgres/test/index-types.test.ts
  • packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract-described-contracts.test.ts
  • packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.test.ts
  • packages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.enums.test.ts
  • packages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.relations.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts
  • packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/identity-column-introspection.integration.test.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.md
  • test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts
  • test/integration/test/infer-roundtrip-runtime.integration.test.ts

Comment on lines +146 to +155
const match = ISO_8601_DATE.exec(json);
if (!match) {
throw new Error(`Invalid date string for pg/date@1: ${json}`);
}
const [, yearText, monthText, dayText] = match;
const date = new Date(Date.UTC(Number(yearText), Number(monthText) - 1, Number(dayText)));
if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid date string for pg/date@1: ${json}`);
}
return date;

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject normalized calendar dates instead of silently changing them.

Date.UTC accepts values such as 2024-02-31 by normalizing them into March, and maps years 0099 into 1900–1999. Compare the resulting UTC components with the parsed values before returning.

Proposed fix
   const [, yearText, monthText, dayText] = match;
-  const date = new Date(Date.UTC(Number(yearText), Number(monthText) - 1, Number(dayText)));
-  if (Number.isNaN(date.getTime())) {
+  const year = Number(yearText);
+  const month = Number(monthText);
+  const day = Number(dayText);
+  const date = new Date(0);
+  date.setUTCHours(0, 0, 0, 0);
+  date.setUTCFullYear(year, month - 1, day);
+  if (
+    Number.isNaN(date.getTime()) ||
+    date.getUTCFullYear() !== year ||
+    date.getUTCMonth() !== month - 1 ||
+    date.getUTCDate() !== day
+  ) {
     throw new Error(`Invalid date string for pg/date@1: ${json}`);
   }

Add regression coverage for 2024-02-31, 2024-13-01, and a year below 100.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const match = ISO_8601_DATE.exec(json);
if (!match) {
throw new Error(`Invalid date string for pg/date@1: ${json}`);
}
const [, yearText, monthText, dayText] = match;
const date = new Date(Date.UTC(Number(yearText), Number(monthText) - 1, Number(dayText)));
if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid date string for pg/date@1: ${json}`);
}
return date;
const [, yearText, monthText, dayText] = match;
const year = Number(yearText);
const month = Number(monthText);
const day = Number(dayText);
const date = new Date(0);
date.setUTCHours(0, 0, 0, 0);
date.setUTCFullYear(year, month - 1, day);
if (
Number.isNaN(date.getTime()) ||
date.getUTCFullYear() !== year ||
date.getUTCMonth() !== month - 1 ||
date.getUTCDate() !== day
) {
throw new Error(`Invalid date string for pg/date@1: ${json}`);
}
return date;
🧰 Tools
🪛 OpenGrep (1.25.0)

[ERROR] 146-146: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 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/3-targets/3-targets/postgres/src/core/codec-helpers.ts` around lines
146 - 155, Update the date-decoding logic around ISO_8601_DATE and the Date.UTC
construction to validate the resulting UTC year, month, and day against the
parsed yearText, monthText, and dayText before returning. Reject normalized
dates such as 2024-02-31 and invalid months, while preserving the existing
invalid-date error behavior; ensure years below 100 are compared against the
parsed year rather than Date.UTC’s 1900-based remapping, and add regression
coverage for the specified cases.

Comment on lines +100 to 109
type NumericParams = { readonly precision?: number; readonly scale?: number };

const lengthParamsSchema = arktype({
'length?': 'number.integer > 0',
}) satisfies StandardSchemaV1<LengthParams>;

const numericParamsSchema = arktype({
precision: 'number.integer > 0 & number.integer <= 1000',
'precision?': 'number.integer > 0 & number.integer <= 1000',
'scale?': 'number.integer >= 0',
}) satisfies StandardSchemaV1<NumericParams>;

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/3-targets/3-targets/postgres/src/core/codecs.ts"

# Map the relevant symbols first.
ast-grep outline "$FILE" --view expanded || true

echo
echo "---- relevant lines ----"
sed -n '1,220p' "$FILE" | cat -n

echo
echo "---- search for renderer and type definitions ----"
rg -n "pgNumericRenderOutputType|NumericParams|lengthParamsSchema|numericParamsSchema|scale|precision" "$FILE" packages/3-targets/3-targets/postgres/src -g '!**/dist/**' -g '!**/build/**'

Repository: prisma/prisma-next

Length of output: 48591


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the renderer and any surrounding helpers to understand behavior.
FILE="packages/3-targets/3-targets/postgres/src/core/codecs.ts"
sed -n '220,420p' "$FILE" | cat -n

echo
echo "---- nearby references in package ----"
rg -n "pgNumericRenderOutputType|NumericParams|precision.*scale|scale.*precision" packages/3-targets/3-targets/postgres/src -g '!**/dist/**' -g '!**/build/**'

Repository: prisma/prisma-next

Length of output: 11705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/3-targets/3-targets/postgres/src/core/codecs.ts"

ast-grep outline "$FILE" --view expanded || true

echo
echo "---- relevant lines ----"
sed -n '1,220p' "$FILE" | cat -n

echo
echo "---- search for renderer and type definitions ----"
rg -n "pgNumericRenderOutputType|NumericParams|lengthParamsSchema|numericParamsSchema|scale|precision" "$FILE" packages/3-targets/3-targets/postgres/src -g '!**/dist/**' -g '!**/build/**'

Repository: prisma/prisma-next

Length of output: 48591


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/3-targets/3-targets/postgres/src/core/codecs.ts"
sed -n '220,420p' "$FILE" | cat -n

echo
echo "---- nearby references in package ----"
rg -n "pgNumericRenderOutputType|NumericParams|precision.*scale|scale.*precision" packages/3-targets/3-targets/postgres/src -g '!**/dist/**' -g '!**/build/**'

Repository: prisma/prisma-next

Length of output: 11705


Require precision when scale is set.

numericParamsSchema accepts { scale: 2 }, but pgNumericRenderOutputType returns undefined whenever precision is missing, so scale is silently ignored. Model this as either empty or { precision, scale? } and enforce the same constraint in the schema.

🤖 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/3-targets/3-targets/postgres/src/core/codecs.ts` around lines 100 -
109, Update NumericParams and numericParamsSchema so the numeric options are
either empty or include required precision with optional scale; preserve the
existing precision and scale bounds while rejecting configurations that specify
scale without precision. Ensure pgNumericRenderOutputType receives the validated
precision/scale shape and no longer silently ignores scale.

Comment thread packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts Outdated

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

Rife with architectural violations

* postgres control adapter stamps from this flag rather than from a raw
* expression when it's set.
*/
readonly identity?: boolean;

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.

Doesn't belong in sql core if it's a postgres thing. Why didn't this trip the forbidden words lint?

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.

Deleted in a2b9fb7. The field was pure duplication: the adapter already stamps resolvedDefault: autoincrement() at IR construction, and both consumers (the normalizer option, infer's flag check) existed only to re-derive that. All three are gone; postgres identity knowledge now lives at exactly one adapter-owned site.

Why the lint never tripped: the vocabulary lint's only scope was packages/1-framework — nothing linted 2-sql at all. Fixed alongside your other comment: a packages/2-sql scope now exists.

nativeType: 'timestamptz',
},
'db.Date': { args: 'noArgs', baseType: 'DateTime', codecId: null, nativeType: 'date' },
'db.Date': { args: 'noArgs', baseType: 'DateTime', codecId: 'pg/date@1', nativeType: 'date' },

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.

No postgres codecs in sql core. Add pg/* to the forbidden dict for the 2-sql dir

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.

Removed in 82ffdd2db.Date reverted to codecId: null; the id now arrives via the target-contributed scalarTypeDescriptors map (existing seam, keyed by the attribute name), so pg/date@1 is named only in target-postgres.

Lint scope added as requested: packages/2-sql forbidding pg/, threshold committed at 61 — the exact origin/main count, so this branch adds zero and the count can only shrink. The ~61 grandfathered sites (the rest of the db.* table + lanes) stay with the remove-db-attributes project (TML-2986/2987).

Comment on lines +993 to +1006
// `GENERATED ALWAYS AS IDENTITY` ('a') and `GENERATED BY DEFAULT AS
// IDENTITY` ('d') both report a NULL column_default — Postgres tracks
// generation via attidentity, not a default expression — so neither
// variant is visible in `rawDefault` at all. The contract has no
// syntax to distinguish the two, so both resolve to the same
// `autoincrement()` a `serial` column's `nextval(...)` default
// already maps to.
const identity =
colRow.attidentity === 'a' || colRow.attidentity === 'd' ? true : undefined;
const resolvedDefault = identity
? parsePostgresDefault(rawDefault ?? '', resolvedNativeType, { identity: true })
: rawDefault !== undefined
? parsePostgresDefault(rawDefault, resolvedNativeType)
: undefined;

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.

I don't love that this nonsense is leaking all over the place. Should we be compensating for this behavior once, in the schemaIR instead of in every consuming site?

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.

Yes — and your suggestion turned out to be the fix for the sibling comment too. Done in a2b9fb7: the adapter stamps resolvedDefault once at SchemaIR construction; the normalizer's {identity} option and infer's flag check are deleted, and infer reads the stamped answer instead.

The same compensate-once principle then drove the fieldContext fix (2803794) and the list-defaults fix (3ae4bb6): normalize at IR construction, consumers read the normalized value.

Comment on lines +19 to +30
/**
* The shape the SQL authoring layer populates
* `DefaultFunctionLoweringContext.fieldContext` with (an opaque `unknown`
* at the framework boundary — see that field's doc comment). Declared here,
* not imported, because core cannot name it and the authoring layer has no
* reason to export a postgres-specific type; the two sides agree on the
* shape by convention, the same way `FuncCallSig` values below are agreed
* independently of the framework's `unknown`-typed `signature` field.
*/
interface PostgresFieldContext {
readonly nativeType?: string;
}

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.

This looks like we're shoving codec data into a field. Not ok

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.

Agreed, deleted in 2803794. lowerDbgenerated is byte-for-byte back to pre-branch; the drift fix moved to expected-side SchemaIR construction — contractToSchemaIR gained a resolveDefault hook (same IoC pattern as expandNativeType/renderDefault), postgres wires parsePostgresDefault into it, so both sides normalize identically at compare time. No opaque channel, no framework surface; contract.json stores the authored expression verbatim again, and the now-moot storageHash upgrade entry is deleted.

One knock-on worth your eye: reverting the lowering reopened the list-defaults defect (the interpreter rightly rejects dbgenerated on lists). Real fix landed in 3ae4bb6 — infer now prints the literal syntax PSL already has (@default([])) from the stamped resolvedDefault, instead of raw text emit rejects.

… at introspection

Review comments 1 and 3 on PR #998: `identity` on SqlColumnIR was postgres
vocabulary in sql core, and every consumer (default-normalizer option,
infer's identity check) existed only to re-derive what the postgres control
adapter already knows from `attidentity` and already stamps into
`resolvedDefault`.

The adapter now stamps `resolvedDefault: { kind: function, expression:
autoincrement() }` directly for an identity column, with no intermediate
option or field. `parsePostgresDefault` loses its `identity` option. Infer
recognizes the same shape by its only distinguishing trait: a resolvedDefault
of autoincrement() with no raw default. `SqlColumnIR.identity` is deleted
entirely; the postgres adapter is the only place identity is recognized.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ce at SchemaIR construction

Review comment 4 on PR #998: DefaultFunctionLoweringContext.fieldContext
was an opaque unknown channel smuggling postgres nativeType from SQL
authoring into the adapter, held together by a blindCast and a comment.
That field is deleted, and lowerDbgenerated reverts to keeping a
dbgenerated(...) default verbatim as { kind: function, expression }, with
no parsePostgresDefault call.

The jsonb/text[] literal-default drift this was compensating for
(resolvedDefaultsEqual comparing kind before content, so a dbgenerated
literal never matched its introspected counterpart) is fixed once
instead, at SchemaIR construction of the EXPECTED (contract-derived)
side: contractToSchemaIR gains a target-supplied resolveDefault hook,
mirroring the existing expandNativeType/renderDefault IoC pattern, and
the postgres target wires postgresResolveDefault — reusing the same
parsePostgresDefault introspection already calls — into both db verify
and migration planning's expected-tree derivation.

Consequence carried through: a dbgenerated(...) default on a *list*
field still fails PSL_LIST_EXECUTION_DEFAULT_UNSUPPORTED at emit time,
because the interpreter's check has no authoring-time signal left to
distinguish a literal-shaped dbgenerated call from a genuine function —
that signal only lived in the now-deleted fieldContext-driven
resolution. auth.custom_oauth_providers.{acceptable_client_ids,scopes}
(text[] literal defaults) go back into DEFAULT_OMISSIONS with the
reason recorded; the D1 journey's matching list-default assertions are
left unmodified, and reproduce this as a known regression relative to
the branch this round reviewed, not a design this round endorses.

The pack's jsonb dbgenerated defaults (attribute_mapping,
authorization_params, transports, metadata) round-trip clean under the
new compare-time fix, proven by
extension-supabase/test/reference-fixture-verify.integration.test.ts.
Upgrade instructions for the now-reverted authoring-side resolution are
removed from both 0.15-to-0.16 instructions.md files — the fix is
invisible to contract.json/storageHash, so no downstream action is
needed.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ql literal

Review comment 2 on PR #998: this branch added codecId: pg/date@1 directly
to db.Date's NATIVE_TYPE_SPECS row in psl-column-resolution.ts (2-sql), a
postgres codec id literal in family code.

db.Date goes back to codecId: null (matching origin/main). The noArgs case
in resolveDbNativeTypeAttribute now falls back through the already-threaded
scalarTypeDescriptors map (target-contributed, keyed everywhere else by PSL
base type name) by the full attribute name ("db.Date") before falling back
to the base type's own descriptor -- reusing the existing IoC seam instead
of inventing one, since baseDescriptor alone would resolve to DateTime's
pg/timestamptz@1, the original defect. The postgres adapter contributes
the db.Date -> pg/date@1 mapping under that same key in
postgresScalarTypeDescriptors (control-mutation-defaults.ts), so the
concrete codec id lives in target-owned code.

Extends lint:framework-vocabulary with a second scope: packages/2-sql,
forbidden ["pg/"], threshold 61 -- the measured count after removing this
branch's one new literal, unchanged from origin/main (every other pg/
occurrence in the scope is pre-existing debt, grandfathered per
TML-2986/2987, not touched here). The ~30-literal db.* attribute table
itself is not relocated; that is the existing remove-db-attributes
project's scope.

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…dDefault

Two designs were tried and abandoned for defect 7 (array columns cant
keep their default): relaxing the interpreters list-default check
admitted DateTime[] @default(now()), which Postgres DDL refuses; riding
defect 8s literal resolution at lowering time required smuggling
postgres nativeType through a framework type and was rejected in
review. Defect 8 landed at compare time instead, so it never rescues
emit for a list column.

The actual defect was narrower: PSL already has literal list syntax
(@default([...])) and the interpreter already lowers it to
kind: literal without touching the list check. Infer just never
printed it — any raw default it could not otherwise map fell back to
dbgenerated(<raw text>), which the interpreter always rejects on a
list column. The postgres control adapter already resolves a list
columns raw default to a structured literal at introspection time
(resolvedDefault, the same mechanism defect 3 uses for identity
columns), so infer now prints PSL literal-list syntax from that
resolved value instead of the raw-text fallback.

Deletes the acceptable_client_ids/scopes entries from
DEFAULT_OMISSIONS and regenerates the Supabase pack; both columns now
carry @default([]).

Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>

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

♻️ Duplicate comments (1)
test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts (1)

138-141: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert each inferred feature before exercising downstream behavior.

These tests can pass when inference omits the construct they are intended to protect. Please verify the expected feature in the PSL string before removing unrelated blockers.

  • test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts#L138-L141: assert the bare identities Identities? back-relation before calling fixIndexTypes.
  • test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts#L279-L281: assert both inferred gin and hash index declarations before calling fixOneToOneBackRelation.
  • test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts#L358-L366: assert the inferred JSONB literal default before calling fixOneToOneBackRelation.
🤖 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 `@test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`
around lines 138 - 141, In
test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts, assert
each inferred feature before applying the blocker-fixing helpers: at lines
138-141 verify the bare “identities Identities?” back-relation before
fixIndexTypes; at lines 279-281 verify both inferred gin and hash index
declarations before fixOneToOneBackRelation; and at lines 358-366 verify the
inferred JSONB literal default before fixOneToOneBackRelation. Keep the existing
downstream behavior checks intact.
🤖 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.

Duplicate comments:
In `@test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`:
- Around line 138-141: In
test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts, assert
each inferred feature before applying the blocker-fixing helpers: at lines
138-141 verify the bare “identities Identities?” back-relation before
fixIndexTypes; at lines 279-281 verify both inferred gin and hash index
declarations before fixOneToOneBackRelation; and at lines 358-366 verify the
inferred JSONB literal default before fixOneToOneBackRelation. Keep the existing
downstream behavior checks intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1f26335-99c6-483a-bdc9-156dd67136ea

📥 Commits

Reviewing files that changed from the base of the PR and between f103431 and 3ae4bb6.

📒 Files selected for processing (29)
  • packages/2-sql/1-core/schema-ir/test/sql-column-ir.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-named-type-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
  • packages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.ts
  • packages/2-sql/9-family/src/exports/control.ts
  • packages/2-sql/9-family/test/contract-to-schema-ir.test.ts
  • packages/3-extensions/supabase/scripts/generate-contract.ts
  • packages/3-extensions/supabase/src/contract/CONTRACT-FIDELITY.md
  • packages/3-extensions/supabase/src/contract/contract.d.ts
  • packages/3-extensions/supabase/src/contract/contract.json
  • packages/3-extensions/supabase/src/contract/contract.prisma
  • packages/3-targets/3-targets/postgres/src/core/default-normalizer.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/diff-database-schema.ts
  • packages/3-targets/3-targets/postgres/src/core/psl-infer/infer-psl-contract.ts
  • packages/3-targets/3-targets/postgres/src/exports/control.ts
  • packages/3-targets/3-targets/postgres/src/exports/default-normalizer.ts
  • packages/3-targets/3-targets/postgres/test/default-normalizer.test.ts
  • packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts
  • packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/identity-column-introspection.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/planner-ddl-builders.test.ts
  • projects/infer-emit-roundtrip/spec.md
  • scripts/lint-framework-vocabulary.config.json
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.md
  • test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts
💤 Files with no reviewable changes (1)
  • skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.md

…er-output-round-trips-through-contract-emit

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>

# Conflicts:
#	skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md
#	skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.md

@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

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md (1)

75-81: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the stale DEFAULT_OMISSIONS documentation.

This block says the table shrinks from seven entries to three and that acceptable_client_ids/scopes remain omitted. The current packages/3-extensions/supabase/scripts/generate-contract.ts change reduces it to a single auth.users.phone omission, so this upgrade note no longer describes the generated behavior.

🤖 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
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md`
around lines 75 - 81, Update the DEFAULT_OMISSIONS documentation in the upgrade
instructions to state that the table is reduced to only the auth.users.phone
omission. Remove the outdated references to the default-null round-trip issue
and auth.custom_oauth_providers acceptable_client_ids/scopes, while preserving
the surrounding generate-contract.ts workaround context.

Source: Coding guidelines

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

Outside diff comments:
In
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md`:
- Around line 75-81: Update the DEFAULT_OMISSIONS documentation in the upgrade
instructions to state that the table is reduced to only the auth.users.phone
omission. Remove the outdated references to the default-null round-trip issue
and auth.custom_oauth_providers acceptable_client_ids/scopes, while preserving
the surrounding generate-contract.ts workaround context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: bfb4fc20-27ea-4951-a736-684b3223bfca

📥 Commits

Reviewing files that changed from the base of the PR and between 3ae4bb6 and 9608d00.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • packages/3-extensions/supabase/scripts/generate-contract.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts

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