| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(sql): @@check declares CHECK constraints; infer adopts existing ones (#29972) ## Linked issue No Linear ticket — the operator waived tracker integration for this project. Final slice of [sql-check-constraint-unification](projects/sql-check-constraint-unification/spec.md) ([slice spec](projects/sql-check-constraint-unification/slices/authored-check-constraints/spec.md)); follows #29892, #29894, #29928. ## At a glance Your database has a constraint somebody added by hand: ```sql ALTER TABLE orders ADD CONSTRAINT positive_total CHECK (total > 0); ``` Run `prisma-next contract infer` against it today and that rule is silently left out of the contract — there is no syntax that can express it. After this PR, it comes back: ```prisma model Order { id Int @id total Decimal @@check(expression: "(total > (0)::numeric)", map: "positive_total") } ``` That one line is the difference between the constraint being invisible to Prisma Next and being owned by it. ## Why that matters Before this project, Prisma Next could not see hand-written checks at all — introspection only recognised the ones it generated itself. Slice 1 (#29892) changed that so it reads every constraint, which is what made complete reconciliation possible. But it created an exposure. A constraint Prisma Next can see and the contract cannot mention reads as *leftover*. `db verify` tolerates it, `db verify --strict` reports it, and a migration allowed to make destructive changes **deletes it**. Somebody's data-integrity rule disappears in a routine migration, and there was no way to say "keep this". ## Decision Give authors a way to declare a check, in both authoring surfaces: ```prisma @@check(expression: "total > 0", name: "order_total_positive") // create a new one @@check(expression: "(total > (0)::numeric)", map: "positive_total") // adopt an existing one ``` ```ts check({ expression: 'total > 0', name: 'order_total_positive' }) // @prisma/orm-postgres/contract-builder ``` Three things follow from that: 1. **`name:` creates**, `map:` **adopts.** Use `name:` for a constraint you want Prisma Next to install; use `map:` to take ownership of one that already exists under its own name. 2. **`contract infer` writes the `map:` form for you** for every live check it did not generate. That is what closes the exposure above — pull a database and its hand-written constraints are declared from the first pull, no longer leftover, no longer dropped. 3. **Both surfaces produce identical contracts**, `storageHash` included, so PSL and TypeScript authors get the same artefact. ## How it works **`name:` is a prefix, not the constraint's name.** Writing `name: "order_total_positive"` produces a constraint actually called `order_total_positive_a1b2c3d4`, where the suffix is a hash of the predicate. Prisma Next then compares constraints *by name* rather than by SQL text. That indirection is the load-bearing part, and it exists because **Postgres rewrites your SQL**. Write `price > 0` and the database hands back `(price > (0)::numeric)`; write `status IN ('a','b')` on a `varchar` and you get `((status)::text = ANY ((ARRAY['a'::character varying, …])::text[]))`. Comparing your text against that never matches, so a naively-named check would report drift on every single verify — permanently, with no way for the author to fix it. Hashing the predicate into the name sidesteps the comparison entirely: same predicate, same name, no drift. Indexes ([ADR 243](docs/architecture%20docs/adrs/ADR%20243%20-%20Name-identified%20indexes%20and%20exact-name%20adoption.md)) and RLS policies ([ADR 234](docs/architecture%20docs/adrs/ADR%20234%20-%20Content-addressed%20wire%20names%20for%20Postgres-normalized%20objects.md)) already work this way; checks now join them. **`map:` is the exception, and it is for adoption only.** It uses the name verbatim and *does* compare the SQL text — which is safe precisely when the text came from the database itself, as it does when `contract infer` writes it. Hand-writing a body under `map:` gets you a warning pointing at `name:`. **One internal rule had to change.** Prisma Next generates some checks itself (enum membership, no-nulls-in-a-list) and strips them from tables it does not manage. It used to identify them by "does this have a hashed name?" — which authored checks now also have, so an author's constraint would have been silently deleted. Derivation is now decided by whether the name's prefix is one *generation* would have produced for a column of that table. An authored name that collides with that shape is rejected up front (`CONTRACT.CHECK_NAME_RESERVED`). ## Reviewer notes - **The proof is one journey test.** `test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts` seeds a hand-written constraint by raw SQL, pulls it, and asserts a destructive-allowing plan carries no check operation. It was confirmed to fail at the true fork point — an earlier attempt against a stale local `main` gave a false signal and was diagnosed rather than accepted — and re-verified independently at review by reproducing `main` and observing the plan there carry exactly `dropCheckConstraint.users.users_tags_not_empty`. - **One existing assertion was deliberately inverted.** Slice 3 asserted a hand-written check is *not* inferred; that is the behaviour this PR changes. The `--strict` expectation flipped with it, confirmed by running it rather than reasoning about it. - **`@@check` is capability-gated; `check()` is not.** Capabilities are adapter-reported and reach the contract only after it is built, so the TypeScript path cannot be refused at authoring time. A `check()` against SQLite is refused at DDL render instead — an accurate error, where previously the constraint was silently dropped. - **84 emitted contracts regenerate**, all gaining the same one additive capability line. The check node's own shape is unchanged. - **Review found `@@check` on a single-table-inheritance variant was silently discarded** — a pre-existing class that `@@index` and `@@unique` still share. Checks now raise a diagnostic; the siblings are left alone and recorded as a follow-up. ## Behaviour changes & evidence - **A hand-written constraint is declared by a pull and survives a destructive plan.** `psl-infer/infer-model-blocks.ts`, `psl-infer/infer-index-attributes.ts` — evidence: `cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`. - **`@@check` / `check()` declare a constraint.** `contract-psl/src/sql-attribute-specs.ts`, `contract-ts/src/contract-dsl.ts`, `contract-ts/src/build-contract.ts` — evidence: `interpreter.check-attribute.test.ts` (parity incl. `storageHash`), `check-constraint.authoring.test.ts`. - **Authored checks survive the strip on unmanaged tables.** `schema-ir/src/naming.ts`, `contract-ts/src/derived-checks.ts` — evidence: the strip tests in `check-constraint.authoring.test.ts`. - **Predicate edits plan drop+add; renames plan one `RENAME CONSTRAINT`.** Evidence: `check-lifecycle-e2e.integration.test.ts`, against a real database. - **SQLite refuses rather than dropping.** `sqlite/src/core/migrations/column-ddl-rendering.ts` — evidence: `column-ddl-rendering.test.ts`. ## Compatibility Additive. No existing contract changes meaning, and the check node's shape is untouched — emitted contracts gain one capability line. Upgrade entries are recorded for both audiences; the pack-author entry flags that `check.prefix !== undefined` no longer means "Prisma Next generated this". ADR 244's known cost — hand-written checks droppable under a destructive policy — is now closed. ## Verification - `pnpm -w build` 86/86 · `pnpm typecheck` 165/165 - `lint:deps`, `fixtures:check`, `check:error-reference`, `check:upgrade-coverage`, cast ratchet (delta 0) — clean - Suites: sql-contract-ts 479 · sql-contract-psl 436 · sql-schema-ir 255 · target-postgres 1397 · target-sqlite 271 · adapter-postgres 814 (+3 pre-existing expected-fail) · adapter-sqlite 268 - Real database: `check-lifecycle-e2e` 19/19 · `infer-roundtrip-fidelity` 19/19 · journeys 157/157 - Unrelated and pre-existing: `issues-28192-pg-historical-dates` fails 2/9 on a non-UTC machine. ## Follow-ups - Validate declared capability requirements after adapter enrichment, so the TypeScript surface can be refused at authoring rather than at DDL render. - `@@index` / `@@unique` on an inheritance variant are still silently discarded. - A live enum-membership check still does not infer back into `enumType()`, so a text-backed domain enum is not recovered from a pull. ## Alternatives considered - **Let the author name the constraint exactly.** The obvious design, and the reason for the prefix indirection above: byte-comparing an author's SQL against Postgres's rewrite fails verify under every policy — including `external` — and hard-errors the planner, with no remedy available to the author. - **Normalise both sides before comparing.** Needs a parser for the rewritten form; that is the approach slice 1 deleted. - **Mark authored checks with a field on the check node** instead of inferring it from the name. Rejected: it changes the contract's wire shape — regenerating every fixture — to record something the naming rule already implies. - **Make `@@check` a Postgres-contributed attribute** rather than a SQL-wide one. Cheaper to gate, but CHECK is standard SQL, the contribution seam files entities in the wrong place for a table-level object, and it would not cover the TypeScript surface. - **A column-level `@check`.** Postgres stores column-level CHECK syntax identically to table-level, so one model-level surface covers both. ## Checklist - [x] All commits carry DCO sign-off - [x] I have read CONTRIBUTING.md - [x] Tests added for every behaviour change, including a regression test proven to fail on `main` - [x] Title follows the prevailing convention 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for authoring PostgreSQL CHECK constraints through `@@check` and the TypeScript contract builder. * Supports generated wire names, exact database-name mapping, multiple checks, and preservation during schema inference. * Added validation, warnings, and diagnostics for invalid names and unsupported inheritance scenarios. * **Bug Fixes** * Improved handling of authored and automatically derived checks during inference and migrations. * SQLite now reports a clear error when CHECK constraints are unsupported. * **Documentation** * Updated capability references, error guidance, architecture decisions, and upgrade instructions. <!-- 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 个月前 | |
feat(sql): @@check declares CHECK constraints; infer adopts existing ones (#29972) ## Linked issue No Linear ticket — the operator waived tracker integration for this project. Final slice of [sql-check-constraint-unification](projects/sql-check-constraint-unification/spec.md) ([slice spec](projects/sql-check-constraint-unification/slices/authored-check-constraints/spec.md)); follows #29892, #29894, #29928. ## At a glance Your database has a constraint somebody added by hand: ```sql ALTER TABLE orders ADD CONSTRAINT positive_total CHECK (total > 0); ``` Run `prisma-next contract infer` against it today and that rule is silently left out of the contract — there is no syntax that can express it. After this PR, it comes back: ```prisma model Order { id Int @id total Decimal @@check(expression: "(total > (0)::numeric)", map: "positive_total") } ``` That one line is the difference between the constraint being invisible to Prisma Next and being owned by it. ## Why that matters Before this project, Prisma Next could not see hand-written checks at all — introspection only recognised the ones it generated itself. Slice 1 (#29892) changed that so it reads every constraint, which is what made complete reconciliation possible. But it created an exposure. A constraint Prisma Next can see and the contract cannot mention reads as *leftover*. `db verify` tolerates it, `db verify --strict` reports it, and a migration allowed to make destructive changes **deletes it**. Somebody's data-integrity rule disappears in a routine migration, and there was no way to say "keep this". ## Decision Give authors a way to declare a check, in both authoring surfaces: ```prisma @@check(expression: "total > 0", name: "order_total_positive") // create a new one @@check(expression: "(total > (0)::numeric)", map: "positive_total") // adopt an existing one ``` ```ts check({ expression: 'total > 0', name: 'order_total_positive' }) // @prisma/orm-postgres/contract-builder ``` Three things follow from that: 1. **`name:` creates**, `map:` **adopts.** Use `name:` for a constraint you want Prisma Next to install; use `map:` to take ownership of one that already exists under its own name. 2. **`contract infer` writes the `map:` form for you** for every live check it did not generate. That is what closes the exposure above — pull a database and its hand-written constraints are declared from the first pull, no longer leftover, no longer dropped. 3. **Both surfaces produce identical contracts**, `storageHash` included, so PSL and TypeScript authors get the same artefact. ## How it works **`name:` is a prefix, not the constraint's name.** Writing `name: "order_total_positive"` produces a constraint actually called `order_total_positive_a1b2c3d4`, where the suffix is a hash of the predicate. Prisma Next then compares constraints *by name* rather than by SQL text. That indirection is the load-bearing part, and it exists because **Postgres rewrites your SQL**. Write `price > 0` and the database hands back `(price > (0)::numeric)`; write `status IN ('a','b')` on a `varchar` and you get `((status)::text = ANY ((ARRAY['a'::character varying, …])::text[]))`. Comparing your text against that never matches, so a naively-named check would report drift on every single verify — permanently, with no way for the author to fix it. Hashing the predicate into the name sidesteps the comparison entirely: same predicate, same name, no drift. Indexes ([ADR 243](docs/architecture%20docs/adrs/ADR%20243%20-%20Name-identified%20indexes%20and%20exact-name%20adoption.md)) and RLS policies ([ADR 234](docs/architecture%20docs/adrs/ADR%20234%20-%20Content-addressed%20wire%20names%20for%20Postgres-normalized%20objects.md)) already work this way; checks now join them. **`map:` is the exception, and it is for adoption only.** It uses the name verbatim and *does* compare the SQL text — which is safe precisely when the text came from the database itself, as it does when `contract infer` writes it. Hand-writing a body under `map:` gets you a warning pointing at `name:`. **One internal rule had to change.** Prisma Next generates some checks itself (enum membership, no-nulls-in-a-list) and strips them from tables it does not manage. It used to identify them by "does this have a hashed name?" — which authored checks now also have, so an author's constraint would have been silently deleted. Derivation is now decided by whether the name's prefix is one *generation* would have produced for a column of that table. An authored name that collides with that shape is rejected up front (`CONTRACT.CHECK_NAME_RESERVED`). ## Reviewer notes - **The proof is one journey test.** `test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts` seeds a hand-written constraint by raw SQL, pulls it, and asserts a destructive-allowing plan carries no check operation. It was confirmed to fail at the true fork point — an earlier attempt against a stale local `main` gave a false signal and was diagnosed rather than accepted — and re-verified independently at review by reproducing `main` and observing the plan there carry exactly `dropCheckConstraint.users.users_tags_not_empty`. - **One existing assertion was deliberately inverted.** Slice 3 asserted a hand-written check is *not* inferred; that is the behaviour this PR changes. The `--strict` expectation flipped with it, confirmed by running it rather than reasoning about it. - **`@@check` is capability-gated; `check()` is not.** Capabilities are adapter-reported and reach the contract only after it is built, so the TypeScript path cannot be refused at authoring time. A `check()` against SQLite is refused at DDL render instead — an accurate error, where previously the constraint was silently dropped. - **84 emitted contracts regenerate**, all gaining the same one additive capability line. The check node's own shape is unchanged. - **Review found `@@check` on a single-table-inheritance variant was silently discarded** — a pre-existing class that `@@index` and `@@unique` still share. Checks now raise a diagnostic; the siblings are left alone and recorded as a follow-up. ## Behaviour changes & evidence - **A hand-written constraint is declared by a pull and survives a destructive plan.** `psl-infer/infer-model-blocks.ts`, `psl-infer/infer-index-attributes.ts` — evidence: `cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`. - **`@@check` / `check()` declare a constraint.** `contract-psl/src/sql-attribute-specs.ts`, `contract-ts/src/contract-dsl.ts`, `contract-ts/src/build-contract.ts` — evidence: `interpreter.check-attribute.test.ts` (parity incl. `storageHash`), `check-constraint.authoring.test.ts`. - **Authored checks survive the strip on unmanaged tables.** `schema-ir/src/naming.ts`, `contract-ts/src/derived-checks.ts` — evidence: the strip tests in `check-constraint.authoring.test.ts`. - **Predicate edits plan drop+add; renames plan one `RENAME CONSTRAINT`.** Evidence: `check-lifecycle-e2e.integration.test.ts`, against a real database. - **SQLite refuses rather than dropping.** `sqlite/src/core/migrations/column-ddl-rendering.ts` — evidence: `column-ddl-rendering.test.ts`. ## Compatibility Additive. No existing contract changes meaning, and the check node's shape is untouched — emitted contracts gain one capability line. Upgrade entries are recorded for both audiences; the pack-author entry flags that `check.prefix !== undefined` no longer means "Prisma Next generated this". ADR 244's known cost — hand-written checks droppable under a destructive policy — is now closed. ## Verification - `pnpm -w build` 86/86 · `pnpm typecheck` 165/165 - `lint:deps`, `fixtures:check`, `check:error-reference`, `check:upgrade-coverage`, cast ratchet (delta 0) — clean - Suites: sql-contract-ts 479 · sql-contract-psl 436 · sql-schema-ir 255 · target-postgres 1397 · target-sqlite 271 · adapter-postgres 814 (+3 pre-existing expected-fail) · adapter-sqlite 268 - Real database: `check-lifecycle-e2e` 19/19 · `infer-roundtrip-fidelity` 19/19 · journeys 157/157 - Unrelated and pre-existing: `issues-28192-pg-historical-dates` fails 2/9 on a non-UTC machine. ## Follow-ups - Validate declared capability requirements after adapter enrichment, so the TypeScript surface can be refused at authoring rather than at DDL render. - `@@index` / `@@unique` on an inheritance variant are still silently discarded. - A live enum-membership check still does not infer back into `enumType()`, so a text-backed domain enum is not recovered from a pull. ## Alternatives considered - **Let the author name the constraint exactly.** The obvious design, and the reason for the prefix indirection above: byte-comparing an author's SQL against Postgres's rewrite fails verify under every policy — including `external` — and hard-errors the planner, with no remedy available to the author. - **Normalise both sides before comparing.** Needs a parser for the rewritten form; that is the approach slice 1 deleted. - **Mark authored checks with a field on the check node** instead of inferring it from the name. Rejected: it changes the contract's wire shape — regenerating every fixture — to record something the naming rule already implies. - **Make `@@check` a Postgres-contributed attribute** rather than a SQL-wide one. Cheaper to gate, but CHECK is standard SQL, the contribution seam files entities in the wrong place for a table-level object, and it would not cover the TypeScript surface. - **A column-level `@check`.** Postgres stores column-level CHECK syntax identically to table-level, so one model-level surface covers both. ## Checklist - [x] All commits carry DCO sign-off - [x] I have read CONTRIBUTING.md - [x] Tests added for every behaviour change, including a regression test proven to fail on `main` - [x] Title follows the prevailing convention 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for authoring PostgreSQL CHECK constraints through `@@check` and the TypeScript contract builder. * Supports generated wire names, exact database-name mapping, multiple checks, and preservation during schema inference. * Added validation, warnings, and diagnostics for invalid names and unsupported inheritance scenarios. * **Bug Fixes** * Improved handling of authored and automatically derived checks during inference and migrations. * SQLite now reports a clear error when CHECK constraints are unsupported. * **Documentation** * Updated capability references, error guidance, architecture decisions, and upgrade instructions. <!-- 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 个月前 | |
feat(sql): @@check declares CHECK constraints; infer adopts existing ones (#29972) ## Linked issue No Linear ticket — the operator waived tracker integration for this project. Final slice of [sql-check-constraint-unification](projects/sql-check-constraint-unification/spec.md) ([slice spec](projects/sql-check-constraint-unification/slices/authored-check-constraints/spec.md)); follows #29892, #29894, #29928. ## At a glance Your database has a constraint somebody added by hand: ```sql ALTER TABLE orders ADD CONSTRAINT positive_total CHECK (total > 0); ``` Run `prisma-next contract infer` against it today and that rule is silently left out of the contract — there is no syntax that can express it. After this PR, it comes back: ```prisma model Order { id Int @id total Decimal @@check(expression: "(total > (0)::numeric)", map: "positive_total") } ``` That one line is the difference between the constraint being invisible to Prisma Next and being owned by it. ## Why that matters Before this project, Prisma Next could not see hand-written checks at all — introspection only recognised the ones it generated itself. Slice 1 (#29892) changed that so it reads every constraint, which is what made complete reconciliation possible. But it created an exposure. A constraint Prisma Next can see and the contract cannot mention reads as *leftover*. `db verify` tolerates it, `db verify --strict` reports it, and a migration allowed to make destructive changes **deletes it**. Somebody's data-integrity rule disappears in a routine migration, and there was no way to say "keep this". ## Decision Give authors a way to declare a check, in both authoring surfaces: ```prisma @@check(expression: "total > 0", name: "order_total_positive") // create a new one @@check(expression: "(total > (0)::numeric)", map: "positive_total") // adopt an existing one ``` ```ts check({ expression: 'total > 0', name: 'order_total_positive' }) // @prisma/orm-postgres/contract-builder ``` Three things follow from that: 1. **`name:` creates**, `map:` **adopts.** Use `name:` for a constraint you want Prisma Next to install; use `map:` to take ownership of one that already exists under its own name. 2. **`contract infer` writes the `map:` form for you** for every live check it did not generate. That is what closes the exposure above — pull a database and its hand-written constraints are declared from the first pull, no longer leftover, no longer dropped. 3. **Both surfaces produce identical contracts**, `storageHash` included, so PSL and TypeScript authors get the same artefact. ## How it works **`name:` is a prefix, not the constraint's name.** Writing `name: "order_total_positive"` produces a constraint actually called `order_total_positive_a1b2c3d4`, where the suffix is a hash of the predicate. Prisma Next then compares constraints *by name* rather than by SQL text. That indirection is the load-bearing part, and it exists because **Postgres rewrites your SQL**. Write `price > 0` and the database hands back `(price > (0)::numeric)`; write `status IN ('a','b')` on a `varchar` and you get `((status)::text = ANY ((ARRAY['a'::character varying, …])::text[]))`. Comparing your text against that never matches, so a naively-named check would report drift on every single verify — permanently, with no way for the author to fix it. Hashing the predicate into the name sidesteps the comparison entirely: same predicate, same name, no drift. Indexes ([ADR 243](docs/architecture%20docs/adrs/ADR%20243%20-%20Name-identified%20indexes%20and%20exact-name%20adoption.md)) and RLS policies ([ADR 234](docs/architecture%20docs/adrs/ADR%20234%20-%20Content-addressed%20wire%20names%20for%20Postgres-normalized%20objects.md)) already work this way; checks now join them. **`map:` is the exception, and it is for adoption only.** It uses the name verbatim and *does* compare the SQL text — which is safe precisely when the text came from the database itself, as it does when `contract infer` writes it. Hand-writing a body under `map:` gets you a warning pointing at `name:`. **One internal rule had to change.** Prisma Next generates some checks itself (enum membership, no-nulls-in-a-list) and strips them from tables it does not manage. It used to identify them by "does this have a hashed name?" — which authored checks now also have, so an author's constraint would have been silently deleted. Derivation is now decided by whether the name's prefix is one *generation* would have produced for a column of that table. An authored name that collides with that shape is rejected up front (`CONTRACT.CHECK_NAME_RESERVED`). ## Reviewer notes - **The proof is one journey test.** `test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts` seeds a hand-written constraint by raw SQL, pulls it, and asserts a destructive-allowing plan carries no check operation. It was confirmed to fail at the true fork point — an earlier attempt against a stale local `main` gave a false signal and was diagnosed rather than accepted — and re-verified independently at review by reproducing `main` and observing the plan there carry exactly `dropCheckConstraint.users.users_tags_not_empty`. - **One existing assertion was deliberately inverted.** Slice 3 asserted a hand-written check is *not* inferred; that is the behaviour this PR changes. The `--strict` expectation flipped with it, confirmed by running it rather than reasoning about it. - **`@@check` is capability-gated; `check()` is not.** Capabilities are adapter-reported and reach the contract only after it is built, so the TypeScript path cannot be refused at authoring time. A `check()` against SQLite is refused at DDL render instead — an accurate error, where previously the constraint was silently dropped. - **84 emitted contracts regenerate**, all gaining the same one additive capability line. The check node's own shape is unchanged. - **Review found `@@check` on a single-table-inheritance variant was silently discarded** — a pre-existing class that `@@index` and `@@unique` still share. Checks now raise a diagnostic; the siblings are left alone and recorded as a follow-up. ## Behaviour changes & evidence - **A hand-written constraint is declared by a pull and survives a destructive plan.** `psl-infer/infer-model-blocks.ts`, `psl-infer/infer-index-attributes.ts` — evidence: `cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`. - **`@@check` / `check()` declare a constraint.** `contract-psl/src/sql-attribute-specs.ts`, `contract-ts/src/contract-dsl.ts`, `contract-ts/src/build-contract.ts` — evidence: `interpreter.check-attribute.test.ts` (parity incl. `storageHash`), `check-constraint.authoring.test.ts`. - **Authored checks survive the strip on unmanaged tables.** `schema-ir/src/naming.ts`, `contract-ts/src/derived-checks.ts` — evidence: the strip tests in `check-constraint.authoring.test.ts`. - **Predicate edits plan drop+add; renames plan one `RENAME CONSTRAINT`.** Evidence: `check-lifecycle-e2e.integration.test.ts`, against a real database. - **SQLite refuses rather than dropping.** `sqlite/src/core/migrations/column-ddl-rendering.ts` — evidence: `column-ddl-rendering.test.ts`. ## Compatibility Additive. No existing contract changes meaning, and the check node's shape is untouched — emitted contracts gain one capability line. Upgrade entries are recorded for both audiences; the pack-author entry flags that `check.prefix !== undefined` no longer means "Prisma Next generated this". ADR 244's known cost — hand-written checks droppable under a destructive policy — is now closed. ## Verification - `pnpm -w build` 86/86 · `pnpm typecheck` 165/165 - `lint:deps`, `fixtures:check`, `check:error-reference`, `check:upgrade-coverage`, cast ratchet (delta 0) — clean - Suites: sql-contract-ts 479 · sql-contract-psl 436 · sql-schema-ir 255 · target-postgres 1397 · target-sqlite 271 · adapter-postgres 814 (+3 pre-existing expected-fail) · adapter-sqlite 268 - Real database: `check-lifecycle-e2e` 19/19 · `infer-roundtrip-fidelity` 19/19 · journeys 157/157 - Unrelated and pre-existing: `issues-28192-pg-historical-dates` fails 2/9 on a non-UTC machine. ## Follow-ups - Validate declared capability requirements after adapter enrichment, so the TypeScript surface can be refused at authoring rather than at DDL render. - `@@index` / `@@unique` on an inheritance variant are still silently discarded. - A live enum-membership check still does not infer back into `enumType()`, so a text-backed domain enum is not recovered from a pull. ## Alternatives considered - **Let the author name the constraint exactly.** The obvious design, and the reason for the prefix indirection above: byte-comparing an author's SQL against Postgres's rewrite fails verify under every policy — including `external` — and hard-errors the planner, with no remedy available to the author. - **Normalise both sides before comparing.** Needs a parser for the rewritten form; that is the approach slice 1 deleted. - **Mark authored checks with a field on the check node** instead of inferring it from the name. Rejected: it changes the contract's wire shape — regenerating every fixture — to record something the naming rule already implies. - **Make `@@check` a Postgres-contributed attribute** rather than a SQL-wide one. Cheaper to gate, but CHECK is standard SQL, the contribution seam files entities in the wrong place for a table-level object, and it would not cover the TypeScript surface. - **A column-level `@check`.** Postgres stores column-level CHECK syntax identically to table-level, so one model-level surface covers both. ## Checklist - [x] All commits carry DCO sign-off - [x] I have read CONTRIBUTING.md - [x] Tests added for every behaviour change, including a regression test proven to fail on `main` - [x] Title follows the prevailing convention 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for authoring PostgreSQL CHECK constraints through `@@check` and the TypeScript contract builder. * Supports generated wire names, exact database-name mapping, multiple checks, and preservation during schema inference. * Added validation, warnings, and diagnostics for invalid names and unsupported inheritance scenarios. * **Bug Fixes** * Improved handling of authored and automatically derived checks during inference and migrations. * SQLite now reports a clear error when CHECK constraints are unsupported. * **Documentation** * Updated capability references, error guidance, architecture decisions, and upgrade instructions. <!-- 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 个月前 |