| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(psl): write raw SQL column defaults as sql`...` tagged literals (#30325) A raw SQL column default is now written as a tagged literal. Before and after, in PSL: ```prisma // before id String @id @default(dbgenerated("gen_random_uuid()")) expiresAt DateTime @default(dbgenerated("(now() + '00:03:00'::interval)")) // after id String @id @default(sql`gen_random_uuid()`) expiresAt DateTime @default(sql`(now() + '00:03:00'::interval)`) ``` And in TypeScript: ```ts // before id: field.column(uuidColumn).defaultSql('gen_random_uuid()').id(), createdAt: field.column(timestamptzColumn).defaultSql('now()'), // after id: field.column(uuidColumn).default(sql`gen_random_uuid()`).id(), createdAt: field.column(timestamptzColumn).default(now()), expiresAt: field.column(timestamptzColumn).default(sql`(now() + '00:03:00'::interval)`), ``` Both forms emit exactly the contract the old ones did. `contract.json` does not change shape, every existing fixture emits byte-identical output, and `dbgenerated` still works after this PR. The Postgres demo app now has a column with a `sql` default, so its migration replay test proves the form end to end against a real database. ## The decision `dbgenerated("...")` puts arbitrary SQL into a schema as an unnamed string. It was accepted in ADR 167 as a stopgap until typed defaults existed, and it was never meant to ship. It is being retired in three PRs: this one adds the replacement, a parallel PR adds codec-owned PSL literals so JSON and other typed defaults no longer need raw SQL (ADR 184), and a final PR deletes `dbgenerated`, regenerates the Supabase contract, and ships the upgrade instruction. The replacement is the tagged literal ADR 129 designed. ADR 129 is rewritten in this PR to describe the mechanism as built; it is the reference for everything below. ## How it works, from the schema down **Syntax.** A tag followed by a string literal: `` sql`...` ``, `sql"..."`, or `sql'...'`. The tag is an ordinary qualified name and the string is an ordinary string literal, which now also accepts backticks; whitespace between them is allowed and the formatter removes it. A backtick string may span lines and has only two escapes, `` \` `` and `\\`, so everything else reaches the database as written. A backtick string is valid only after a tag. An unterminated backtick string stops before the next line that starts with `}`, so the rest of the file still parses. **One canonical body.** After escapes, the framework normalises line endings, trims a blank first and last line, removes common indentation, refuses NUL, and caps the body at 64 KiB. PSL and the TypeScript `sql` tag call the same function on the same raw text, so the two languages cannot disagree about a default. **Who registers tags.** Any pack may; only the target may register an unprefixed one. Postgres registers `sql` and `pg.sql`, SQLite registers `sql` and `sqlite.sql`, both through one implementation the SQL family exports. The registry sits beside the existing default-function registry on `ControlMutationDefaults`. Attribute parsing only checks that an argument is a tagged literal; lowering checks the tag is registered (the diagnostic lists the registered tags) and that the body canonicalizes. The language server completes the registered tags inside `@default(`. **Lowering.** The `sql` tag lowers to the function-kind column default the contract already has. The body is used verbatim: nothing rewrites it at authoring, in the contract, or in DDL. One body check, `checkSqlDefaultBody` in the SQL contract package, refuses `;`, comment markers, `$$`, and `SELECT`; authoring runs it with a source span and both planners run it before rendering. SQLite's planner previously had a weaker copy of that check. A body that is exactly `now()` or `autoincrement()` is refused, in PSL and TypeScript alike, with a hint to write the named function: those two texts are Prisma markers the planners render specially, so the SQL would not be used as written. Every other body, including `NOW()` and `gen_random_uuid()`, passes unchanged. **TypeScript.** The SQL contract builder exports `sql` (interpolation is a compile error and a runtime error), `now()`, and `autoincrement()`. `.defaultSql()` stays, marked deprecated with the replacement named, and is removed at 8.0.0 GA. Every call in this repository is rewritten. ## Behaviour changes to check - **List columns take storage defaults.** `` tags String[] @default(sql`'{}'::text[]`) `` and `tags String[] @default(now())` both lower. The interpreter cannot know any function's return type, so it no longer refuses functions on lists; the database reports a wrong type. Two things are still refused on a list: single-value client generators such as `uuid()`, and `autoincrement()`, which is a Prisma marker for a sequence-backed scalar column rather than SQL (`PSL_LIST_AUTOINCREMENT_UNSUPPORTED`). - **Both planners render the authored default.** Postgres previously rendered its normalised form, so `` sql`nextval('orders_seq'::regclass)` `` would have become a `SERIAL` column and `CURRENT_TIMESTAMP` would have become `now()`. Plan and verify on both targets compare through each target's introspection parser, so `` sql`CURRENT_TIMESTAMP` `` verifies clean and a second `db update` plans nothing. - **`ControlMutationDefaults.defaultLiteralTagRegistry` is required.** A pack that contributes default functions must now contribute a tag registry too, empty or not. The extension upgrade note shows the one-line fix. ## Where to start reviewing 1. `packages/1-framework/2-authoring/psl-parser/src/tokenizer.ts`, `parse.ts`, and `syntax/ast/expressions.ts`: backtick strings, their escapes, and the tagged-literal node built from a qualified name and a string literal. 2. `packages/1-framework/1-core/framework-components/src/shared/tagged-literal.ts`: the canonical body, with a table test per step. 3. `packages/2-sql/9-family/src/core/sql-default-literal-tag.ts` and both adapters' `control-mutation-defaults.ts`: registration and lowering. 4. `packages/2-sql/2-authoring/contract-ts/src/sql-default-literal.ts`: the TypeScript tag. 5. `test/integration/test/authoring/parity/default-sql-literal/`: PSL and TypeScript emitting the same contract. 6. `examples/prisma-8-demo/migrations/app/20260917T0818_add_post_expires_at/`: the DDL the planner renders for a `sql` default. ## Alternatives considered - **Named functions plus typed literals only, with anything else reported as a gap.** Rejected: a real default such as `(now() + '00:03:00'::interval)` would have no way to be written. - **Keep `dbgenerated` and document it.** Rejected: it was never accepted as a feature, and it keeps raw text in the language with no owner. - **Store raw defaults as a hashed payload, like index expressions and check constraints.** Not adopted here. A column default has no catalog name to carry a hash, so verify would still compare the database's reprint, and the contract shape would change for every consumer. Recorded in ADR 129's alternatives. - **A separate token and node for tagged literals.** Replaced during review: the node now reuses the qualified-name and string-literal parsing, so escaping and unterminated-string recovery live in one place. - **A named `gen_random_uuid()` default on Postgres.** Tried and removed during review. It read like Prisma's own `uuid()`, which generates the value in the client, while `gen_random_uuid()` makes the database generate it, and the names do not show that. Named defaults stay limited to `now()` and `autoincrement()`; database functions are written as `sql`. - **Rewrite known expressions at authoring time**, as the SQLite adapter did for `CURRENT_TIMESTAMP`. Rejected: a person who writes SQL expects that SQL to run. Refs: ADR 129, ADR 167, ADR 184. 🤖 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 SQL tagged literals for column defaults in PSL and TypeScript, including `sql`, `pg.sql`, and `sqlite.sql`. - Added `now()` and `autoincrement()` helpers for common defaults. - Added editor completions and formatting support for tagged literals. - SQL defaults now support multiline content with normalization and safety validation. - **Bug Fixes** - Migrations preserve authored SQL default expressions across PostgreSQL and SQLite. - **Documentation** - Added upgrade guidance and updated architecture, error-reference, and usage documentation. <!-- 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> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 3 天前 | |
fix(sql): the TypeScript sql tag resolves \$, so a raw default can contain ${ (#30347) A raw SQL default whose text contains `${` can now be written from TypeScript: ```ts // the column default is the literal text 'Home | ${user.name}' title: field.column(textColumn).default(sql`'Home | \${user.name}'`), ``` ```prisma // the same default in PSL, where no escape is needed title String @default(sql`'Home | ${user.name}'`) ``` Both emit the same contract, `{ kind: 'function', expression: "'Home | ${user.name}'" }`. ## The decision The `sql` tagged literal, added in #30325, reads its body as raw text and resolves two escapes, a backtick and a backslash. That works for PSL, where `${` is ordinary text. It does not work for TypeScript, because `${` in a template literal starts an interpolation. Writing it unescaped makes JavaScript substitute a value, which the tag refuses; writing `\${`, the only alternative JavaScript offers, left the backslash in the body and sent the wrong text to the database. So a default containing those two characters could be written in PSL and not in TypeScript. The TypeScript tag now resolves a third escape, `\$` to `$`. PSL is unchanged. The two languages therefore differ for the sequence `\$` alone, and only there. The alternative was to resolve `\$` in PSL too, so the escaping rules stay identical. That was rejected: PSL needs no escape for `${`, so the rule would tax every PSL author who writes a backslash before a dollar sign, to serve a body that only JavaScript struggles to write. ADR 129 records both. ## Changes - **Framework (`@internal/framework-components`)**: the one escape resolver becomes two, `resolvePslBacktickEscapes` and `resolveTemplateTagEscapes`, sharing a helper. Each says at its declaration which surface uses it and why the template one differs. - **TypeScript builder**: the `sql` tag uses the template-tag resolver. A real interpolation is still a compile error and still throws `CONTRACT.DEFAULT_SQL_INTERPOLATION` at runtime. - **Tests**: both resolvers, including `\$` on each side; the tag turning `` sql`'Home | \${user}'` `` into `'Home | ${user}'`; `` sql`\\$x` `` giving a backslash then a dollar; and, on the PSL side, a parser test and an interpreter test pinning that `${` and `\$` pass through as written. - **Docs**: both authoring READMEs, the two pending upgrade instructions, and ADR 129. Refs: ADR 129, #30325. 🤖 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** * TypeScript `sql` templates now support escaped dollar signs and literal `${...}` sequences using `\${...}`. * PSL backtick strings preserve `${...}` and dollar escapes as written. * **Documentation** * Updated SQL default-value guidance and migration instructions to explain differing TypeScript and PSL escape rules, including how to preserve literal `\$` sequences. * **Tests** * Added coverage for dollar-brace sequences, escaped dollars, backslashes, and preserved PSL escapes. <!-- 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> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 2 天前 | |
Project shaping: remove dbgenerated(...) from Prisma 8 (spec, plan, slice specs) (#30327) ## Linked issue n/a — project shaping artifacts. Follow-up PRs: slice A https://github.com/prisma/orm/pull/30325, slice B https://github.com/prisma/orm/pull/30324; slice C follows both. ## Summary Adds the project folder for removing `@default(dbgenerated("..."))` from Prisma 8: the project spec with its decisions, the plan splitting the work into three slices, the deferred-decision list, and the three slice specs. Nothing outside `projects/remove-dbgenerated/` changes. Merging this first lets both slice PRs rebase to pure code diffs instead of each replaying the same docs commit. ## Testing performed - n/a — docs only; `pnpm lint:docs` covers the markdown. ## Skill update n/a — internal only. ## Checklist - [x] All commits are signed off (`git commit -s`) per the [DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). - [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is scoped to one logical concern. - [x] Tests are updated (n/a, doc-only). - [ ] The PR title is in `TML-NNNN: <sentence-case title>` form (no ticket; the project spec records that Linear is not yet created). - [x] The **Skill update** section above is filled in. ## Notes for the reviewer The folder is transient: the project's close-out step moves long-lived content to `docs/` and deletes it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 3 天前 | |
feat(psl): write raw SQL column defaults as sql`...` tagged literals (#30325) A raw SQL column default is now written as a tagged literal. Before and after, in PSL: ```prisma // before id String @id @default(dbgenerated("gen_random_uuid()")) expiresAt DateTime @default(dbgenerated("(now() + '00:03:00'::interval)")) // after id String @id @default(sql`gen_random_uuid()`) expiresAt DateTime @default(sql`(now() + '00:03:00'::interval)`) ``` And in TypeScript: ```ts // before id: field.column(uuidColumn).defaultSql('gen_random_uuid()').id(), createdAt: field.column(timestamptzColumn).defaultSql('now()'), // after id: field.column(uuidColumn).default(sql`gen_random_uuid()`).id(), createdAt: field.column(timestamptzColumn).default(now()), expiresAt: field.column(timestamptzColumn).default(sql`(now() + '00:03:00'::interval)`), ``` Both forms emit exactly the contract the old ones did. `contract.json` does not change shape, every existing fixture emits byte-identical output, and `dbgenerated` still works after this PR. The Postgres demo app now has a column with a `sql` default, so its migration replay test proves the form end to end against a real database. ## The decision `dbgenerated("...")` puts arbitrary SQL into a schema as an unnamed string. It was accepted in ADR 167 as a stopgap until typed defaults existed, and it was never meant to ship. It is being retired in three PRs: this one adds the replacement, a parallel PR adds codec-owned PSL literals so JSON and other typed defaults no longer need raw SQL (ADR 184), and a final PR deletes `dbgenerated`, regenerates the Supabase contract, and ships the upgrade instruction. The replacement is the tagged literal ADR 129 designed. ADR 129 is rewritten in this PR to describe the mechanism as built; it is the reference for everything below. ## How it works, from the schema down **Syntax.** A tag followed by a string literal: `` sql`...` ``, `sql"..."`, or `sql'...'`. The tag is an ordinary qualified name and the string is an ordinary string literal, which now also accepts backticks; whitespace between them is allowed and the formatter removes it. A backtick string may span lines and has only two escapes, `` \` `` and `\\`, so everything else reaches the database as written. A backtick string is valid only after a tag. An unterminated backtick string stops before the next line that starts with `}`, so the rest of the file still parses. **One canonical body.** After escapes, the framework normalises line endings, trims a blank first and last line, removes common indentation, refuses NUL, and caps the body at 64 KiB. PSL and the TypeScript `sql` tag call the same function on the same raw text, so the two languages cannot disagree about a default. **Who registers tags.** Any pack may; only the target may register an unprefixed one. Postgres registers `sql` and `pg.sql`, SQLite registers `sql` and `sqlite.sql`, both through one implementation the SQL family exports. The registry sits beside the existing default-function registry on `ControlMutationDefaults`. Attribute parsing only checks that an argument is a tagged literal; lowering checks the tag is registered (the diagnostic lists the registered tags) and that the body canonicalizes. The language server completes the registered tags inside `@default(`. **Lowering.** The `sql` tag lowers to the function-kind column default the contract already has. The body is used verbatim: nothing rewrites it at authoring, in the contract, or in DDL. One body check, `checkSqlDefaultBody` in the SQL contract package, refuses `;`, comment markers, `$$`, and `SELECT`; authoring runs it with a source span and both planners run it before rendering. SQLite's planner previously had a weaker copy of that check. A body that is exactly `now()` or `autoincrement()` is refused, in PSL and TypeScript alike, with a hint to write the named function: those two texts are Prisma markers the planners render specially, so the SQL would not be used as written. Every other body, including `NOW()` and `gen_random_uuid()`, passes unchanged. **TypeScript.** The SQL contract builder exports `sql` (interpolation is a compile error and a runtime error), `now()`, and `autoincrement()`. `.defaultSql()` stays, marked deprecated with the replacement named, and is removed at 8.0.0 GA. Every call in this repository is rewritten. ## Behaviour changes to check - **List columns take storage defaults.** `` tags String[] @default(sql`'{}'::text[]`) `` and `tags String[] @default(now())` both lower. The interpreter cannot know any function's return type, so it no longer refuses functions on lists; the database reports a wrong type. Two things are still refused on a list: single-value client generators such as `uuid()`, and `autoincrement()`, which is a Prisma marker for a sequence-backed scalar column rather than SQL (`PSL_LIST_AUTOINCREMENT_UNSUPPORTED`). - **Both planners render the authored default.** Postgres previously rendered its normalised form, so `` sql`nextval('orders_seq'::regclass)` `` would have become a `SERIAL` column and `CURRENT_TIMESTAMP` would have become `now()`. Plan and verify on both targets compare through each target's introspection parser, so `` sql`CURRENT_TIMESTAMP` `` verifies clean and a second `db update` plans nothing. - **`ControlMutationDefaults.defaultLiteralTagRegistry` is required.** A pack that contributes default functions must now contribute a tag registry too, empty or not. The extension upgrade note shows the one-line fix. ## Where to start reviewing 1. `packages/1-framework/2-authoring/psl-parser/src/tokenizer.ts`, `parse.ts`, and `syntax/ast/expressions.ts`: backtick strings, their escapes, and the tagged-literal node built from a qualified name and a string literal. 2. `packages/1-framework/1-core/framework-components/src/shared/tagged-literal.ts`: the canonical body, with a table test per step. 3. `packages/2-sql/9-family/src/core/sql-default-literal-tag.ts` and both adapters' `control-mutation-defaults.ts`: registration and lowering. 4. `packages/2-sql/2-authoring/contract-ts/src/sql-default-literal.ts`: the TypeScript tag. 5. `test/integration/test/authoring/parity/default-sql-literal/`: PSL and TypeScript emitting the same contract. 6. `examples/prisma-8-demo/migrations/app/20260917T0818_add_post_expires_at/`: the DDL the planner renders for a `sql` default. ## Alternatives considered - **Named functions plus typed literals only, with anything else reported as a gap.** Rejected: a real default such as `(now() + '00:03:00'::interval)` would have no way to be written. - **Keep `dbgenerated` and document it.** Rejected: it was never accepted as a feature, and it keeps raw text in the language with no owner. - **Store raw defaults as a hashed payload, like index expressions and check constraints.** Not adopted here. A column default has no catalog name to carry a hash, so verify would still compare the database's reprint, and the contract shape would change for every consumer. Recorded in ADR 129's alternatives. - **A separate token and node for tagged literals.** Replaced during review: the node now reuses the qualified-name and string-literal parsing, so escaping and unterminated-string recovery live in one place. - **A named `gen_random_uuid()` default on Postgres.** Tried and removed during review. It read like Prisma's own `uuid()`, which generates the value in the client, while `gen_random_uuid()` makes the database generate it, and the names do not show that. Named defaults stay limited to `now()` and `autoincrement()`; database functions are written as `sql`. - **Rewrite known expressions at authoring time**, as the SQLite adapter did for `CURRENT_TIMESTAMP`. Rejected: a person who writes SQL expects that SQL to run. Refs: ADR 129, ADR 167, ADR 184. 🤖 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 SQL tagged literals for column defaults in PSL and TypeScript, including `sql`, `pg.sql`, and `sqlite.sql`. - Added `now()` and `autoincrement()` helpers for common defaults. - Added editor completions and formatting support for tagged literals. - SQL defaults now support multiline content with normalization and safety validation. - **Bug Fixes** - Migrations preserve authored SQL default expressions across PostgreSQL and SQLite. - **Documentation** - Added upgrade guidance and updated architecture, error-reference, and usage documentation. <!-- 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> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 3 天前 |