Commit 649e805
authored
TML-3037: contract infer output round-trips through contract emit (#1011)
## Linked issue
Refs
[TML-3037](https://linear.app/prisma-company/issue/TML-3037/contract-infer-output-round-trips-through-contract-emit)
· Closes
[TML-3024](https://linear.app/prisma-company/issue/TML-3024/contract-infer-correct-back-relation-field-pluralization-dont-double)
· 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`, on
`main`, before this PR:
```ts
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, 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
1. **A round-trip instrument exists**: introspect → infer → emit → `db
verify --schema-only` against a live database, plus a runtime instrument
that builds a real `ExecutionContext` from the emitted contract. Every
defect below was reproduced by it before its fix.
2. **Seven infer→emit round-trip defects are fixed** (pluralization, 1:1
back-relations, `Decimal`-on-postgres, identity columns, index types,
list literal defaults, `dbgenerated` default drift), and dropped
dangling FKs are now explained in infer's output instead of vanishing
silently.
3. **The eighth defect — `date` columns — is fixed halfway,
deliberately.** The missing `pg/date@1` codec lands (with strict
parsing: `2024-02-31` is rejected, not normalized into March), but
nothing binds the `@db.Date` spelling 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 #975); its future bare `Date` type 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.ts` they run after
every `contract 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
infer` writes PSL that `contract emit` rejects, or that `db verify`
reports 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 unbounded `numeric`, `date`, `text[]`, `jsonb`, GIN and hash
indexes, and an FK pointing out of scope.
## What was broken
**Back-relation names double-pluralized.** `pluralize()` appended `es`
to anything ending in `s`, so already-plural table names — 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.** Not an infer bug: `Decimal` maps
to `pg/numeric@1` on the base-scalar path with no `typeParams`, so any
schema with a plain `amount Decimal` field threw
`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`.
**`date` columns break through `.include()`.** `@db.Date` inherits
`DateTime`'s `pg/timestamptz@1`, whose `decodeJson` rejects the bare
`YYYY-MM-DD` that `json_agg` renders. This PR ships the `pg/date@1`
codec 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 threw
`unregistered index type "gin"`. The target now registers its built-ins
(`btree`, `hash`, `gin`, `gist`, `spgist`, `brin`) via the same
`IndexTypeRegistry` mechanism ParadeDB uses for `bm25`; an unregistered
type is still rejected.
**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. Identity maps onto
`autoincrement()` symmetrically — infer emits it and the verify
normalizer resolves a live identity column to it, so neither side
drifts. PSL doesn't model `GENERATED ALWAYS` vs `BY DEFAULT`; a fresh
`db init` from such a contract creates `serial` (pre-existing gap,
[TML-3044](https://linear.app/prisma-company/issue/TML-3044)).
**`dbgenerated` literal defaults drifted forever.** Emit kept
`'{}'::jsonb` as `kind: 'function'`; introspection parsed the same
literal to `kind: 'literal'`; `resolvedDefaultsEqual` compares `kind`
first, so `db verify` reported such columns `not-equal` permanently.
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.users` relationship with no indication. It now says
so, and points at the likely cause.
## Reviewer notes
- **Prior review is absorbed, not pending.** The bulk of this branch
carried two review rounds on #998 (a pre-open pass and a round of four
architectural findings, all fixed at the root), and this PR itself had a
three-lens local review whose artifacts ship in
`projects/infer-emit-roundtrip/reviews/pr-1011/`; its findings are fixed
in the last three commits (strict date parsing made real,
`PSL_*_BACKRELATION_LIST` diagnostic codes renamed to drop the now-false
`_LIST` suffix, deferred-binding marker + spec correction).
- **The date-deferral delta is one commit** — `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's
`control-mutation-defaults.ts`,
`scripts/lint-framework-vocabulary.config.json`) byte-identical to
`main`.
- **The `.include()` decode failure is bigger than dates**:
[TML-3054](https://linear.app/prisma-company/issue/TML-3054/include-decode-breaks-for-most-non-trivial-native-types-codec)
records that the timestamp codecs reject Postgres's own `json_agg`
renderings and `numeric`/`int8` lose precision in the envelope parse.
Out of scope here; the pinned date scenario is the first recorded
instance of that class.
- **The largest commits are the instrument and the pack regeneration.**
The pack's `contract.prisma` diff 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.md` records 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 — `storageHash` changes.
- **Back-relation names change** on a future `contract infer` re-run
(`sessionses` → `sessions`). These are public field names consumers
type.
- **Identity columns need an explicit default under `db verify
--strict`** only; non-strict verify filters an undeclared live default
out entirely.
`@db.Date` columns do **not** change `codecId` in this PR — that entry
rides with the future binding. Index-type registration and the `Decimal`
fix 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 green
- `pnpm test:packages` — 995 files / 13185 passed (3 expected-fail, 1
skipped)
- `pnpm test:integration` — 204 files / 1173 passed
- `pnpm test:e2e` — 20 files / 109 passed
- Both round-trip instruments in isolation — 2 files / 13 passed
- `pnpm --filter @prisma-next/extension-supabase run contract:generate`
— zero diff against the committed contract
## Skill update
`skills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.md`
and the extension-author mirror carry the three breaking-change entries
above. No CLI/API surface changed beyond what those entries describe.
## Follow-ups
- `Date` binding: asked of the remove-db-attributes project (pin its
bare `Date` to `pg/date@1`, via `codecId` — the descriptor carries a
marker); the runtime instrument's pinned red assertion flips when it
lands.
- Deferred round-trip gaps, each filed:
[TML-3041](https://linear.app/prisma-company/issue/TML-3041)
(`@default(null)`),
[TML-3042](https://linear.app/prisma-company/issue/TML-3042) (nullable
lists), [TML-3043](https://linear.app/prisma-company/issue/TML-3043)
(FK-less relations),
[TML-3044](https://linear.app/prisma-company/issue/TML-3044) (identity
DDL), [TML-3045](https://linear.app/prisma-company/issue/TML-3045)
(array-returning function defaults),
[TML-3048](https://linear.app/prisma-company/issue/TML-3048)
(named-sequence `nextval`),
[TML-3054](https://linear.app/prisma-company/issue/TML-3054)
(`.include()` decode class).
## Alternatives considered
**Merge #998 as-is.** This branch's first life. Its final review round
bound `@db.Date` to `pg/date@1` through a double-keyed
`scalarTypeDescriptors` map (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 the `date` native type
for its bare `Date`; 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
- [x] All commits are signed off (`git commit -s`) per the
[DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO
status check will block merge if any commit is missing a
`Signed-off-by:` trailer.
- [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is
scoped to one logical concern.
- [x] Tests are updated (or `n/a` if the change is doc-only / refactor
with no behavioural delta).
- [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear
ticket prefix + concise title naming the concrete deliverable). See
`.claude/skills/create-pr/SKILL.md` for the full convention.
- [x] The **Skill update** section above is filled in (or stated `n/a —
internal only`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added PostgreSQL `date` support with reliable JSON and runtime
encoding/decoding.
* Added support for PostgreSQL identity columns and `autoincrement()`
defaults.
* Added PostgreSQL index-type support, including hash and GIN indexes.
* Improved array default handling and support for unbounded numeric
columns.
* **Bug Fixes**
* Improved one-to-one relation and backrelation detection, cardinality,
and diagnostics.
* Prevented double-pluralized inferred relation fields.
* Added warnings for foreign keys referencing unavailable tables.
* **Documentation**
* Updated upgrade guidance for identity defaults and relation naming
changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>1 parent e45302a commit 649e805
58 files changed
Lines changed: 2845 additions & 271 deletions
File tree
- packages
- 1-framework/3-tooling/cli/test
- 2-sql
- 1-core/schema-ir/test
- 2-authoring/contract-psl
- src
- test
- 9-family
- src
- core
- migrations
- psl-contract-infer
- exports
- test
- psl-contract-infer
- 3-extensions/supabase
- scripts
- src/contract
- 3-targets
- 3-targets/postgres
- src
- core
- migrations
- psl-infer
- exports
- test
- psl-infer
- print-psl
- 6-adapters/postgres
- src/core
- test
- migrations
- projects
- infer-emit-roundtrip
- sql-orm-many-to-many/slices/05-psl-many-to-many-authoring
- skills
- extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16
- upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16
- test/integration/test
- authoring/diagnostics
- ambiguous-backrelation
- orphaned-backrelation
- cli-journeys
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
| 101 | + | |
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| |||
Lines changed: 21 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
170 | 191 | | |
Lines changed: 37 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
82 | | - | |
| 82 | + | |
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| |||
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | | - | |
| 106 | + | |
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
| |||
668 | 668 | | |
669 | 669 | | |
670 | 670 | | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
671 | 686 | | |
672 | 687 | | |
673 | 688 | | |
| |||
726 | 741 | | |
727 | 742 | | |
728 | 743 | | |
729 | | - | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
730 | 755 | | |
731 | 756 | | |
732 | | - | |
| 757 | + | |
733 | 758 | | |
734 | 759 | | |
735 | 760 | | |
| |||
739 | 764 | | |
740 | 765 | | |
741 | 766 | | |
742 | | - | |
743 | 767 | | |
744 | 768 | | |
745 | 769 | | |
| |||
786 | 810 | | |
787 | 811 | | |
788 | 812 | | |
| 813 | + | |
789 | 814 | | |
790 | 815 | | |
791 | 816 | | |
| |||
1095 | 1120 | | |
1096 | 1121 | | |
1097 | 1122 | | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
1098 | 1130 | | |
1099 | 1131 | | |
1100 | 1132 | | |
| |||
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
347 | 347 | | |
348 | 348 | | |
349 | 349 | | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
350 | 357 | | |
351 | 358 | | |
352 | 359 | | |
| |||
Lines changed: 36 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
| |||
453 | 455 | | |
454 | 456 | | |
455 | 457 | | |
456 | | - | |
457 | | - | |
458 | | - | |
459 | | - | |
460 | | - | |
461 | | - | |
462 | | - | |
463 | | - | |
464 | | - | |
465 | | - | |
466 | | - | |
467 | | - | |
468 | | - | |
469 | | - | |
470 | | - | |
471 | | - | |
472 | | - | |
473 | | - | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
474 | 465 | | |
475 | | - | |
476 | | - | |
477 | | - | |
478 | | - | |
479 | | - | |
480 | | - | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
481 | 487 | | |
482 | 488 | | |
483 | | - | |
484 | | - | |
| 489 | + | |
| 490 | + | |
485 | 491 | | |
486 | 492 | | |
487 | 493 | | |
488 | 494 | | |
489 | 495 | | |
490 | 496 | | |
491 | 497 | | |
492 | | - | |
493 | | - | |
| 498 | + | |
| 499 | + | |
494 | 500 | | |
495 | 501 | | |
496 | 502 | | |
| |||
506 | 512 | | |
507 | 513 | | |
508 | 514 | | |
509 | | - | |
| 515 | + | |
510 | 516 | | |
511 | 517 | | |
512 | 518 | | |
| |||
517 | 523 | | |
518 | 524 | | |
519 | 525 | | |
520 | | - | |
| 526 | + | |
521 | 527 | | |
522 | 528 | | |
523 | 529 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
503 | | - | |
| 503 | + | |
504 | 504 | | |
505 | 505 | | |
506 | 506 | | |
| |||
534 | 534 | | |
535 | 535 | | |
536 | 536 | | |
537 | | - | |
| 537 | + | |
538 | 538 | | |
539 | 539 | | |
540 | 540 | | |
| |||
Lines changed: 3 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
428 | 428 | | |
429 | 429 | | |
430 | 430 | | |
431 | | - | |
| 431 | + | |
432 | 432 | | |
433 | 433 | | |
434 | 434 | | |
| |||
566 | 566 | | |
567 | 567 | | |
568 | 568 | | |
569 | | - | |
| 569 | + | |
570 | 570 | | |
571 | 571 | | |
572 | 572 | | |
| |||
590 | 590 | | |
591 | 591 | | |
592 | 592 | | |
593 | | - | |
| 593 | + | |
594 | 594 | | |
595 | 595 | | |
596 | 596 | | |
| |||
0 commit comments