TML-3037: contract infer output round-trips through contract emit - #1011
Conversation
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>
… 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>
…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
…db-attributes The remove-db-attributes project (spec on tml-2985-unify-type-channel; slice 2 = PR #975) replaces the whole @db.* channel with target-contributed bare scalar types, and its Date type pins its codec explicitly. Binding date columns to pg/date@1 through the @db.* machinery — first as a pg/ literal in 2-sql, then as a double-keyed scalarTypeDescriptors entry — extends exactly the mechanism that project deletes, so the binding moves there. This commit reverts the double-keyed map (82ffdd2) and returns db.Date to codecId: null (inherit DateTime's pg/timestamptz@1), matching main. The pg/date@1 codec itself stays registered on the postgres target, unbound; the remove-db-attributes Date pin is one line once it lands. Consequences carried in the same commit: the runtime instrument's date scenario pins the still-broken include() decode (RUNTIME.DECODE_FAILED) instead of asserting the fixed value, so the binding change flips a red assertion rather than silently landing; the 0.15-to-0.16 upgrade entry for the codecId change is withdrawn (the contract no longer changes in this slice); the slice spec records the deferral. Refs: TML-3037 Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThis PR reworks PSL 1:1 backrelation resolution and diagnostics, switches back-relation pluralization to the Estimated code review effort: 4 (Complex) | ~75 minutes ChangesRelation inference and naming
Estimated code review effort: 4 (Complex) | ~75 minutes Default normalization and identity inference
Estimated code review effort: 4 (Complex) | ~75 minutes Postgres codecs and index metadata
Estimated code review effort: 3 (Moderate) | ~30 minutes Supabase contract regeneration
Estimated code review effort: 2 (Simple) | ~15 minutes Round-trip validation and upgrade documentation
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CLI as contract infer/emit CLI
participant Introspect as control-adapter (introspection)
participant Infer as infer-psl-contract
participant Interpreter as contract-psl interpreter
participant Runtime as ExecutionContext
CLI->>Introspect: introspect Postgres schema
Introspect->>Introspect: read attidentity, column_default
Introspect-->>CLI: columns with resolvedDefault (autoincrement/date/etc)
CLI->>Infer: buildPslDocumentAst(columns, danglingForeignKeysByTable)
Infer->>Infer: buildScalarField (default/list-literal), buildDanglingForeignKeyWarning
Infer-->>CLI: contract.prisma
CLI->>Interpreter: interpretPslDocumentToSqlContract
Interpreter->>Interpreter: resolve owning-side/back-side relations, backrelation candidates
Interpreter-->>CLI: SQL contract with relations/defaults/indexes
CLI->>Runtime: createExecutionContext(contract)
Runtime->>Runtime: decode date/numeric columns via pg/date, pg/numeric codecs
Runtime-->>CLI: query results
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts (1)
166-196: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer separate assertions over
toMatchObjectfor multiple fields.Based on learnings, in test files, prefer separate
expect()assertions for each field instead of combining checks withtoMatchObject()when validating multiple fields. This yields clearer, more actionable failure messages that pinpoint exactly which field failed (e.g., ifokis false, the error is immediately obvious without digging through a deep object diff).♻️ Proposed refactor
it('keeps a jsonb literal expression as a function, unresolved', () => { const expression = "'{}'::jsonb"; - expect(lower(expression)).toMatchObject({ - ok: true, - value: { kind: 'storage', defaultValue: { kind: 'function', expression } }, - }); + const result = lower(expression); + expect(result.ok).toBe(true); + expect((result as any).value).toEqual({ + kind: 'storage', + defaultValue: { kind: 'function', expression }, + }); }); it('keeps a text[] literal expression as a function, unresolved', () => { const expression = "'{}'::text[]"; - expect(lower(expression)).toMatchObject({ - ok: true, - value: { kind: 'storage', defaultValue: { kind: 'function', expression } }, - }); + const result = lower(expression); + expect(result.ok).toBe(true); + expect((result as any).value).toEqual({ + kind: 'storage', + defaultValue: { kind: 'function', expression }, + }); }); it('keeps gen_random_uuid() a function', () => { const expression = 'gen_random_uuid()'; - expect(lower(expression)).toMatchObject({ - ok: true, - value: { kind: 'storage', defaultValue: { kind: 'function', expression } }, - }); + const result = lower(expression); + expect(result.ok).toBe(true); + expect((result as any).value).toEqual({ + kind: 'storage', + defaultValue: { kind: 'function', expression }, + }); }); it("keeps nextval(...) a function, unchanged (doesn't adopt the normalizer's autoincrement() rewrite)", () => { const expression = "nextval('seq'::regclass)"; - expect(lower(expression)).toMatchObject({ - ok: true, - value: { kind: 'storage', defaultValue: { kind: 'function', expression } }, - }); + const result = lower(expression); + expect(result.ok).toBe(true); + expect((result as any).value).toEqual({ + kind: 'storage', + defaultValue: { kind: 'function', expression }, + }); });(Note: If
resultis a discriminated union like aResulttype,if (!result.ok) throw new Error(...)can be used instead of castingas anyif you prefer stricter typing, or simply rely onresult.valueif the compiler understands it.)🤖 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/6-adapters/postgres/test/control-mutation-defaults.test.ts` around lines 166 - 196, Refactor the four tests around lower so each expected field is asserted separately instead of using toMatchObject. After validating ok, assert the storage kind, defaultValue kind, and expression independently while preserving the existing expected values and test coverage.Source: Learnings
🤖 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-extensions/supabase/src/contract/CONTRACT-FIDELITY.md`:
- Around line 22-23: The “Column defaults (3)” audit entry in
CONTRACT-FIDELITY.md is stale: only auth.users.phone remains omitted. Update the
count to one and remove the acceptable_client_ids/scopes omission claim,
optionally incorporating them into the explanation that these defaults are now
declared as literal list defaults.
In `@packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts`:
- Around line 137-138: Replace Date.UTC construction in pgDateDecode with a
zero-initialized Date followed by setUTCFullYear using the wire year, month, and
date. Apply the same pattern in pgDateDecodeJson at
packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts lines 150-151,
preserving UTC-midnight behavior and correct years 0-99.
---
Nitpick comments:
In
`@packages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.ts`:
- Around line 166-196: Refactor the four tests around lower so each expected
field is asserted separately instead of using toMatchObject. After validating
ok, assert the storage kind, defaultValue kind, and expression independently
while preserving the existing expected values and test coverage.
🪄 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: 78942b14-00d8-48c2-b0c1-545c04d83bd2
⛔ Files ignored due to path filters (4)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlprojects/infer-emit-roundtrip/plan.mdis excluded by!projects/**projects/infer-emit-roundtrip/reproduction.mdis excluded by!projects/**projects/infer-emit-roundtrip/spec.mdis excluded by!projects/**
📒 Files selected for processing (45)
packages/2-sql/1-core/schema-ir/test/sql-column-ir.test.tspackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.tspackages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.relations.test.tspackages/2-sql/9-family/package.jsonpackages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.tspackages/2-sql/9-family/src/core/psl-contract-infer/name-transforms.tspackages/2-sql/9-family/src/exports/control.tspackages/2-sql/9-family/test/contract-to-schema-ir.test.tspackages/2-sql/9-family/test/psl-contract-infer/name-transforms.test.tspackages/3-extensions/supabase/scripts/generate-contract.tspackages/3-extensions/supabase/src/contract/CONTRACT-FIDELITY.mdpackages/3-extensions/supabase/src/contract/contract.d.tspackages/3-extensions/supabase/src/contract/contract.jsonpackages/3-extensions/supabase/src/contract/contract.prismapackages/3-targets/3-targets/postgres/src/core/codec-helpers.tspackages/3-targets/3-targets/postgres/src/core/codec-ids.tspackages/3-targets/3-targets/postgres/src/core/codecs.tspackages/3-targets/3-targets/postgres/src/core/default-normalizer.tspackages/3-targets/3-targets/postgres/src/core/descriptor-meta.tspackages/3-targets/3-targets/postgres/src/core/index-types.tspackages/3-targets/3-targets/postgres/src/core/migrations/diff-database-schema.tspackages/3-targets/3-targets/postgres/src/core/psl-infer/infer-psl-contract.tspackages/3-targets/3-targets/postgres/src/exports/codecs.tspackages/3-targets/3-targets/postgres/src/exports/control.tspackages/3-targets/3-targets/postgres/src/exports/default-normalizer.tspackages/3-targets/3-targets/postgres/test/codecs-class.test.tspackages/3-targets/3-targets/postgres/test/codecs-class.types.test-d.tspackages/3-targets/3-targets/postgres/test/codecs-runtime-and-helpers.test.tspackages/3-targets/3-targets/postgres/test/codecs.test.tspackages/3-targets/3-targets/postgres/test/default-normalizer.test.tspackages/3-targets/3-targets/postgres/test/index-types.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract-described-contracts.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.enums.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.relations.test.tspackages/3-targets/6-adapters/postgres/src/core/control-adapter.tspackages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.tspackages/3-targets/6-adapters/postgres/test/migrations/identity-column-introspection.integration.test.tspackages/3-targets/6-adapters/postgres/test/migrations/planner-ddl-builders.test.tsskills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.mdskills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.mdtest/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.tstest/integration/test/infer-roundtrip-runtime.integration.test.ts
| **Column defaults (3):** `auth.users.phone` (`DEFAULT NULL` is a no-op, but round-trips through the raw-default parser as an explicit `@default(null)`, which the interpreter rejects); `auth.custom_oauth_providers.acceptable_client_ids` and `.scopes` (both `text[]` with `DEFAULT '{}'::text[]`, printed as `@default(dbgenerated("'{}'::text[]"))` — the interpreter rejects any function-kind default on a list field, and a `dbgenerated(...)` default is always function-kind at authoring time). Column type is declared in full for all three; only the `@default` is dropped. The jsonb `dbgenerated(...)` defaults that used to widen this list (TML-3037) are declared again — `db verify`'s permanent-drift disagreement is fixed generically, at the postgres target's `SchemaIR` construction, so it needs no authoring-side omission. | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Stale "Column defaults (3)" entry — acceptable_client_ids/scopes are no longer omitted.
The doc states Machine-readable versions of these lists live in scripts/generate-contract.ts (COLUMN_OMISSIONS / DEFAULT_OMISSIONS), each with the full reasoning; this is the audit summary. and claims three column defaults are omitted, specifically citing auth.custom_oauth_providers.acceptable_client_ids and .scopes as rejected because they're function-kind dbgenerated(...) defaults on list fields.
However, generate-contract.ts's DEFAULT_OMISSIONS now contains only auth.users: ['phone'] — no entry for custom_oauth_providers — and the regenerated contract.d.ts/contract.prisma/contract.json show acceptable_client_ids and scopes now carry an explicit literal [] default (not function-kind/dbgenerated). The list-literal-default inference fix apparently resolved this omission, but this section wasn't updated to reflect it.
Update the count to reflect only auth.users.phone remaining omitted, and either drop the acceptable_client_ids/.scopes bullet or fold it into the "declared again" explanation used for the jsonb defaults later in the same paragraph.
As per coding guidelines, **/*.{md,mdc}: "Keep docs current (READMEs, rules, links)."
🤖 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-extensions/supabase/src/contract/CONTRACT-FIDELITY.md` around
lines 22 - 23, The “Column defaults (3)” audit entry in CONTRACT-FIDELITY.md is
stale: only auth.users.phone remains omitted. Update the count to one and remove
the acceptable_client_ids/scopes omission claim, optionally incorporating them
into the explanation that these defaults are now declared as literal list
defaults.
Source: Coding guidelines
| export const pgDateDecode = (wire: Date): Date => | ||
| new Date(Date.UTC(wire.getFullYear(), wire.getMonth(), wire.getDate())); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Date.UTC corrupts years 0-99. Both decoding sites use Date.UTC, which maps year arguments between 0 and 99 to 1900 through 1999, silently corrupting historical dates from the first century AD.
packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts#L137-L138: instantiatenew Date(0)and callsetUTCFullYear(wire.getFullYear(), wire.getMonth(), wire.getDate())to safely construct the UTC-midnight instant inpgDateDecode.packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts#L150-L151: apply the samesetUTCFullYearpattern to safely construct the parsed JSON date inpgDateDecodeJson.
📍 Affects 1 file
packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts#L137-L138(this comment)packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts#L150-L151
🤖 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
137 - 138, Replace Date.UTC construction in pgDateDecode with a zero-initialized
Date followed by setUTCFullYear using the wire year, month, and date. Apply the
same pattern in pgDateDecodeJson at
packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts lines 150-151,
preserving UTC-midnight behavior and correct years 0-99.
Date.UTC(y, m, d) normalizes out-of-range components (e.g. "2024-02-31" becomes March 2, and two-digit years like "0024" map into 1900+y) rather than producing NaN, so the previous Number.isNaN(getTime()) check was unreachable for these inputs. decodeJson now compares the constructed date's UTC year/month/day back against the parsed components and throws if they diverge. Found in PR #1011 review (F01). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…or singular fields too PSL_ORPHANED_BACKRELATION_LIST and PSL_AMBIGUOUS_BACKRELATION_LIST kept their "_LIST" suffix after the messages were de-listed to also cover singular back-relations, so the codes no longer described what they fire on. Renamed to PSL_ORPHANED_BACKRELATION and PSL_AMBIGUOUS_BACKRELATION everywhere: source, tests, integration diagnostic fixtures, and project docs. No alias kept. PR #1011 review (M1). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…y IR bullet in spec The pg/date@1 codec descriptor had no note explaining why it sits in codecDescriptors unbound to any PSL type; a one-line comment states the binding is deferred to remove-db-attributes' Date type and must be pinned via codecId, not a second targetTypes activation. The spec's Contract-impact section and the identity-columns writeup both still claimed SqlColumnIR gains an identity field. Commit a2b9fb7 deleted that plan: identity is recognized once, at introspection, by the postgres control adapter stamping resolvedDefault directly, with no new IR surface. Rewrote both bullets to match. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Review-rework round for the local review in
Full gate set re-run green on the new head: packages 13185, integration 1173, e2e 109, plus lints/fixtures/upgrade-coverage. |
Comments across the branch narrated dead system states ("used to throw",
"the shape emit rejected before this fix", "no longer resolves") and the
work that removed them. A comment survives the change it rode in on; the
next reader has no "before". Rewritten in present tense to state the
invariant or constraint each site actually relies on, and compressed where
the narration was the bulk. The runtime instrument header shrinks from 28
lines to 10.
Refs: TML-3037
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Refs: TML-3037 Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
| * target-agnostic. Omitted entirely, the contract's raw default is the | ||
| * resolved default unchanged (today's behavior). | ||
| */ | ||
| export type DefaultResolver = (def: ColumnDefault, resolvedNativeType: string) => ColumnDefault; |
| pgFloat4Descriptor, | ||
| pgFloat8Descriptor, | ||
| pgNumericDescriptor, | ||
| // PSL `@db.Date` binding is deferred to remove-db-attributes' `Date` type; pin it via codecId, not a second targetTypes activation here. |
There was a problem hiding this comment.
No TODOs without a Linear ticket ID
…d codes Refs: TML-3037 Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…oundtrip-recut Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # pnpm-lock.yaml
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
Linked issue
Refs TML-3037 · Closes TML-3024 · Supersedes #998 (closed unmerged — see Alternatives considered for why).
At a glance
Here is a fragment of
packages/3-extensions/supabase/scripts/generate-contract.ts, onmain, before this PR:We ship a script that repairs
contract infer's output before our owncontract emitwill accept it. Alongside that list are tables of column, default, 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.Decision
db verify --schema-onlyagainst a live database, plus a runtime instrument that builds a realExecutionContextfrom the emitted contract. Every defect below was reproduced by it before its fix.Decimal-on-postgres, identity columns, index types, list literal defaults,dbgenerateddefault drift), and dropped dangling FKs are now explained in infer's output instead of vanishing silently.datecolumns — is fixed halfway, deliberately. The missingpg/date@1codec lands (with strict parsing:2024-02-31is rejected, not normalized into March), but nothing binds the@db.Datespelling to it. That binding belongs to remove-db-attributes, a parallel in-flight project that replaces PSL's@db.*attributes with first-class scalar types (slice 2 of it is open as TML-2986: postgres native types as bare PSL scalar types; Json→pg/json, new Jsonb #975); its future bareDatetype is where the pin goes. Wiring the binding through@db.*machinery here would extend exactly what that project deletes. The runtime instrument pins the still-broken.include()decode so the future binding flips a red assertion instead of landing silently.Why the work exists
A user sent in a 260-line
fix-inferred-contract.tsthey run after everycontract infer. Auditing it claim-by-claim against the source turned up the defects. They had written our pack's repair script independently, against a different database, without ever seeing ours. Two people arriving at the same workaround is the finding:contract inferwrites PSL thatcontract emitrejects, or thatdb verifyreports as drift — and no test anywhere did introspect → infer → emit, which is why all of it shipped. So the first commit is the instrument, not a fix: a round-trip journey carrying already-plural table names, 1:1 / 1:N / self-referencing FKs, both identity variants,serial, bounded and unboundednumeric,date,text[],jsonb, GIN and hash indexes, and an FK pointing out of scope.What was broken
Back-relation names double-pluralized.
pluralize()appendedesto anything ending ins, so already-plural table names — most real schemas — came outsessionses. Now uses the maintainedpluralizelibrary. 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 onlyif (field.list). We had a snapshot test asserting we print PSL our own emitter rejects.Decimalnever worked on Postgres. Not an infer bug:Decimalmaps topg/numeric@1on the base-scalar path with notypeParams, so any schema with a plainamount Decimalfield threwRUNTIME.CODEC_PARAMETERIZATION_MISMATCHat connect.NumericParams.precisionwas 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 aDecimal.datecolumns break through.include().@db.DateinheritsDateTime'spg/timestamptz@1, whosedecodeJsonrejects the bareYYYY-MM-DDthatjson_aggrenders. This PR ships thepg/date@1codec but leaves it unbound; see Decision 3.Non-btree indexes could never emit. Infer prints
@@index(…, type: "gin"); the Postgres target registered zero index types, so emit threwunregistered index type "gin". The target now registers its built-ins (btree,hash,gin,gist,spgist,brin) via the sameIndexTypeRegistrymechanism ParadeDB uses forbm25; an unregistered type is still rejected.Identity columns lost their default. The columns query joined
pg_attributebut never selectedattidentity;serialworked only because it sets a realnextval(...)default. Identity maps ontoautoincrement()symmetrically — infer emits it and the verify normalizer resolves a live identity column to it, so neither side drifts. PSL doesn't modelGENERATED ALWAYSvsBY DEFAULT; a freshdb initfrom such a contract createsserial(pre-existing gap, TML-3044).dbgeneratedliteral defaults drifted forever. Emit kept'{}'::jsonbaskind: 'function'; introspection parsed the same literal tokind: 'literal';resolvedDefaultsEqualcompareskindfirst, sodb verifyreported such columnsnot-equalpermanently. Normalization now happens once, at SchemaIR construction.Dangling FKs dropped silently. Infer correctly drops an FK whose target is outside the introspected scope, but said nothing — a user loses every
auth.usersrelationship with no indication. It now says so, and points at the likely cause.Reviewer notes
projects/infer-emit-roundtrip/reviews/pr-1011/; its findings are fixed in the last three commits (strict date parsing made real,PSL_*_BACKRELATION_LISTdiagnostic codes renamed to drop the now-false_LISTsuffix, deferred-binding marker + spec correction).fix(contract-psl): defer the @db.Date -> pg/date@1 binding to remove-db-attributes— and it returns the four type-channel files (psl-column-resolution.ts,psl-named-type-resolution.ts, the postgres adapter'scontrol-mutation-defaults.ts,scripts/lint-framework-vocabulary.config.json) byte-identical tomain..include()decode failure is bigger than dates: TML-3054 records that the timestamp codecs reject Postgres's ownjson_aggrenderings andnumeric/int8lose precision in the envelope parse. Out of scope here; the pinned date scenario is the first recorded instance of that class.contract.prismadiff is the proof-of-done: regenerating against this branch produces zero diff.projects/infer-emit-roundtrip/(spec, plan, reproduction record, review artifacts) stays on disk for review; close-out deletes it after merge.reproduction.mdrecords the pre-fix state verbatim and is deliberately not updated.Breaking changes
Upgrade instructions are recorded in
skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/and the extension-author mirror.dbgenerated(...)literal defaults resolve differently on the next emit —storageHashchanges.contract inferre-run (sessionses→sessions). These are public field names consumers type.db verify --strictonly; non-strict verify filters an undeclared live default out entirely.@db.Datecolumns do not changecodecIdin this PR — that entry rides with the future binding. Index-type registration and theDecimalfix are not breaking: both previously threw unconditionally.Testing performed
On the final HEAD (after the review-rework commits):
pnpm build·pnpm typecheck·pnpm lint(incl.lint:deps,lint:casts,lint:framework-vocabulary) ·pnpm fixtures:check·pnpm check:upgrade-coverage— all greenpnpm test:packages— 995 files / 13185 passed (3 expected-fail, 1 skipped)pnpm test:integration— 204 files / 1173 passedpnpm test:e2e— 20 files / 109 passedpnpm --filter @prisma-next/extension-supabase run contract:generate— zero diff against the committed contractSkill update
skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.mdand the extension-author mirror carry the three breaking-change entries above. No CLI/API surface changed beyond what those entries describe.Follow-ups
Datebinding: asked of the remove-db-attributes project (pin its bareDatetopg/date@1, viacodecId— the descriptor carries a marker); the runtime instrument's pinned red assertion flips when it lands.@default(null)), TML-3042 (nullable lists), TML-3043 (FK-less relations), TML-3044 (identity DDL), TML-3045 (array-returning function defaults), TML-3048 (named-sequencenextval), TML-3054 (.include()decode class).Alternatives considered
Merge #998 as-is. This branch's first life. Its final review round bound
@db.Datetopg/date@1through a double-keyedscalarTypeDescriptorsmap (scalar names and attribute names sharing one map) to keep the codec id out of family-layer code. It worked, but it extended the exact bespoke@db.*channel that remove-db-attributes deletes — new machinery with a planned demolition date. Closing #998 and re-cutting with the binding deferred cost one commit; merging would have cost that project a migration.Bind the date spelling ourselves via a namespaced constructor (
pg.Date()). The existing pack-contribution channel supports it today, but remove-db-attributes already claims thedatenative type for its bareDate; shipping a second spelling now means two spellings with two bindings colliding at that project's printer handoff.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 is collective — the pack script shrinking 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. Admits
tags DateTime[] @default(now()), whose DDL Postgres refuses — moving the error from authoring time to apply time. Review caught it; the literal-default fix makes the array case work without it.Checklist
git commit -s) per the DCO. The DCO status check will block merge if any commit is missing aSigned-off-by:trailer.n/aif the change is doc-only / refactor with no behavioural delta).TML-NNNN: <sentence-case title>form (Linear ticket prefix + concise title naming the concrete deliverable). See.claude/skills/create-pr/SKILL.mdfor the full convention.n/a — internal only).Summary by CodeRabbit
datesupport with reliable JSON and runtime encoding/decoding.autoincrement()defaults.