From 1665062a1ab12e3d81a50e5df80417ac1b2bdddf Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 21 Sep 2026 16:39:10 +0200 Subject: [PATCH 1/6] docs(orm): add the Migration API reference (C13) A new reference page, /orm/reference/migration-api, lists everything a migration.ts can call: the Migration class and the migration-file CLI, the four operation classes and the two checks, every PostgreSQL operation with the SQL it runs, the column and constraint helpers, dataTransform, rawSql, and the MongoDB operations. The site previously showed six of the thirty-odd methods. Every PostgreSQL operation was run against Postgres 17 with @prisma/orm-postgres 8.0.0-rc.11 and prisma 8.0.0-rc.15: 34 operations in one migration, plus a rawSql column rename verified by db verify. The SQL in the Runs columns is what those runs produced. Two findings from the run are recorded on the page: setDefault's defaultSql must include the DEFAULT keyword, and createTable's ifNotExists option has no effect, so it is not listed. Six reader-review rounds. Editing a migration links to the page. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../orm/migrations/editing-a-migration.mdx | 3 +- .../docs/content/docs/orm/reference/index.mdx | 1 + .../docs/content/docs/orm/reference/meta.json | 1 + .../docs/orm/reference/migration-api.mdx | 523 ++++++++++++++++++ apps/docs/cspell.json | 2 + 5 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 apps/docs/content/docs/orm/reference/migration-api.mdx diff --git a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx index 3c05534c84..3c74eb9b5b 100644 --- a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx @@ -241,7 +241,7 @@ If you would rather not keep an intermediate contract around, you can reach the ## Raw SQL [#escape-hatch-raw-sql] -When the statement you need has no method of its own, such as `COMMENT ON`, you write the SQL yourself. Import `rawSql` from `@prisma/orm-postgres/migration` and add the call to the `operations` array alongside the other operations. You also say what kind of change the statement is, by setting `operationClass` to one of the [four classes](/orm/migrations/how-migrations-work#every-operation-checks-itself): `additive`, `widening`, `destructive`, or `data`. `npx prisma db migrate` runs all four of them, and adds a data-loss warning for `destructive`. Give each operation its own `id`, because error messages name it and Prisma ORM does not check that it is unique, so two operations sharing an `id` leave you unable to tell which one an error is about. The `label` is the text the CLI prints. One limit matters before you write any SQL: one `npx prisma db migrate` run on PostgreSQL is [one transaction](/orm/migrations/applying-a-migration#when-something-goes-wrong), so `rawSql` cannot run `CREATE INDEX CONCURRENTLY`, or anything else that has to run outside a transaction. For an index, use `this.createIndex`, which is a plain `CREATE INDEX` and blocks writes while it builds. The example below is only an illustration, because it enables the `pgcrypto` PostgreSQL extension, which you would really do with `this.installExtension`: +When the statement you need has no method of its own, such as `COMMENT ON`, you write the SQL yourself. The [Migration API reference](/orm/reference/migration-api) lists every method that does exist, so check it first. Import `rawSql` from `@prisma/orm-postgres/migration` and add the call to the `operations` array alongside the other operations. You also say what kind of change the statement is, by setting `operationClass` to one of the [four classes](/orm/migrations/how-migrations-work#every-operation-checks-itself): `additive`, `widening`, `destructive`, or `data`. `npx prisma db migrate` runs all four of them, and adds a data-loss warning for `destructive`. Give each operation its own `id`, because error messages name it and Prisma ORM does not check that it is unique, so two operations sharing an `id` leave you unable to tell which one an error is about. The `label` is the text the CLI prints. One limit matters before you write any SQL: one `npx prisma db migrate` run on PostgreSQL is [one transaction](/orm/migrations/applying-a-migration#when-something-goes-wrong), so `rawSql` cannot run `CREATE INDEX CONCURRENTLY`, or anything else that has to run outside a transaction. For an index, use `this.createIndex`, which is a plain `CREATE INDEX` and blocks writes while it builds. The example below is only an illustration, because it enables the `pgcrypto` PostgreSQL extension, which you would really do with `this.installExtension`: ```ts rawSql({ @@ -321,4 +321,5 @@ Projects created with `npm create prisma@latest` include the [Prisma ORM skills] - [Generating a migration](/orm/migrations/generating-a-migration): where the first draft of `migration.ts` comes from - [Applying a migration](/orm/migrations/applying-a-migration): run the edited migration +- [Migration API](/orm/reference/migration-api): every method and helper `migration.ts` can call - [Data Migrations in Prisma 8](https://www.prisma.io/blog/data-migrations-in-prisma-next): how `dataTransform` was designed diff --git a/apps/docs/content/docs/orm/reference/index.mdx b/apps/docs/content/docs/orm/reference/index.mdx index 208090e5d7..8ee689ce98 100644 --- a/apps/docs/content/docs/orm/reference/index.mdx +++ b/apps/docs/content/docs/orm/reference/index.mdx @@ -48,4 +48,5 @@ const docs = await runtime.query(plan); }>Every MongoDB pipeline-builder stage, accumulator, expression helper, and write method. }>Raw SQL on PostgreSQL and raw commands on MongoDB, for queries the typed APIs cannot express. }>Connecting and disconnecting the client, transactions, prepared statements, and execution options. + }>Every method and helper a migration.ts can call, with the SQL each PostgreSQL operation runs, and the MongoDB operations. \ No newline at end of file diff --git a/apps/docs/content/docs/orm/reference/meta.json b/apps/docs/content/docs/orm/reference/meta.json index 6f0774462d..06f045f597 100644 --- a/apps/docs/content/docs/orm/reference/meta.json +++ b/apps/docs/content/docs/orm/reference/meta.json @@ -7,6 +7,7 @@ "pipeline-builder", "raw-queries", "transactions-and-runtime", + "migration-api", "error-reference" ] } diff --git a/apps/docs/content/docs/orm/reference/migration-api.mdx b/apps/docs/content/docs/orm/reference/migration-api.mdx new file mode 100644 index 0000000000..08c3a3e986 --- /dev/null +++ b/apps/docs/content/docs/orm/reference/migration-api.mdx @@ -0,0 +1,523 @@ +--- +title: Migration API +description: 'Every method and helper a migration.ts can use: the Migration class, the PostgreSQL operations, the column and constraint helpers, rawSql, and the MongoDB operations.' +url: /orm/reference/migration-api +metaTitle: Migration API reference | Prisma ORM +metaDescription: 'Reference for what a Prisma ORM migration.ts can call: the Migration class, every PostgreSQL operation with the SQL it runs, the col, primaryKey, foreignKey and unique helpers, rawSql, and the MongoDB operations.' +--- + +Each migration is its own folder inside `migrations/app/`, and the file you edit in it is `migration.ts`. `app` is the fixed name of the folder that holds your project's migrations; extension packages that ship their own migrations get folders of their own next to it. + +In Prisma ORM 8, `schema.prisma` is now `contract.prisma`, your contract: the same language and the same models. `npx prisma migration plan` writes `migration.ts` from a change to the contract, and saves a frozen copy of the contract for the migration to point at; when this page says a migration's start or end contract, it means one of those copies. You edit `migration.ts`, then run it with Node.js, and it writes `ops.json`, the SQL that will run, next to itself. [`npx prisma db migrate`](/cli/db-migrate) applies `ops.json` to the database. + +This page lists everything `migration.ts` can call. For the editing workflow, with a worked backfill, read [Editing a migration](/orm/migrations/editing-a-migration). + +On PostgreSQL, everything comes from one module: + +```ts +import { Migration, MigrationCLI, col, primaryKey, unique, foreignKey, checkExpression, lit, fn, rawSql, createExtension, placeholder } from '@prisma/orm-postgres/migration'; +``` + +`migration plan` writes the import line with only the names the migration needs. When you add a call by hand, add its name to the import line. On MongoDB the module is `@prisma/orm-mongo/target/migration`; the path really does contain `target`, and it is not a placeholder. See [MongoDB operations](#mongodb-operations). + +## The migration file + +`migration.ts` exports one class that extends `Migration`. This is what `migration plan` writes for a migration that adds one column: + +```ts title="migrations/app/20260921T1408_add_user_bio/migration.ts" +#!/usr/bin/env -S node +import type { Contract as End } from '../../snapshots/e3ff474101c6c1ce0b878352db859a50ba02f327d20945612f66631b2e425475/contract'; +import endContract from '../../snapshots/e3ff474101c6c1ce0b878352db859a50ba02f327d20945612f66631b2e425475/contract.json' with { type: 'json' }; +import type { Contract as Start } from '../../snapshots/f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5/contract'; +import startContract from '../../snapshots/f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5/contract.json' with { type: 'json' }; +import { Migration, MigrationCLI, col } from '@prisma/orm-postgres/migration'; + +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; + + override get operations() { + return [ + this.addColumn({ + schema: 'public', + table: 'user', + column: col('bio', 'text', { codecRef: { codecId: 'pg/text@1' } }), + }), + ]; + } +} + +MigrationCLI.run(import.meta.url, M); +``` + +The four imports at the top point at snapshots, which are copies of your contract that `migration plan` saves under `migrations/snapshots//`: one for the contract the migration starts from and one for the contract it ends at. You never type those paths; `migration plan` writes them. `codecRef` on the column names how its values are read and written; leave it as `migration plan` wrote it. The last line is what makes the file runnable with Node.js; keep it as written. + +| Member | What it is | +| --- | --- | +| `Migration` | The base class. `Start` and `End` are the two snapshots' types. | +| `startContractJson` | The start snapshot's `contract.json`. | +| `endContractJson` | The end snapshot's `contract.json`. | +| `operations` | The list of changes. They run in the order you list them, table changes and data changes alike, so a `dataTransform` between two column changes runs between them. Each entry is one call to a method or helper on this page. Return the list as it is; do not `await` the calls. | +| `this.endContract` | A read-only view of the end contract, for looking up names and settings in your own code. `this.endContract.namespace.public.table.user` is the `user` table in the `public` schema (`namespace` in that path means the PostgreSQL schema). | +| `this.startContract` | The same for the start contract, or `null` on a first migration. | + +A first migration starts from an empty database, so it has no start snapshot. `migration plan` writes it as `extends Migration`, without the two `Start` imports and without `startContractJson`. + +Running the file turns `operations` into `ops.json`, and also writes `migration.json`, which records the start and end contracts. Node.js 22.18 or later runs the `.ts` file directly, with no build step. Run it from your project root: it reads `prisma.config.ts` from the directory you run it in, to learn which database and extensions the project uses, and it never connects to a database. + +```bash +node migrations/app/20260921T1408_add_user_bio/migration.ts +``` + +```text +Wrote ops.json + migration.json to /path/to/my-app/migrations/app/20260921T1408_add_user_bio +``` + +| Flag | What it does | +| --- | --- | +| `--dry-run` | Prints `migration.json` and `ops.json` to the terminal instead of writing them. | +| `--config ` | Reads that config file instead of `./prisma.config.ts`. | +| `--help` | Prints the flags. | + +Any other flag fails with an error whose `code` is `CLI.UNKNOWN_FLAG`. + +## Operation classes and checks + +Every operation carries an `operationClass`, which says what kind of change it makes. The methods below set it for you, and `rawSql` asks you for it. The class changes nothing about what runs: `npx prisma db migrate` applies every class, and marks each destructive operation with `⚠` and a data-loss warning. + +| Class | Meaning | Example | +| --- | --- | --- | +| `additive` | Adds something new | Add a column, create an index | +| `widening` | Loosens a rule, or renames something without losing anything | Drop `NOT NULL`, rename an index | +| `destructive` | Removes or changes something that exists, and can lose data | Drop a column, change a column's type | +| `data` | Changes rows, not tables | A `dataTransform` | + +Most operations also carry two check queries, a precheck and a postcheck, and `db migrate` runs them in this order: + +1. The postcheck, which asks "is the change already there?". If it passes, the operation is skipped. This is what lets you run `db migrate` again after a run stopped partway: what already landed is skipped. +2. The precheck, which asks "is the database in the state this change expects?". If it fails, the run stops. +3. The statements. +4. The postcheck again. If it fails now, the run stops. + +The methods below write both checks for you; you write them yourself only in `rawSql`. [How migrations work](/orm/migrations/how-migrations-work#every-operation-checks-itself) has the full rule. + +## PostgreSQL operations + +Each operation is a method you call on the migration, `this.({ ... })`, with one options object. Every method takes `schema`. For `createSchema` it is the schema to create. For every other method it is the PostgreSQL schema the table is in, which is `public` unless your contract sets another. Names are quoted for you: pass `user`, not `"user"`. Runs is the SQL for this page's example names. + +### Schemas and tables + +| Method | Options | Runs | Class | +| --- | --- | --- | --- | +| `createSchema` | `schema` | `CREATE SCHEMA IF NOT EXISTS "app"` | additive | +| `createTable` | `schema`, `table`, `columns`, `constraints?` | `CREATE TABLE "public"."post" (...)` | additive | +| `dropTable` | `schema`, `table` | `DROP TABLE "public"."post"` | destructive | + +`columns` is a list of [`col(...)`](#column-and-constraint-helpers) calls and `constraints` a list of `primaryKey(...)`, `unique(...)`, `foreignKey(...)`, and `checkExpression(...)` calls, all rendered inside the `CREATE TABLE` statement. That section shows a full `createTable` call and the SQL it produces. + +### Columns + +| Method | Options | Runs | Class | +| --- | --- | --- | --- | +| `addColumn` | `schema`, `table`, `column` | `ALTER TABLE "public"."user" ADD COLUMN "bio" text` | additive | +| `dropColumn` | `schema`, `table`, `column` | `ALTER TABLE "public"."user" DROP COLUMN "bio"` | destructive | +| `alterColumnType` | `schema`, `table`, `column`, `options` | `ALTER TABLE "public"."post" ALTER COLUMN "views" TYPE bigint USING "views"::bigint` | destructive | +| `setNotNull` | `schema`, `table`, `column` | `ALTER TABLE "public"."post" ALTER COLUMN "views" SET NOT NULL` | destructive | +| `dropNotNull` | `schema`, `table`, `column` | `ALTER TABLE "public"."post" ALTER COLUMN "views" DROP NOT NULL` | widening | +| `setDefault` | `schema`, `table`, `column`, `defaultSql`, `operationClass?` | `ALTER TABLE "public"."user" ALTER COLUMN "name" SET DEFAULT 'anonymous'` | additive | +| `dropDefault` | `schema`, `table`, `column` | `ALTER TABLE "public"."user" ALTER COLUMN "name" DROP DEFAULT` | destructive | + +`addColumn` takes a `col(...)` call as `column`. The other methods take the column's name as a string. + +`alterColumnType` takes the new type in `options`, which has four fields: + +| Field | What to put in it | +| --- | --- | +| `qualifiedTargetType` | The new type, as it appears in the `ALTER COLUMN ... TYPE` statement: `'bigint'`, `'varchar(255)'`. | +| `formatTypeExpected` | The same type, spelled the way PostgreSQL itself names it. The postcheck compares the column's type after the change with this string, and the run stops if they differ. See the table below. | +| `rawTargetTypeForLabel` | The type as the CLI prints it in the operation's label. It affects nothing else, so use the same value as `qualifiedTargetType`. | +| `using` | Optional. The expression in the `USING` clause, for a conversion PostgreSQL cannot do on its own. Without it, the column is cast to the new type with `::`. | + +```ts +this.alterColumnType({ + schema: 'public', + table: 'post', + column: 'views', + options: { qualifiedTargetType: 'bigint', formatTypeExpected: 'bigint', rawTargetTypeForLabel: 'bigint' }, +}), +``` + +PostgreSQL spells most types the way you wrote them. These are the common ones it spells differently, as `\d post` in psql prints them: + +| You write | `formatTypeExpected` | +| --- | --- | +| `varchar(255)`, `varchar` | `character varying(255)`, `character varying` | +| `char(3)` | `character(3)` | +| `int4`, `int8`, `int2` | `integer`, `bigint`, `smallint` | +| `float8`, `float4` | `double precision`, `real` | +| `bool` | `boolean` | +| `timestamptz`, `timestamp` | `timestamp with time zone`, `timestamp without time zone` | +| `int[]` | `integer[]` | + +`text`, `integer`, `bigint`, `numeric(10,2)`, `jsonb`, `uuid`, `date`, `bytea`, and `citext` are spelled the same in both places. + +`setDefault` runs `SET` followed by whatever you pass as `defaultSql`, so include the keyword: `defaultSql: "DEFAULT 'anonymous'"`, or `defaultSql: 'DEFAULT now()'`. When the column already has a default and you are replacing it, set `operationClass: 'widening'`, which only changes how the CLI reports the operation. + +`setNotNull` has an extra precheck that fails while the column still holds a `NULL`, so backfill the column with a `dataTransform` first. [Editing a migration](/orm/migrations/editing-a-migration#worked-example-making-a-column-required) shows the three steps. + +### Constraints + +| Method | Options | Runs | Class | +| --- | --- | --- | --- | +| `addPrimaryKey` | `schema`, `table`, `constraint`, `columns` | `ALTER TABLE "public"."tag" ADD CONSTRAINT "tag_pkey" PRIMARY KEY ("id")` | additive | +| `addUnique` | `schema`, `table`, `constraint`, `columns` | `ALTER TABLE "public"."user" ADD CONSTRAINT "user_name_key" UNIQUE ("name")` | additive | +| `addForeignKey` | `schema`, `table`, `foreignKey` | `ALTER TABLE "public"."comment" ADD CONSTRAINT "comment_post_fkey" FOREIGN KEY ("postId") REFERENCES "public"."post" ("id") ON DELETE CASCADE` | additive | +| `dropConstraint` | `schema`, `table`, `constraint`, `kind?` | `ALTER TABLE "public"."user" DROP CONSTRAINT "user_name_key"` | destructive | +| `addCheckConstraint` | `schema`, `table`, `constraint`, `expression` | `ALTER TABLE "public"."post" ADD CONSTRAINT "post_title_len" CHECK (length("title") > 0)` | additive | +| `renameCheckConstraint` | `schema`, `table`, `from`, `to` | `ALTER TABLE "public"."post" RENAME CONSTRAINT "post_title_len" TO "post_title_nonempty"` | widening | +| `dropCheckConstraint` | `schema`, `table`, `constraint` | `ALTER TABLE "public"."post" DROP CONSTRAINT "post_title_nonempty"` | destructive | + +In these calls, `constraint` is the name to give the constraint, or the name of the one to drop, and `columns` is the list of column names it covers: + +```ts +this.addUnique({ schema: 'public', table: 'user', constraint: 'user_name_key', columns: ['name'] }), +``` + +`foreignKey` is an object: `name`, `columns` (on this table), `references` with its own `schema`, `table`, and `columns`, and optional `onDelete` and `onUpdate`, each one of `'noAction'`, `'restrict'`, `'cascade'`, `'setNull'`, or `'setDefault'`: + +```ts +this.addForeignKey({ + schema: 'public', + table: 'comment', + foreignKey: { + name: 'comment_post_fkey', + columns: ['postId'], + references: { schema: 'public', table: 'post', columns: ['id'] }, + onDelete: 'cascade', + }, +}), +``` + +`dropConstraint` removes a primary key, unique, or foreign key constraint by name. Pass its kind as `kind`, one of `'primaryKey'`, `'unique'`, or `'foreignKey'`. `kind` is recorded in `ops.json` for reporting only and does not change the SQL; it defaults to `'unique'`. A check constraint has its own `dropCheckConstraint`. + +`addCheckConstraint` adds a check to a table that already exists; `checkExpression(...)`, listed under [helpers](#column-and-constraint-helpers), is the same thing inside a `createTable`. In both, `expression` is SQL and goes into the statement as you wrote it, so quote column names yourself: `'length("title") > 0'`. + +There is no method that renames a table or a column. [Raw SQL](#raw-sql) shows a column rename; a table rename is the same shape around `ALTER TABLE ... RENAME TO`. + +### Indexes + +| Method | Options | Runs | Class | +| --- | --- | --- | --- | +| `createIndex` | `schema`, `table`, `index`, `columns` or `expression`, `extras?` | `CREATE INDEX "post_author_idx" ON "public"."post" ("authorId")` | additive | +| `renameIndex` | `schema`, `table`, `from`, `to` | `ALTER INDEX "public"."post_author_idx" RENAME TO "post_author_id_idx"` | widening | +| `dropIndex` | `schema`, `table`, `index` | `DROP INDEX "public"."post_author_id_idx"` | destructive | + +`createIndex` takes either `columns`, a list of column names, or `expression`, one SQL string that becomes everything between the parentheses, such as `'lower("title")'`. Everything else goes in `extras`: + +| Field | What it is | +| --- | --- | +| `unique` | `true` for `CREATE UNIQUE INDEX`. | +| `where` | The partial-index condition, as SQL, without the `WHERE` keyword. | +| `type` | The index method: `'gin'` renders `USING "gin"`. | +| `options` | Index storage parameters as an object: `{ fastupdate: false }` renders `WITH ("fastupdate" = off)`. | + +The call below runs `CREATE UNIQUE INDEX "post_title_lower_idx" ON "public"."post" (lower("title")) WHERE ("views" > 0)`: + +```ts +this.createIndex({ + schema: 'public', + table: 'post', + index: 'post_title_lower_idx', + expression: 'lower("title")', + extras: { unique: true, where: '"views" > 0' }, +}), +``` + +There is no `CONCURRENTLY` option, because one `npx prisma db migrate` run is [one transaction](/orm/migrations/applying-a-migration#when-something-goes-wrong), so the index is built inside it and blocks writes to the table until it is done. + +### Native enum types + +| Method | Options | Runs | Class | +| --- | --- | --- | --- | +| `createNativeEnumType` | `schema`, `typeName`, `members` | `CREATE TYPE "public"."role" AS ENUM ('admin', 'member')` | additive | +| `addNativeEnumValue` | `schema`, `typeName`, `value` | `ALTER TYPE "public"."role" ADD VALUE 'guest'` | additive | +| `dropNativeEnumType` | `schema`, `typeName` | `DROP TYPE "public"."role"` | destructive | + +`members` and `value` are the enum's values as strings, and `addNativeEnumValue` adds one value per call, so call it once for each value you add: + +```ts +this.createNativeEnumType({ schema: 'public', typeName: 'role', members: ['admin', 'member'] }), +this.addNativeEnumValue({ schema: 'public', typeName: 'role', value: 'guest' }), +``` + +### Row-level security + +| Method | Options | Runs | Class | +| --- | --- | --- | --- | +| `enableRowLevelSecurity` | `schema`, `table` | `ALTER TABLE "public"."post" ENABLE ROW LEVEL SECURITY` | additive | +| `disableRowLevelSecurity` | `schema`, `table` | `ALTER TABLE "public"."post" DISABLE ROW LEVEL SECURITY` | destructive | +| `createRlsPolicy` | `schema`, `table`, `policy` | `CREATE POLICY "post_read_all" ON "public"."post" AS PERMISSIVE FOR SELECT TO public USING (true)` | additive | +| `renameRlsPolicy` | `schema`, `table`, `from`, `to` | `ALTER POLICY "post_read_all" ON "public"."post" RENAME TO "post_read_any"` | widening | +| `dropRlsPolicy` | `schema`, `table`, `policy` | `DROP POLICY "post_read_any" ON "public"."post"` | destructive | + +`policy` describes the policy in full: + +```ts +this.createRlsPolicy({ + schema: 'public', + table: 'post', + policy: { + naming: { kind: 'exact', name: 'post_read_all' }, + tableName: 'post', + namespaceId: 'public', + operation: 'select', + roles: ['public'], + using: 'true', + permissive: true, + }, +}), +``` + +`naming.name` is the policy's name, and `kind` is always `'exact'`. `tableName` and `namespaceId` take the same values as `table` and `schema`. `operation` is one of `'select'`, `'insert'`, `'update'`, `'delete'`, or `'all'`. `roles` becomes the `TO` clause. `using` and `withCheck` are SQL conditions and go in as written; `SELECT` and `DELETE` policies take only `using`, `INSERT` policies only `withCheck`. `permissive: false` makes the policy `AS RESTRICTIVE`. `dropRlsPolicy` takes the policy's name as `policy`. + +### Extensions + +| Call | Options | Runs | Class | +| --- | --- | --- | --- | +| `createExtension(name)` | the extension's name | `CREATE EXTENSION IF NOT EXISTS "pgcrypto"` | additive | +| `this.installExtension` | `extensionName`, `invariantId`, `id`, `label?` | `CREATE EXTENSION IF NOT EXISTS citext` | additive | + +`createExtension` is a plain function, not a method, and is the one to use for an extension your migration needs, such as `pgcrypto` or `citext`. It goes in the `operations` list like any other call: + +```ts +override get operations() { + return [ + createExtension('pgcrypto'), + this.addColumn({ schema: 'public', table: 'user', column: col('token', 'text') }), + ]; +} +``` + +It has no precheck or postcheck, so if a `db migrate` run stops partway and you run it again, this operation runs again, which is harmless because of `IF NOT EXISTS`. + +`installExtension` is only for the first migration of an [extension package](/orm/extensions) that ships its own migrations, and there `extensionName`, `invariantId`, and `id` are all required. `id` names the operation in error messages, `label` is the text the CLI prints, and `invariantId` is a name other migrations in that package can refer to the install by. Only extension packages use `invariantId`; the optional `invariantId` on `dataTransform` and `rawSql` is for them too, so leave it out. + +### Data + +| Call | Arguments | Class | +| --- | --- | --- | +| `this.dataTransform(contract, name, options)` | `endContract`, the contract object built below; a name for the operation; and `options` with `run`, `check?`, and `invariantId?` | data | + +`run` is a callback that returns an update built with the [SQL query builder](/orm/reference/sql-query-builder), or a list of such callbacks that run in order. `check` is a callback that returns a query for the rows still needing the change. You write that one query; the precheck passes while it finds rows, and the postcheck passes once it finds none. + +Both callbacks build their queries with `db`, a query builder, and `db` needs a contract object built from the end snapshot's JSON. `migration plan` does not write those lines; you add them whenever a migration has a `dataTransform`. Pass the same contract object as the `contract` argument (not `this.endContract`, which is only a view for looking names up). The planned file already uses the name `endContract` for the JSON import, so first rename the import to free the name: + +```ts +import endContract from '../../snapshots//contract.json' with { type: 'json' }; // [!code --] +import endContractJson from '../../snapshots//contract.json' with { type: 'json' }; // [!code ++] +``` + +```ts + override readonly endContractJson = endContract; // [!code --] + override readonly endContractJson = endContractJson; // [!code ++] +``` + +Then add these lines above the class, copied as they are; they build `db` and never connect to a database: + +```ts +import postgresAdapter from '@prisma/orm-postgres/adapter/runtime'; +import { sql } from '@prisma/orm-postgres/builder/runtime'; +import { createExecutionContext, createSqlExecutionStack } from '@prisma/orm-postgres/family-runtime'; +import postgresTarget, { PostgresContractSerializer } from '@prisma/orm-postgres/target/runtime'; + +const endContract = new PostgresContractSerializer().deserializeContract(endContractJson); +const stack = createSqlExecutionStack({ target: postgresTarget, adapter: postgresAdapter }); + +const db = sql({ + context: createExecutionContext({ contract: endContract, stack }), + rawCodecInferer: stack.adapter.rawCodecInferer, +}); +``` + +Then the operation itself, from the worked example on Editing a migration. Inside each `where`, `f` holds the table's columns and `fns` the comparison functions: + +```ts +this.dataTransform(endContract, 'backfill-user-displayName', { + check: () => + db.public.user + .select('id') + .where((f, fns) => fns.eq(f.displayName, null)) + .limit(1), + run: () => + db.public.user + .update({ displayName: 'Anonymous' }) + .where((f, fns) => fns.eq(f.displayName, null)), +}), +``` + +`migration plan` writes a `dataTransform` with `placeholder(':check')` and `placeholder(':run')` in the two positions wherever a change needs your decision about existing rows; you replace each `placeholder(...)` call with a query. [Editing a migration](/orm/migrations/editing-a-migration) shows the whole file, and what to do when the backfill reads a column the migration removes. + +### Raw SQL + +| Call | Argument | Class | +| --- | --- | --- | +| `rawSql(operation)` | an operation object you write in full | the `operationClass` you set | + +Use `rawSql` for a statement that has no method on this page, such as a rename or `COMMENT ON`. `db migrate` checks the database against the end contract after applying a migration, so a raw change has to match a change in your contract: to rename a column, rename the field in `contract.prisma`, run `migration plan`, and replace the `dropColumn` and `addColumn` it writes with one `rawSql`. Pass it an object with these fields: + +| Field | What it is | +| --- | --- | +| `id` | A name for the operation, used in error messages. Any string; keep it unique within the migration. The planner's own look like `index.post.post_author_idx`. | +| `label` | The text the CLI prints while applying it. | +| `operationClass` | One of the [four classes](#operation-classes-and-checks). | +| `target` | Always `{ id: 'postgres' }`. | +| `precheck`, `execute`, `postcheck` | Lists of steps, each `{ description, sql, params? }`. `execute` holds the statements; the other two hold the checks, described below. | +| `summary?` | A longer description, optional. | +| `invariantId?` | Optional; leave it out. | + +```ts +rawSql({ + id: 'rename.user.bio', + label: 'Rename column "bio" to "biography" on "user"', + operationClass: 'widening', + target: { id: 'postgres' }, + precheck: [ + { + description: 'column "bio" exists', + sql: `SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'user' AND column_name = 'bio')`, + }, + ], + execute: [{ description: 'rename', sql: `ALTER TABLE "public"."user" RENAME COLUMN "bio" TO "biography"` }], + postcheck: [ + { + description: 'column "biography" exists', + sql: `SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'user' AND column_name = 'biography')`, + }, + ], +}), +``` + +#### Writing the checks + +A step's `sql` can use `$1`, `$2`, and so on, with the values in `params`. A check passes when its query returns `true` in the first column of its first row. It fails when the query returns `false` or no rows. + +Write the precheck so that `true` means "go ahead", as in "column `bio` exists" above. Write the postcheck so that `true` means "the change is there", as in "column `biography` exists". Remember the [order](#operation-classes-and-checks): the postcheck runs first, and if it already passes the operation is skipped. An empty `precheck` always lets the statements run. An empty `postcheck` never passes beforehand, so the operation runs on every `db migrate` that reaches it, including a second run after a failure. + +## Column and constraint helpers + +These are plain functions from the same module. `createTable` and `addColumn` take what they return. + +| Helper | Returns | +| --- | --- | +| `col(name, type, options?)` | A column. `type` is the PostgreSQL type as SQL, such as `'text'`, `'integer'`, `'timestamptz'`, or `'varchar(255)'`. `options` holds `notNull`, `primaryKey` (renders `PRIMARY KEY` on the column; for a single-column key it does the same as the `primaryKey(...)` constraint, which is what `migration plan` writes), `default`, and `codecRef`. | +| `lit(value)` | A literal default for `col`: `default: lit(0)` renders `DEFAULT 0`. | +| `fn(expression)` | A function default for `col`: `default: fn('now()')` renders `DEFAULT (now())`. | +| `primaryKey(columns, { name? })` | A `PRIMARY KEY` table constraint. | +| `unique(columns, { name? })` | A `UNIQUE` table constraint. | +| `foreignKey(columns, refTable, refColumns, { name?, onDelete?, onUpdate? })` | A `FOREIGN KEY` table constraint, with positional arguments; the `foreignKey` option of `this.addForeignKey` is a different shape, an object. `refTable` is a table in the same schema. To reference a table in another schema, use `this.addForeignKey` after the table exists. | +| `checkExpression(name, expression)` | A `CHECK` table constraint. `expression` is SQL, written as it should appear. | +| `placeholder(name)` | What `migration plan` leaves where you have to write a query. Running a migration that still contains one fails with an error whose `code` is `MIGRATION.UNFILLED_PLACEHOLDER`. | + +`migration plan` writes `codecRef` on every column it plans, such as `{ codecId: 'pg/text@1' }`, which names how the column's values are read and written. Leave it as written. There is no published list of these ids, so leave `codecRef` out of a column you write by hand: the column is created the same way, and both `npx prisma migration check`, which verifies the migration files, and `db migrate` accept it. + +```ts +this.createTable({ + schema: 'public', + table: 'post', + columns: [ + col('id', 'integer', { notNull: true }), + col('title', 'text', { notNull: true }), + col('authorId', 'integer', { notNull: true }), + col('views', 'integer', { notNull: true, default: lit(0) }), + col('createdAt', 'timestamptz', { notNull: true, default: fn('now()') }), + col('meta', 'jsonb'), + ], + constraints: [ + primaryKey(['id']), + unique(['title'], { name: 'post_title_key' }), + foreignKey(['authorId'], 'user', ['id'], { name: 'post_author_fkey', onDelete: 'cascade' }), + checkExpression('post_views_min', '"views" >= 0'), + ], +}), +``` + +```sql +CREATE TABLE "public"."post" ( + "id" integer NOT NULL, + "title" text NOT NULL, + "authorId" integer NOT NULL, + "views" integer DEFAULT 0 NOT NULL, + "createdAt" timestamptz DEFAULT (now()) NOT NULL, + "meta" jsonb, + PRIMARY KEY ("id"), + CONSTRAINT "post_title_key" UNIQUE ("title"), + CONSTRAINT "post_author_fkey" FOREIGN KEY ("authorId") REFERENCES "user" ("id") ON DELETE CASCADE, + CONSTRAINT "post_views_min" CHECK ("views" >= 0) +) +``` + +## MongoDB operations + +A MongoDB `migration.ts` has the same shape, and everything comes from one module: + +```ts +import { Migration, MigrationCLI, placeholder, createCollection, dropCollection, createIndex, dropIndex, setValidation, collMod, validatedCollection, dataTransform } from '@prisma/orm-mongo/target/migration'; +``` + +The operations are plain functions rather than methods, so `operations` returns calls such as `createCollection('products')`, not `this.createCollection(...)`. `this.endContract.collection.products` is the `products` collection in the end contract, with its `validator`. + +A data transform's `run` callback returns a query object with three fields: the `collection`, a `command` such as `RawUpdateManyCommand` from `@prisma/orm-mongo/query-ast/execution`, and `meta`, which carries `storageHash`, the hash of the end contract. This helper, from the [retail-store example](https://github.com/prisma/orm/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts), builds the update; `RawUpdateManyCommand` takes the collection name, a filter, and an update: + +```ts +function backfillRun(storageHash: string): MongoQueryPlan { + return { + collection: 'products', + command: new RawUpdateManyCommand( + 'products', + { status: { $exists: false } }, + { $set: { status: 'active' } }, + ), + meta: { target: 'mongo', storageHash, lane: 'mongo-raw' }, + }; +} +``` + +The check's `source` returns the same kind of object with an `AggregateCommand` that finds the documents still needing the change; the example's `existingProductsWithoutStatus` matches documents with no `status` and limits to one. The migration then uses both: + +```ts +override get operations() { + const storageHash = this.endContract.storage.storageHash; + const productsValidator = this.endContract.collection.products.validator; + return [ + setValidation('products', productsValidator.jsonSchema, { + validationLevel: productsValidator.validationLevel, + validationAction: productsValidator.validationAction, + }), + dataTransform('backfill-product-status', { + check: { source: () => existingProductsWithoutStatus(storageHash) }, + run: () => backfillRun(storageHash), + }), + ]; +} +``` + +| Function | Arguments | Class | +| --- | --- | --- | +| `createCollection(name, options?)` | `options`: `validator`, `validationLevel`, `validationAction`, `capped`, `size`, `max`, `timeseries` | additive | +| `dropCollection(name)` | | destructive | +| `createIndex(collection, keys, options?)` | `keys`: a list of `{ field, direction }`; `options`: `unique`, `sparse`, `name`, `expireAfterSeconds`, `partialFilterExpression`, `collation`, `weights`, `wildcardProjection` | additive | +| `dropIndex(collection, keys)` | the same `keys` the index was created with | destructive | +| `setValidation(collection, schema, options?)` | `schema`: a JSON Schema object; `options`: `validationLevel` (`'strict'` or `'moderate'`) and `validationAction` (`'error'` or `'warn'`) | destructive | +| `collMod(collection, options, meta?)` | `options`: `validator`, `validationLevel`, `validationAction`, `changeStreamPreAndPostImages: { enabled }`; `meta`: `id`, `label`, `operationClass` | destructive, unless you pass `meta: { operationClass: 'additive' }` | +| `validatedCollection(name, schema, indexes)` | creates the collection with `schema` as its validator and one index per `{ keys, unique? }` entry, with `keys` as in `createIndex`. Returns a list of operations, so spread it: `...validatedCollection(...)` | additive | +| `dataTransform(name, options)` | `options`: `run`, `check?`, `invariantId?` | data | + +`direction` is `1`, `-1`, `'text'`, `'2dsphere'`, `'2d'`, or `'hashed'`. + +`dataTransform` on MongoDB takes no contract argument, and its `check` is an object whose `source` is a callback returning a query for the documents that still need the change. Its other two fields, `filter` and `expect`, are optional and you can leave them out. [Editing a migration](/orm/migrations/editing-a-migration#the-same-pattern-on-mongodb) shows a filled-in example. + +## See also + +- [Editing a migration](/orm/migrations/editing-a-migration): filling in a planned migration, backfills, and raw SQL +- [How migrations work](/orm/migrations/how-migrations-work): what `ops.json` holds and how each operation checks itself +- [`migration plan`](/cli/migration-plan) and [`migration new`](/cli/migration-new): the commands that write `migration.ts` diff --git a/apps/docs/cspell.json b/apps/docs/cspell.json index bb27ef9bc0..a9930db88a 100644 --- a/apps/docs/cspell.json +++ b/apps/docs/cspell.json @@ -3,6 +3,8 @@ "version": "0.2", "language": "en", "words": [ + "dsphere", + "fastupdate", "migrationjson", "isnt", "recompiles", From 0a16cc3ff17512e1106cbcb912d32dbdb4c7118b Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 21 Sep 2026 16:59:17 +0200 Subject: [PATCH 2/6] docs(orm): complete the PSL and TypeScript contract references (C14) The two authoring pages now cover what ships but was undocumented. PSL: the @default generator list (uuid(7), cuid(2), ulid(), nanoid(n), dbgenerated), scalar lists and @noCheck, @@index with expression:, where:, unique:, type:, and name: versus map:, @@check, @@control with the four policies, namespace blocks, row-level security (@@rls, the policy_* and role blocks), @relation("Name") with onDelete/onUpdate values, the inline extension type form, the big-integer types, and MongoDB's @@index, @@unique, and @@textIndex arguments. TypeScript: the naming, foreignKeyDefaults, defaultControlPolicy, and namespaces options, the config output option, .many() and .noCheck(), index expression/where/type forms, checks and control on .sql(...), namespaces on a model, row-level security through entities, and the MongoDB builder's index options, collectionOptions, valueObject, field.vector(), and discriminator/base polymorphism. Every construct was emitted with @prisma/orm-postgres 8.0.0-rc.11 and @prisma/orm-mongo 8.0.0-rc.11 and the lowered contract.json inspected. One reader-review round on each page. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../orm/contract-authoring/psl-syntax.mdx | 168 +++++++++++++++++- .../typescript-schema-builder.mdx | 128 ++++++++++++- 2 files changed, 288 insertions(+), 8 deletions(-) diff --git a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx index fa9de1e3c6..a2fe02488c 100644 --- a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx @@ -6,7 +6,7 @@ metaTitle: Author the Prisma ORM contract in PSL metaDescription: 'Learn how to write a Prisma ORM contract in the Prisma schema language, including named types, enums, value objects, relations, and extension types.' --- -PSL, the Prisma Schema Language, is the preferred way to author [your contract](/orm/contract-authoring/the-data-contract), the `contract.prisma` file that replaced `schema.prisma`. You write one file, usually `src/prisma/contract.prisma`, and [`npx prisma contract emit`](/cli/contract-emit) writes `contract.json` and `contract.d.ts` beside it. If you know the Prisma schema language, most of a contract file reads exactly as you expect. Prisma ORM 8 differs in five places: +PSL, the Prisma Schema Language, is the preferred way to author [your contract](/orm/contract-authoring/the-data-contract), the `contract.prisma` file that replaced `schema.prisma`. You write one file, usually `src/prisma/contract.prisma`, and [`npx prisma contract emit`](/cli/contract-emit) writes `contract.json` and `contract.d.ts` beside it. If you know the Prisma schema language, most of a contract file reads exactly as you expect. The five biggest additions are listed here, and the rest of the page is the full reference for every attribute and block: - named types: give a database column type a name you can reuse on many fields. - enums: an enum can now say how its values are stored, and what each member stores. @@ -133,8 +133,12 @@ Models declare fields with a type, an optional `?` marker, and attributes. [Scal - `@id` marks the primary key. `@@id([a, b])` declares a composite key. - `@unique` adds a unique constraint on one field. `@@unique([userId, title])` adds one across several fields. - `@@index([...])` declares a secondary index. -- `@default(...)` sets a default. Database function defaults such as `@default(now())` become column defaults in the database. Generated defaults such as `@default(uuid())` come from Prisma ORM, and the database will not fill them in for you, so a row written by raw SQL or another application gets no value. +- `@default(...)` sets a default. Database defaults are a literal, `@default(now())`, `@default(autoincrement())`, or `@default(dbgenerated("nextval('user_serial_seq')"))`, whose string is any SQL expression; the database fills them in. Generated defaults are `uuid()` (a version 4 UUID, the same as `uuid(4)`), `uuid(7)`, `cuid(2)`, `ulid()`, and `nanoid()`, where `nanoid(21)` sets the length to any value from 2 to 255; Prisma ORM computes them when it writes the row, and the database will not fill them in for you, so a row written by raw SQL or another application gets no value. Prisma ORM 7's `cuid()` is rejected with a hint to use `cuid(2)`. - `@map("column_name")` sets a field's column name in the database. `@@map("table_name")` sets the table or collection name when it differs from the model name. +- `@@check(expression: "total >= 0", name: "order_total_positive")` adds a check constraint of your own to the table. See [Check constraints](#check-constraints). +- `@@control(observed)` says how far Prisma ORM manages the table. See [Control policy](#control-policy). + +A field can be a list on PostgreSQL: `tags String[]` is a `text[]` column, and the same works for `Int[]` and the other scalar types. Prisma ORM adds a check constraint to a list column so it cannot hold a `NULL` element. To leave that constraint out, for example when the column already exists without it, add `@noCheck(elementNotNull)` to the field. Lists of enums and of named types are not supported; a list of a value object is, as [Value objects](#value-objects) shows. `@updatedAt` is gone, so write `temporal.updatedAt()` where the field's type would go: @@ -162,6 +166,65 @@ model User { On PostgreSQL the primary key is an ordinary column, so pick its type and default yourself. On MongoDB the primary key is the document's `_id`, so type it `ObjectId` and map it to `_id`. +### Indexes + +`@@index([...])` takes a list of fields, and on PostgreSQL the named arguments below. Either the list or `expression:` is required, and an `expression:` index needs a `name:`: + +```prisma +model User { + @@index([name], where: "(name IS NOT NULL)", name: "user_name_active") + @@index(expression: "lower(handle)", unique: true, name: "user_handle_lower") + @@index([slug], type: "hash", name: "user_slug_hash") +} +``` + +- `expression:` is the whole index expression as SQL, in place of the field list. +- `where:` makes a partial index; it is the condition as SQL, without the `WHERE` keyword. +- `unique:` makes a unique index. `@@unique([...])` is a unique constraint, which is the usual way to say a value must be unique; use `unique:` on `@@index` when you also need `expression:` or `where:`. +- `type:` picks the index method, such as `"hash"` or `"gin"`. +- `name:` names the index. The name in the database is not exactly what you typed: it gets an eight-character hash on the end, `user_name_active_000a85d8`, and if you change `name:` later, the next `migration plan` renames the index instead of dropping and recreating it. `map:` instead sets the exact name, for an index that already exists in the database, which is what [`contract infer`](/cli/contract-infer) writes for indexes it finds. Give one or the other, not both. + +The `expression:` and `where:` strings go into the SQL as written, so they use column names, not field names (they differ when a field has `@map`), and you quote the names yourself. Prisma ORM does not check the SQL until the migration runs. On MongoDB, `@@index` takes different arguments; see [MongoDB indexes](#mongodb-indexes). + +### Check constraints + +`@@check` adds a check constraint you write yourself, on top of the ones Prisma ORM generates for enum and list columns: + +```prisma +model Order { + id Int @id @default(autoincrement()) + total Decimal + + @@check(expression: "total >= 0", name: "order_total_positive") +} +``` + +`expression:` is the condition as SQL, using column names, as on `@@index`. `name:` and `map:` work as they do on `@@index`: `name:` for a constraint you are adding, and `map:` for one that already exists in the database under that exact name. A model can carry any number of `@@check` attributes. PostgreSQL only. + +Prisma ORM also adds two check constraints of its own: one on every enum column, so it accepts only the enum's values, and one on every list column, so it rejects `NULL` elements. When one of them gets in your way, `@noCheck` on the field leaves both out, `@noCheck(membership)` leaves out only the enum check, and `@noCheck(elementNotNull)` only the list check. The field's TypeScript type does not change, so the database can then hold values the type does not describe. + +### Control policy + +`@@control(...)` says how far Prisma ORM manages the table, with one of four values: + +| Value | `db verify` | Migrations | +| --- | --- | --- | +| `managed` (the default) | The table must exist and match the model exactly. | Create, alter, and drop it. | +| `tolerated` | Declared columns must match; extra columns are accepted. | Create it if missing; never alter or drop it. | +| `external` | Declared columns must match; extra columns and constraints are ignored. | Never touch it. | +| `observed` | Anything goes; a mismatch is a warning, not a failure. | Never touch it. | + +Put it on a model whose table something else owns, such as an audit table another system writes: + +```prisma +model AuditLog { + id Int @id @default(autoincrement()) + message String + + @@control(observed) +} +``` + ## Named types The `types` block gives a database column type a name you can reuse on many fields: @@ -172,7 +235,15 @@ types { } ``` -Fields then use `ShortName` like any built-in type. The name keeps the column decision in one place: a `varchar(35)` column rather than `text`. A name is optional, and a field can use the native PostgreSQL type directly, such as `VarChar(35)`, `Uuid`, or `Timestamptz`. In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`, and the `@db.` attributes are gone. The `types` block is PostgreSQL only. +Fields then use `ShortName` like any built-in type. The name keeps the column decision in one place: a `varchar(35)` column rather than `text`. You do not have to name a type: a field can use the native PostgreSQL type directly, such as `VarChar(35)`, `Numeric(10, 2)`, `Uuid`, or `Timestamptz`. + +Three types cover big integers, and they differ in what your code receives: + +| Type | Column | In your code | +| --- | --- | --- | +| `BigInt` | `bigint` | a JavaScript `bigint` | +| `BigIntNumber` | `bigint` | a JavaScript `number`; a read or write outside the safe integer range fails | +| `UnboundedInt` | `numeric` | a JavaScript `bigint` of any size | In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`, and the `@db.` attributes are gone. The `types` block is PostgreSQL only. ## Enums @@ -242,7 +313,23 @@ model User { } ``` -Add `onDelete` and `onUpdate` to the same `@relation`: they belong on the side that holds the foreign key, not on the list side. For a one-to-one, make the other side singular instead of a list, so `User` declares `profile Profile?`, and put `@unique` on the foreign-key field: +Add `onDelete` and `onUpdate` to the same `@relation`: they belong on the side that holds the foreign key, not on the list side, and each takes `Cascade`, `Restrict`, `NoAction`, `SetNull`, or `SetDefault`: + +```prisma +model Post { + authorId Uuid + editorId Uuid? + author User @relation("Authored", fields: [authorId], references: [id], onDelete: Cascade) + editor User? @relation("Edited", fields: [editorId], references: [id], onDelete: SetNull) +} + +model User { + posts Post[] @relation("Authored") + edited Post[] @relation("Edited") +} +``` + +When two relations join the same two models, as `Authored` and `Edited` do here, give each pair the same `@relation("Name")` on both ends so Prisma ORM can tell which list belongs to which foreign key; otherwise `npx prisma contract emit` fails with an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`. For a one-to-one, make the other side singular instead of a list, so `User` declares `profile Profile?`, and put `@unique` on the foreign-key field: ```prisma model Profile { @@ -278,6 +365,77 @@ You then read `post.tags` as a list of `Tag`, without mentioning `PostTag` in th For which shape to choose and which side owns the foreign key, see [relational data modeling](/orm/data-modeling/relational-databases) and [MongoDB data modeling](/orm/data-modeling/mongodb). +## Namespaces + +A PostgreSQL schema other than `public` is a `namespace` block, and the models inside it get their tables there: + +```prisma +namespace audit { + model AuditLog { + id Int @id @default(autoincrement()) + message String + + @@map("audit_log") + } +} +``` + +Models outside any block are in `public`. In queries, the block name is the segment after `db.orm`, so this model is `db.orm.audit.AuditLog`. A relation can point at a model that another extension pack owns in a different schema, written `:.`, for example `supabase:auth.AuthUser`; the [Supabase extension](/orm/extensions) documents that form. + +## Row-level security + +Two block kinds and one attribute declare PostgreSQL row-level security, and [`migration plan`](/cli/migration-plan) turns them into `ENABLE ROW LEVEL SECURITY` and `CREATE POLICY` statements. `@@rls` on a model turns row-level security on for its table. A `policy_select`, `policy_insert`, `policy_update`, `policy_delete`, or `policy_all` block declares one policy for one operation, and a `role` block declares a database role a policy names: + +```prisma +model User { + id Uuid @id @default(uuid()) + + @@map("user") + @@rls +} + +namespace unbound { + role authenticated {} +} + +policy_select user_self_read { + target = User + roles = [authenticated] + using = "id = current_setting('app.user_id')::uuid" +} + +policy_update user_self_write { + target = User + roles = [authenticated] + using = "id = current_setting('app.user_id')::uuid" + withCheck = "id = current_setting('app.user_id')::uuid" + permissive = false +} +``` + +Policy and role blocks assign their settings with `=`, unlike model attributes. `target` names the model, which must carry `@@rls`; put the policy blocks inside the same `namespace` block as the model when it has one. `roles` lists bare role names. A role Prisma ORM should create needs a `role` block, whose braces stay empty; a role that already exists in the database, such as `public`, is written bare with no block: `roles = [public]`. `using` and `withCheck` are the two conditions as SQL, using column names: `policy_select` and `policy_delete` take `using`, `policy_insert` takes `withCheck`, and `policy_update` and `policy_all` take either or both. `permissive = false` makes the policy `AS RESTRICTIVE` in PostgreSQL's terms, so a row must pass it as well as the permissive policies. The block's name becomes the policy name with a hash on the end, as index names do. A `role` block must be inside `namespace unbound { }`; `unbound` is a reserved word meaning "not in any schema", which is where a role belongs. + +## MongoDB indexes + +On MongoDB, `@@index` and `@@unique` take a list of fields, each with an optional sort direction, plus MongoDB's own index options as named arguments, and `@@textIndex` declares a text index: + +```prisma +model Post { + @@index([authorId]) + @@index([createdAt(sort: Desc), authorId]) + @@index([expiresAt], expireAfterSeconds: 3600, sparse: true) + @@index([title], filter: "{ \"kind\": \"article\" }") + @@index([location], type: "2dsphere") + @@textIndex([title, body], weights: { "title": 10, "body": 1 }, language: "english") +} + +model User { + @@unique([email], collationLocale: "en", collationStrength: 2) +} +``` + +`sort: Asc` or `sort: Desc` on a field sets its direction; this form is MongoDB only. The keys in `weights` are field names, quoted. The named arguments are `type` (`"text"`, `"2dsphere"`, `"2d"`, or `"hashed"`), `sparse`, `expireAfterSeconds`, `filter` (a partial filter expression, written as a JSON string), and the collation options `collationLocale`, `collationStrength`, `collationCaseLevel`, `collationCaseFirst`, `collationNumericOrdering`, `collationAlternate`, `collationMaxVariable`, `collationBackwards`, and `collationNormalization`. `@@textIndex` takes `weights`, `language`, `languageOverride`, `filter`, and the same collation options. There is no `name:` on MongoDB; MongoDB names the index from its keys. + ## Base models and variants A base model declares a discriminator field, the field whose value says which variant a row is. Each variant names its base and its discriminator value: @@ -335,7 +493,7 @@ model Post { } ``` -The `pgvector` part of `pgvector.Vector(1536)` is a fixed name the pack declares, not the name you gave the import. List the pack before using its types, and run `npx prisma contract emit` again after changing the extension list. [Using extensions](/orm/extensions/using-extensions) covers installing a pack and names the packs you can add. +A field can also use the pack's type directly, with the argument named: `embedding pgvector.Vector(length: 1536)?`. The `pgvector` part of `pgvector.Vector(1536)` is a fixed name the pack declares, not the name you gave the import. List the pack before using its types, and run `npx prisma contract emit` again after changing the extension list. [Using extensions](/orm/extensions/using-extensions) covers installing a pack and names the packs you can add. ## Starting from an existing database diff --git a/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx b/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx index 5f4b1826ba..85976593a7 100644 --- a/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/typescript-schema-builder.mdx @@ -21,7 +21,7 @@ If neither applies, write PSL, which is more compact and is what [`contract infe For a new project, run [`npx prisma orm init`](/cli/orm-init), which asks how you want to write your schema, and choosing TypeScript creates the contract file and the config together. -The config's `contract` path names the one file Prisma ORM reads, and a `.ts` extension selects TypeScript authoring: +The config's `contract` path names the one file Prisma ORM reads, and a `.ts` extension selects TypeScript authoring. An optional `output` names a directory for `contract.json` and `contract.d.ts`, which otherwise land next to the contract file; `npm create prisma@latest` sets it to `./src/prisma/generated` for TypeScript projects: ```typescript title="prisma.config.ts" tab="PostgreSQL" import { definePrismaConfig } from "prisma/config"; @@ -30,6 +30,7 @@ import { defineConfig as ormConfig } from "@prisma/orm-postgres/config"; export default definePrismaConfig({ orm: ormConfig({ contract: "./src/prisma/contract.ts", + output: "./src/prisma/generated", }), }); ``` @@ -124,7 +125,16 @@ export const contract = defineContract({ models: { User, Post } }); ## How `defineContract` works -On PostgreSQL, `defineContract` takes an options object and then a function. You do not set a `provider` anywhere: importing `@prisma/orm-postgres/contract-builder` is what selects PostgreSQL. The options object lists the extension packs you use, and you write `{}` when you use none. The function returns the contract's content: `models`, plus `enums` and `types` if you have them. +On PostgreSQL, `defineContract` takes an options object and then a function. You do not set a `provider` anywhere: importing `@prisma/orm-postgres/contract-builder` is what selects PostgreSQL. The options object lists the extension packs you use, and you write `{}` when you use none. The function returns the contract's content: `models`, plus `enums`, `types`, and `entities` if you have them. + +The options object takes four more keys, all optional: + +| Option | What it does | +| --- | --- | +| `naming: { tables: "snake_case", columns: "snake_case" }` | Derives table and column names from model and field names, so `createdAt` becomes `created_at` without a `.column(...)` call on every field. Set either key or both. | +| `foreignKeyDefaults: { constraint: true, index: true }` | Gives every `rel.belongsTo` a foreign key constraint and an index in the database. Without this option a relation gets neither unless its own `.sql({ fk })` asks; see [Relations](#relations). | +| `defaultControlPolicy: "managed"` | The [control policy](#control-policy) for every model that does not set its own. | +| `namespaces: ["audit"]` | The PostgreSQL schemas other than `public` that models may use; see [Namespaces](#namespaces). | Take `field`, `model`, `rel`, and `type` from the function's one argument, and import `defineContract`, `enumType`, and `member` from the package. The package also exports `model` and `rel` for use outside the function, plus a `field` that has only `column`, `generated`, and `namedType`. @@ -138,6 +148,35 @@ On MongoDB, `defineContract` also accepts a single object holding `models`, as a Enums and extension packs work on MongoDB too: `enumType` and `member` come from `@prisma/orm-mongo/contract-builder`. Pass the enums in the same object as the models, as `defineContract({ models, enums })`, and extension packs go in the same `extensions` option. +The MongoDB builder also has, on the model object: + +- `indexes`, a list of `index(keys, options?)` calls. `keys` maps each field to `1`, `-1`, `"text"`, `"2dsphere"`, `"2d"`, or `"hashed"`, and `options` takes MongoDB's own index options: `unique`, `sparse`, `name`, `expireAfterSeconds`, `partialFilterExpression`, `collation`, `weights`, and `wildcardProjection`. For example, `index({ expiresAt: 1 }, { expireAfterSeconds: 3600, sparse: true })`. +- `collectionOptions`, for options on the collection itself, such as `{ collation: { locale: "en", strength: 2 } }`. +- `discriminator` and `base`, for one collection that holds more than one kind of document. The base model declares `discriminator: { field: "kind", variants: { Article: { value: "article" } } }`, and each variant model declares `base: Post` and the same `collection` as its base, plus its own fields. [MongoDB data modeling](/orm/data-modeling/mongodb#polymorphic-collections) covers when to use it. + +And two more field helpers: `field.vector()` for a vector, which takes no dimension count, and `field.valueObject(Address)` for an embedded document, where `Address` is declared with `valueObject("Address", { fields: { ... } })` from the package and returned in `defineContract`'s `valueObjects` map: + +```typescript +import { defineContract, field, index, model, valueObject } from "@prisma/orm-mongo/contract-builder"; + +const Address = valueObject("Address", { + fields: { street: field.string(), zip: field.string().optional() }, +}); + +const User = model("User", { + collection: "users", + fields: { + _id: field.objectId(), + email: field.string(), + address: field.valueObject(Address).optional(), + embedding: field.vector().optional(), + }, + indexes: [index({ email: 1 }, { unique: true, collation: { locale: "en", strength: 2 } })], +}); + +export const contract = defineContract({ models: { User }, valueObjects: { Address } }); +``` + The examples below use the PostgreSQL builder. ## Fields @@ -170,6 +209,8 @@ Every field builder supports chained modifiers: - `.default(value)` sets a literal default. `.defaultSql(expression)` sets a default the database computes, and the argument is a SQL expression as a string: `.defaultSql("now()")`. - `.unique()` adds a unique constraint. `.id()` marks the primary key, for a key that is not generated, such as an integer you set yourself. - `.column("column_name")` sets the column name in the database when it differs from the field name. +- `.many()` makes the field a list, stored as a PostgreSQL array column such as `text[]`. A list column gets a check constraint that rejects `NULL` elements. +- `.noCheck()` leaves out the check constraints Prisma ORM generates for the column: the one that keeps an enum column to the enum's values, and the one that keeps `NULL` out of a list. `.noCheck("membership")` and `.noCheck("elementNotNull")` leave out one or the other. The field's TypeScript type does not change. ## Enums @@ -240,7 +281,30 @@ Post.relations({ ... }).sql(({ cols, constraints }) => ({ })); ``` -`constraints.index` always takes a list of columns, even when the list has one entry. Add `{ unique: true }` to make it a unique index. +`constraints.index` takes a list of columns, even when the list has one entry, or an object with `expression`, the whole index expression as SQL. The options are `unique`, `where` (a partial-index condition as SQL, without the `WHERE` keyword), `type` with `options` (the index method and its parameters; `options: {}` when there are none), and `name` or `map`. `name: "user_handle_active"` creates an index called `user_handle_active_2a0c4277`, with a hash on the end; `map` sets the exact name, which is what you want when the index already exists. An `expression` index needs one of the two: + +```typescript +indexes: [ + constraints.index([cols.handle], { where: "(handle IS NOT NULL)", name: "user_handle_active" }), + constraints.index({ expression: "lower(handle)", unique: true, name: "user_handle_lower" }), + constraints.index([cols.tags], { type: "gin", options: {}, name: "user_tags_gin" }), +], +``` + +The `where` and `expression` strings go into the SQL as written, so they use column names, and you quote them yourself. + +Two more keys go in the same object, in either the object form or the callback form: `checks`, a list of check constraints you write yourself, built with `check` from the package, and `control`, the model's [control policy](#control-policy): + +```typescript +import { check } from "@prisma/orm-postgres/contract-builder"; + +Order.sql({ + table: "order", + checks: [check({ expression: "total >= 0", name: "order_total_positive" })], +}); +``` + +`expression` is the condition as SQL, using column names, and `name` and `map` work as they do on an index. `Model.refs` provides typed references to another model's fields, for the constraint builders inside `.sql(...)`. Write `constraints.foreignKey(cols.userId, User.refs.id)` and TypeScript checks `id` against the actual `User` definition. @@ -254,6 +318,64 @@ PostTag.attributes(({ fields, constraints }) => ({ That object accepts exactly two keys, `id` and `uniques`. `id` takes one constraint, and `uniques` takes a list, so `uniques: [constraints.unique([fields.postId, fields.tagId])]` makes a unique constraint across two fields. For a key made of one field, `.id()` on the field is enough, and the `field.id.*` helpers already do it. +## Namespaces + +A model can go in a PostgreSQL schema other than `public`: declare the schema in `defineContract`'s `namespaces` option, then name it on the model. Without the declaration, `npx prisma contract emit` fails and names the missing entry. + +```typescript +export const contract = defineContract({ namespaces: ["audit"] }, ({ field, model }) => { + const AuditLog = model("AuditLog", { + namespace: "audit", + fields: { id: field.id.uuidv4String(), message: field.text() }, + }); + return { models: { AuditLog: AuditLog.sql({ table: "audit_log" }) } }; +}); +``` + +## Control policy + +`control` on `.sql(...)` says how far Prisma ORM manages the table, with one of four values: + +| Value | `db verify` | Migrations | +| --- | --- | --- | +| `"managed"` (the default) | The table must exist and match the model exactly. | Create, alter, and drop it. | +| `"tolerated"` | Declared columns must match; extra columns are accepted. | Create it if missing; never alter or drop it. | +| `"external"` | Declared columns must match; extra columns and constraints are ignored. | Never touch it. | +| `"observed"` | Anything goes; a mismatch is a warning, not a failure. | Never touch it. | + +Put it on a model whose table something else owns: + +```typescript +AuditLog.sql({ table: "audit_log", control: "observed" }) +``` + +`defaultControlPolicy` in `defineContract`'s options sets the policy for every model that does not set its own. + +## Row-level security + +The package exports the pieces of PostgreSQL row-level security, and [`migration plan`](/cli/migration-plan) turns them into `ENABLE ROW LEVEL SECURITY` and `CREATE POLICY` statements. `rlsEnabled(Model)` turns row-level security on for the model's table. `policySelect`, `policyInsert`, `policyUpdate`, `policyDelete`, and `policyAll` each declare one policy for one operation, and `role("name")` declares a database role a policy names. Return them all in the `entities` list: + +```typescript +import { defineContract, policySelect, policyUpdate, rlsEnabled, role } from "@prisma/orm-postgres/contract-builder"; + +export const contract = defineContract({}, ({ field, model }) => { + const User = model("User", { fields: { id: field.id.uuidv4String() } }); + const authenticated = role("authenticated"); + + return { + models: { User: User.sql({ table: "user" }) }, + entities: [ + authenticated, + rlsEnabled(User), + policySelect(User, { name: "user_self_read", roles: [authenticated], using: "id = current_setting('app.user_id')::uuid" }), + policyUpdate(User, { name: "user_self_write", roles: [authenticated], using: "id = current_setting('app.user_id')::uuid", withCheck: "id = current_setting('app.user_id')::uuid" }), + ], + }; +}); +``` + +`roles` lists role handles, and a role Prisma ORM should create goes in `entities` as well, as `authenticated` does above, while a role that already exists in the database, such as `public`, is `role("public")` in `roles` and left out of `entities`. `using` and `withCheck` are the two conditions as SQL, using column names: `policySelect` and `policyDelete` take `using`, `policyInsert` takes `withCheck`, and `policyUpdate` and `policyAll` take either or both. The policy `name` gets an eight-character hash on the end in the database. + ## Extension types An extension pack is an npm package that adds column types to the builder. List packs in `defineContract`'s options object, and the `type` helper exposes their constructors: From 1b8e9ece6dba269f9aaa24954afddd72b8a6a2ae Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 21 Sep 2026 17:01:55 +0200 Subject: [PATCH 3/6] docs(cli): document the Vite plugin and the prebuild pattern for contract emit (C16) contract emit gains a "Run it automatically" section: the prismaVitePlugin export of @prisma/orm-postgres and @prisma/orm-mongo, its two options, what the dev server prints, and the prebuild script for every other bundler and for builds. The artifact page's version control section and the three Vite-based framework guides (React Router, SolidStart, TanStack Start) point at it. Verified with Vite 7.3.6 and @prisma/orm-postgres 8.0.0-rc.11: the plugin emitted on server start and again after a contract edit. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- apps/docs/content/docs/cli/contract-emit.mdx | 30 +++++++++++++++++++ .../docs/guides/frameworks/react-router-7.mdx | 2 +- .../docs/guides/frameworks/solid-start.mdx | 2 +- .../docs/guides/frameworks/tanstack-start.mdx | 2 +- .../the-contract-artifact.mdx | 2 +- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/docs/cli/contract-emit.mdx b/apps/docs/content/docs/cli/contract-emit.mdx index e3846d727a..c7b6943053 100644 --- a/apps/docs/content/docs/cli/contract-emit.mdx +++ b/apps/docs/content/docs/cli/contract-emit.mdx @@ -33,6 +33,36 @@ The command emits: Do not edit these files by hand. Re-run `contract emit` after changing the contract source or extension pack list. +## Run it automatically + +In development, a Vite plugin runs `contract emit` for you: once when the dev server starts, and again whenever the contract source or `prisma.config.ts` changes. It needs Vite 7 or 8, and comes with your database package, so there is nothing to install: + +```typescript title="vite.config.ts" +import { defineConfig } from "vite"; +import { prismaVitePlugin } from "@prisma/orm-postgres/vite-plugin-contract-emit"; + +export default defineConfig({ + plugins: [prismaVitePlugin()], +}); +``` + +On MongoDB, import it from `@prisma/orm-mongo/vite-plugin-contract-emit`. The plugin reads `prisma.config.ts` from the Vite root; pass another path as the first argument, `prismaVitePlugin("config/prisma.config.ts")`. A second argument takes `debounceMs` (how long to wait after a change before emitting, 150 by default) and `logLevel` (`"silent"`, `"info"`, or `"debug"`). The dev server prints one line per emit: + +```text +[prisma-vite-plugin-contract-emit] Emitted contract (storageHash: ab5014a0...) +``` + +An emit that fails shows in Vite's error overlay. The plugin does nothing in a production build, so builds and CI still run the command themselves. With any other bundler, or for a build, add the command as a `prebuild` script, which npm, pnpm, and yarn run before `build`: + +```json title="package.json (excerpt)" +{ + "scripts": { + "prebuild": "prisma contract emit", + "build": "next build" + } +} +``` + ## Examples ```npm diff --git a/apps/docs/content/docs/guides/frameworks/react-router-7.mdx b/apps/docs/content/docs/guides/frameworks/react-router-7.mdx index 613b30f52a..ba746c9e8c 100644 --- a/apps/docs/content/docs/guides/frameworks/react-router-7.mdx +++ b/apps/docs/content/docs/guides/frameworks/react-router-7.mdx @@ -558,7 +558,7 @@ Run [`npx prisma@latest init`](/cli/init) once to install the [Prisma ORM skills ## Next steps -- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`, or plan a checked-in migration with [`migration plan`](/cli/migration-plan). +- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`, or plan a checked-in migration with [`migration plan`](/cli/migration-plan). React Router builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you on every save. - [Learn the fundamentals](/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. - [Relations and joins](/orm/fundamentals/relations-and-joins) covers `include` in depth. - [Read the Prisma ORM overview](/orm) for the concepts behind contracts and typed queries. diff --git a/apps/docs/content/docs/guides/frameworks/solid-start.mdx b/apps/docs/content/docs/guides/frameworks/solid-start.mdx index 4cf1588729..ae352a7324 100644 --- a/apps/docs/content/docs/guides/frameworks/solid-start.mdx +++ b/apps/docs/content/docs/guides/frameworks/solid-start.mdx @@ -405,7 +405,7 @@ Prompts that map to this guide: ## Next steps -- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`. +- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`. SolidStart builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you on every save. - [Learn the fundamentals](/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. - [Read the Prisma ORM overview](/orm) for the concepts behind contracts and typed queries. - [SolidStart documentation](https://start.solidjs.com/) for routing, server functions, and deployment presets. diff --git a/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx b/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx index a070aee9b0..9b5c068bd2 100644 --- a/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx +++ b/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx @@ -115,6 +115,6 @@ Model access is namespace-qualified on PostgreSQL: `db.orm.public.User`. The [Pr ## Next steps -- Change the schema in `src/prisma/contract.prisma`, then run `npm run contract:emit` and `npm run db:update`. +- Change the schema in `src/prisma/contract.prisma`, then run `npm run contract:emit` and `npm run db:update`. TanStack Start builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you on every save. - [Learn the fundamentals](/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. - [Read the Prisma ORM overview](/orm) for the concepts behind contracts and typed queries. diff --git a/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx b/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx index df30ca00ec..eb8bdb9c56 100644 --- a/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx @@ -136,7 +136,7 @@ When `CONTRACT.MARKER_MISMATCH` shows up in production, apply the pending migrat ## Version control -Commit `contract.json` and `contract.d.ts` alongside the source: they hold structure only, no data and no credentials. Committing them lets teammates, CI, and deploys read the contract without running `npx prisma contract emit` first. Run `npx prisma contract emit` after every source change so the two files never trail the source. A CI job can check this in two lines: +Commit `contract.json` and `contract.d.ts` alongside the source: they hold structure only, no data and no credentials. Committing them lets teammates, CI, and deploys read the contract without running `npx prisma contract emit` first. Run `npx prisma contract emit` after every source change so the two files never trail the source; in a Vite project, the [Vite plugin](/cli/contract-emit#run-it-automatically) does that on every save. A CI job can check this in two lines: ```bash npx prisma contract emit From f04920803dbf5bd35de7d5e6aadd04044cc630d4 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 21 Sep 2026 17:14:30 +0200 Subject: [PATCH 4/6] docs(orm): address review on the reference completeness PR The big-integer table no longer swallows the paragraph after it. The Vite plugin pages say when it emits (contract or config changes while the dev server runs) instead of "on every save". Editing a migration points the pgcrypto example at createExtension, matching the reference. The MongoDB data-transform excerpt carries its imports and both helper functions, so it can be copied whole. The suggestion that an expression index refuses `map` was checked and is wrong: both PSL and the TypeScript builder emit an expression index with `map` on rc.11, so that wording stays. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- .../docs/guides/frameworks/react-router-7.mdx | 2 +- .../docs/guides/frameworks/solid-start.mdx | 2 +- .../docs/guides/frameworks/tanstack-start.mdx | 2 +- .../orm/contract-authoring/psl-syntax.mdx | 4 +++- .../the-contract-artifact.mdx | 2 +- .../orm/migrations/editing-a-migration.mdx | 2 +- .../docs/orm/reference/migration-api.mdx | 24 +++++++++++++++++-- 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/apps/docs/content/docs/guides/frameworks/react-router-7.mdx b/apps/docs/content/docs/guides/frameworks/react-router-7.mdx index ba746c9e8c..a029940b24 100644 --- a/apps/docs/content/docs/guides/frameworks/react-router-7.mdx +++ b/apps/docs/content/docs/guides/frameworks/react-router-7.mdx @@ -558,7 +558,7 @@ Run [`npx prisma@latest init`](/cli/init) once to install the [Prisma ORM skills ## Next steps -- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`, or plan a checked-in migration with [`migration plan`](/cli/migration-plan). React Router builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you on every save. +- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`, or plan a checked-in migration with [`migration plan`](/cli/migration-plan). React Router builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you whenever the contract changes while the dev server runs. - [Learn the fundamentals](/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. - [Relations and joins](/orm/fundamentals/relations-and-joins) covers `include` in depth. - [Read the Prisma ORM overview](/orm) for the concepts behind contracts and typed queries. diff --git a/apps/docs/content/docs/guides/frameworks/solid-start.mdx b/apps/docs/content/docs/guides/frameworks/solid-start.mdx index ae352a7324..22b3cd0128 100644 --- a/apps/docs/content/docs/guides/frameworks/solid-start.mdx +++ b/apps/docs/content/docs/guides/frameworks/solid-start.mdx @@ -405,7 +405,7 @@ Prompts that map to this guide: ## Next steps -- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`. SolidStart builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you on every save. +- Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`. SolidStart builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you whenever the contract changes while the dev server runs. - [Learn the fundamentals](/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. - [Read the Prisma ORM overview](/orm) for the concepts behind contracts and typed queries. - [SolidStart documentation](https://start.solidjs.com/) for routing, server functions, and deployment presets. diff --git a/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx b/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx index 9b5c068bd2..71bf08c375 100644 --- a/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx +++ b/apps/docs/content/docs/guides/frameworks/tanstack-start.mdx @@ -115,6 +115,6 @@ Model access is namespace-qualified on PostgreSQL: `db.orm.public.User`. The [Pr ## Next steps -- Change the schema in `src/prisma/contract.prisma`, then run `npm run contract:emit` and `npm run db:update`. TanStack Start builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you on every save. +- Change the schema in `src/prisma/contract.prisma`, then run `npm run contract:emit` and `npm run db:update`. TanStack Start builds on Vite, so the [Vite plugin](/cli/contract-emit#run-it-automatically) can run `contract emit` for you whenever the contract changes while the dev server runs. - [Learn the fundamentals](/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes. - [Read the Prisma ORM overview](/orm) for the concepts behind contracts and typed queries. diff --git a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx index a2fe02488c..e88edafad4 100644 --- a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx @@ -243,7 +243,9 @@ Three types cover big integers, and they differ in what your code receives: | --- | --- | --- | | `BigInt` | `bigint` | a JavaScript `bigint` | | `BigIntNumber` | `bigint` | a JavaScript `number`; a read or write outside the safe integer range fails | -| `UnboundedInt` | `numeric` | a JavaScript `bigint` of any size | In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`, and the `@db.` attributes are gone. The `types` block is PostgreSQL only. +| `UnboundedInt` | `numeric` | a JavaScript `bigint` of any size | + +In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`, and the `@db.` attributes are gone. The `types` block is PostgreSQL only. ## Enums diff --git a/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx b/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx index eb8bdb9c56..5a62786f01 100644 --- a/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/the-contract-artifact.mdx @@ -136,7 +136,7 @@ When `CONTRACT.MARKER_MISMATCH` shows up in production, apply the pending migrat ## Version control -Commit `contract.json` and `contract.d.ts` alongside the source: they hold structure only, no data and no credentials. Committing them lets teammates, CI, and deploys read the contract without running `npx prisma contract emit` first. Run `npx prisma contract emit` after every source change so the two files never trail the source; in a Vite project, the [Vite plugin](/cli/contract-emit#run-it-automatically) does that on every save. A CI job can check this in two lines: +Commit `contract.json` and `contract.d.ts` alongside the source: they hold structure only, no data and no credentials. Committing them lets teammates, CI, and deploys read the contract without running `npx prisma contract emit` first. Run `npx prisma contract emit` after every source change so the two files never trail the source; in a Vite project, the [Vite plugin](/cli/contract-emit#run-it-automatically) does that whenever the contract or `prisma.config.ts` changes while the dev server runs. A CI job can check this in two lines: ```bash npx prisma contract emit diff --git a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx index 3c74eb9b5b..1ac106600c 100644 --- a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx @@ -241,7 +241,7 @@ If you would rather not keep an intermediate contract around, you can reach the ## Raw SQL [#escape-hatch-raw-sql] -When the statement you need has no method of its own, such as `COMMENT ON`, you write the SQL yourself. The [Migration API reference](/orm/reference/migration-api) lists every method that does exist, so check it first. Import `rawSql` from `@prisma/orm-postgres/migration` and add the call to the `operations` array alongside the other operations. You also say what kind of change the statement is, by setting `operationClass` to one of the [four classes](/orm/migrations/how-migrations-work#every-operation-checks-itself): `additive`, `widening`, `destructive`, or `data`. `npx prisma db migrate` runs all four of them, and adds a data-loss warning for `destructive`. Give each operation its own `id`, because error messages name it and Prisma ORM does not check that it is unique, so two operations sharing an `id` leave you unable to tell which one an error is about. The `label` is the text the CLI prints. One limit matters before you write any SQL: one `npx prisma db migrate` run on PostgreSQL is [one transaction](/orm/migrations/applying-a-migration#when-something-goes-wrong), so `rawSql` cannot run `CREATE INDEX CONCURRENTLY`, or anything else that has to run outside a transaction. For an index, use `this.createIndex`, which is a plain `CREATE INDEX` and blocks writes while it builds. The example below is only an illustration, because it enables the `pgcrypto` PostgreSQL extension, which you would really do with `this.installExtension`: +When the statement you need has no method of its own, such as `COMMENT ON`, you write the SQL yourself. The [Migration API reference](/orm/reference/migration-api) lists every method that does exist, so check it first. Import `rawSql` from `@prisma/orm-postgres/migration` and add the call to the `operations` array alongside the other operations. You also say what kind of change the statement is, by setting `operationClass` to one of the [four classes](/orm/migrations/how-migrations-work#every-operation-checks-itself): `additive`, `widening`, `destructive`, or `data`. `npx prisma db migrate` runs all four of them, and adds a data-loss warning for `destructive`. Give each operation its own `id`, because error messages name it and Prisma ORM does not check that it is unique, so two operations sharing an `id` leave you unable to tell which one an error is about. The `label` is the text the CLI prints. One limit matters before you write any SQL: one `npx prisma db migrate` run on PostgreSQL is [one transaction](/orm/migrations/applying-a-migration#when-something-goes-wrong), so `rawSql` cannot run `CREATE INDEX CONCURRENTLY`, or anything else that has to run outside a transaction. For an index, use `this.createIndex`, which is a plain `CREATE INDEX` and blocks writes while it builds. The example below is only an illustration, because it enables the `pgcrypto` PostgreSQL extension, which you would really do with `createExtension('pgcrypto')` from the same module: ```ts rawSql({ diff --git a/apps/docs/content/docs/orm/reference/migration-api.mdx b/apps/docs/content/docs/orm/reference/migration-api.mdx index 08c3a3e986..438c7e7de7 100644 --- a/apps/docs/content/docs/orm/reference/migration-api.mdx +++ b/apps/docs/content/docs/orm/reference/migration-api.mdx @@ -466,9 +466,29 @@ import { Migration, MigrationCLI, placeholder, createCollection, dropCollection, The operations are plain functions rather than methods, so `operations` returns calls such as `createCollection('products')`, not `this.createCollection(...)`. `this.endContract.collection.products` is the `products` collection in the end contract, with its `validator`. -A data transform's `run` callback returns a query object with three fields: the `collection`, a `command` such as `RawUpdateManyCommand` from `@prisma/orm-mongo/query-ast/execution`, and `meta`, which carries `storageHash`, the hash of the end contract. This helper, from the [retail-store example](https://github.com/prisma/orm/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts), builds the update; `RawUpdateManyCommand` takes the collection name, a filter, and an update: +A data transform's `run` callback returns a query object with three fields: the `collection`, a `command` such as `RawUpdateManyCommand`, and `meta`, which carries `storageHash`, the hash of the end contract. The check's `source` returns the same kind of object with an `AggregateCommand`. These two helpers are from the [retail-store example](https://github.com/prisma/orm/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts); the first finds documents with no `status` and limits to one, and the second sets it, where `RawUpdateManyCommand` takes the collection name, a filter, and an update: ```ts +import { + AggregateCommand, + MongoExistsExpr, + MongoLimitStage, + MongoMatchStage, + type MongoQueryPlan, + RawUpdateManyCommand, +} from '@prisma/orm-mongo/query-ast/execution'; + +function existingProductsWithoutStatus(storageHash: string): MongoQueryPlan { + return { + collection: 'products', + command: new AggregateCommand('products', [ + new MongoMatchStage(new MongoExistsExpr('status', false)), + new MongoLimitStage(1), + ]), + meta: { target: 'mongo', storageHash, lane: 'mongo-pipeline' }, + }; +} + function backfillRun(storageHash: string): MongoQueryPlan { return { collection: 'products', @@ -482,7 +502,7 @@ function backfillRun(storageHash: string): MongoQueryPlan { } ``` -The check's `source` returns the same kind of object with an `AggregateCommand` that finds the documents still needing the change; the example's `existingProductsWithoutStatus` matches documents with no `status` and limits to one. The migration then uses both: +The migration's `operations` then uses both, alongside `setValidation` from `@prisma/orm-mongo/target/migration`: ```ts override get operations() { From 09adbf59c49cbc9325f78688feff8f5edbb16275 Mon Sep 17 00:00:00 2001 From: "reviewer (Program)" Date: Mon, 21 Sep 2026 15:18:13 +0000 Subject: [PATCH 5/6] docs(docs): list the remaining MongoDB migration options on the Migration API page createCollection also takes collation, changeStreamPreAndPostImages, and clusteredIndex, and createIndex also takes default_language and language_override, per the @prisma/orm-mongo 8.0.0-rc.11 types. --- apps/docs/content/docs/orm/reference/migration-api.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/docs/orm/reference/migration-api.mdx b/apps/docs/content/docs/orm/reference/migration-api.mdx index 438c7e7de7..5fb87df3d9 100644 --- a/apps/docs/content/docs/orm/reference/migration-api.mdx +++ b/apps/docs/content/docs/orm/reference/migration-api.mdx @@ -523,9 +523,9 @@ override get operations() { | Function | Arguments | Class | | --- | --- | --- | -| `createCollection(name, options?)` | `options`: `validator`, `validationLevel`, `validationAction`, `capped`, `size`, `max`, `timeseries` | additive | +| `createCollection(name, options?)` | `options`: `validator`, `validationLevel`, `validationAction`, `capped`, `size`, `max`, `timeseries`, `collation`, `changeStreamPreAndPostImages`, `clusteredIndex` | additive | | `dropCollection(name)` | | destructive | -| `createIndex(collection, keys, options?)` | `keys`: a list of `{ field, direction }`; `options`: `unique`, `sparse`, `name`, `expireAfterSeconds`, `partialFilterExpression`, `collation`, `weights`, `wildcardProjection` | additive | +| `createIndex(collection, keys, options?)` | `keys`: a list of `{ field, direction }`; `options`: `unique`, `sparse`, `name`, `expireAfterSeconds`, `partialFilterExpression`, `collation`, `weights`, `default_language`, `language_override`, `wildcardProjection` | additive | | `dropIndex(collection, keys)` | the same `keys` the index was created with | destructive | | `setValidation(collection, schema, options?)` | `schema`: a JSON Schema object; `options`: `validationLevel` (`'strict'` or `'moderate'`) and `validationAction` (`'error'` or `'warn'`) | destructive | | `collMod(collection, options, meta?)` | `options`: `validator`, `validationLevel`, `validationAction`, `changeStreamPreAndPostImages: { enabled }`; `meta`: `id`, `label`, `operationClass` | destructive, unless you pass `meta: { operationClass: 'additive' }` | From 682f9a8a3364a1487dd9415c74707ae3997c2935 Mon Sep 17 00:00:00 2001 From: "reviewer (Program)" Date: Mon, 21 Sep 2026 17:30:29 +0000 Subject: [PATCH 6/6] docs(orm): list the wildcard and text-index arguments of MongoDB @@index on the PSL page The "named arguments are" list for @@index and @@unique on MongoDB left out four that @prisma/orm-mongo 8.0.0-rc.11 accepts: default_language and languageOverride for a type: "text" index, and include or exclude for a wildcard index. The wildcard() field element, which those two narrow, was not on the page either. Adds them, with the rules the lowering enforces: one wildcard() per index, not on @@unique, and not with expireAfterSeconds or a type: such as "hashed". --- apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx index e88edafad4..b0eac04486 100644 --- a/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx +++ b/apps/docs/content/docs/orm/contract-authoring/psl-syntax.mdx @@ -428,6 +428,7 @@ model Post { @@index([expiresAt], expireAfterSeconds: 3600, sparse: true) @@index([title], filter: "{ \"kind\": \"article\" }") @@index([location], type: "2dsphere") + @@index([wildcard(meta)], exclude: ["meta.internal"]) @@textIndex([title, body], weights: { "title": 10, "body": 1 }, language: "english") } @@ -436,7 +437,9 @@ model User { } ``` -`sort: Asc` or `sort: Desc` on a field sets its direction; this form is MongoDB only. The keys in `weights` are field names, quoted. The named arguments are `type` (`"text"`, `"2dsphere"`, `"2d"`, or `"hashed"`), `sparse`, `expireAfterSeconds`, `filter` (a partial filter expression, written as a JSON string), and the collation options `collationLocale`, `collationStrength`, `collationCaseLevel`, `collationCaseFirst`, `collationNumericOrdering`, `collationAlternate`, `collationMaxVariable`, `collationBackwards`, and `collationNormalization`. `@@textIndex` takes `weights`, `language`, `languageOverride`, `filter`, and the same collation options. There is no `name:` on MongoDB; MongoDB names the index from its keys. +`sort: Asc` or `sort: Desc` on a field sets its direction; this form is MongoDB only. The keys in `weights` are field names, quoted. The named arguments are `type` (`"text"`, `"2dsphere"`, `"2d"`, or `"hashed"`), `sparse`, `expireAfterSeconds`, `filter` (a partial filter expression, written as a JSON string), `default_language` and `languageOverride` for a `type: "text"` index, `include` or `exclude` for a wildcard index, and the collation options `collationLocale`, `collationStrength`, `collationCaseLevel`, `collationCaseFirst`, `collationNumericOrdering`, `collationAlternate`, `collationMaxVariable`, `collationBackwards`, and `collationNormalization`. `@@textIndex` takes `weights`, `language`, `languageOverride`, `filter`, and the same collation options. There is no `name:` on MongoDB; MongoDB names the index from its keys. + +A wildcard index covers every field under a path, which is how you index documents whose keys you do not know in advance. Write `wildcard()` in the field list to cover the whole document, or `wildcard(meta)` to cover the fields under `meta`; the key becomes `$**` or `meta.$**`. `include` and `exclude` are lists of field names, quoted, that narrow what the index covers; give one or the other, and only with `wildcard()`. An index can hold one `wildcard()`, and it cannot be an `@@unique`, take `expireAfterSeconds`, or set a `type:` such as `"hashed"`. ## Base models and variants