From f89116a688903e0f392c29dfd70b566f078486f4 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 15 Sep 2026 16:58:49 +0200 Subject: [PATCH 01/13] docs(orm): migrations banned-term pass and fact check against rc.11 Adds the section on backfills that read a column the migration removes (intermediate contracts). Co-Authored-By: Claude Opus 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../orm/migrations/applying-a-migration.mdx | 112 ++++++------ .../orm/migrations/editing-a-migration.mdx | 118 ++++++++++--- .../orm/migrations/generating-a-migration.mdx | 159 +++++++++--------- .../orm/migrations/how-migrations-work.mdx | 70 ++++---- .../orm/migrations/rollbacks-and-recovery.mdx | 78 +++++---- .../orm/migrations/the-migration-graph.mdx | 128 +++++++------- apps/docs/cspell.json | 1 + 7 files changed, 363 insertions(+), 303 deletions(-) diff --git a/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx b/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx index f17cc31e3e4..89cd3bb86eb 100644 --- a/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx @@ -1,40 +1,42 @@ --- title: Applying a migration -description: 'The db migrate command walks the graph from where your database is to where you want it, with a preview, a checkpoint at every step, and safe retries.' +description: 'The db migrate command moves your database from where it is to where you want it, with a preview, checks on every operation, and safe re-runs.' url: /orm/migrations/applying-a-migration metaTitle: Applying a migration in Prisma ORM metaDescription: 'How the Prisma ORM db migrate command applies migrations, from targeting refs and hashes and previewing with --show to checking status and running safely in development and production.' --- -Applying is the one step that touches a database, via a single command: +Applying is the one step that changes a database, and it is a single command: ```npm -npx prisma@latest db migrate +npx prisma db migrate ``` -Note that the command is `db migrate`, not `migration apply`: the `migration ...` subcommands manage files on disk, while `db migrate` moves a database. It reads where the database currently is (its **marker**), finds a path through the [migration graph](/orm/migrations/the-migration-graph) to the target, and applies each migration along the path, running every operation's precheck, execute, and postcheck as it goes. +The command is `db migrate`, not `migration apply`, because the `migration ...` commands never change a database. `db migrate` reads the database's **marker**, the record in the database of which contract state it currently matches. A contract state is one version of your contract, the `contract.prisma` file that replaced `schema.prisma`. `db migrate` then finds a path through your [migration history](/orm/migrations/the-migration-graph) to the target and applies each migration on that path, running every operation's [precheck, execute, and postcheck](/orm/migrations/how-migrations-work#every-operation-checks-itself) steps. By default, the connection comes from `db.connection` in `prisma.config.ts`. Pass `--db` to override it: ```npm -npx prisma@latest db migrate --db $DATABASE_URL +npx prisma db migrate --db $DATABASE_URL ``` -A successful run reports every operation it performed and the marker that it left behind: +A successful run reports what it applied and the marker it left behind: ```text -✔ Applied 3 operation(s) across 1 contract space +✔ Applied 1 migration(s) (3 operation(s)) across 1 contract space(s) App space ├─ Add column "displayName" to "user" ├─ Data transform: backfill-user-displayName - └─ Set NOT NULL on "user"."displayName" (destructive) - marker: e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63 + ├─ Set NOT NULL on "user"."displayName" + └─ marker e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63 + +⚠ This migration contains destructive operations that may cause data loss. Next: prisma migration status ``` -`App space` is your application's migration lane, one of the run's *contract spaces*. Projects that use database extensions gain additional spaces; this is covered below in [Extension spaces](#extension-spaces). +`App space` is your app's contract space. A contract space is a separate migration history: one for your app, and one for each database extension that ships its own migrations. See [Extension spaces](#extension-spaces). ## Check before, preview, then apply @@ -42,66 +44,64 @@ When working on shared databases, it helps to always follow these three steps: ```npm # 1. Where is the database, what's pending? -npx prisma@latest migration status --db $DATABASE_URL +npx prisma migration status --db $DATABASE_URL # 2. What exactly would run? -npx prisma@latest db migrate --show --db $DATABASE_URL +npx prisma db migrate --show --db $DATABASE_URL # 3. Run it. -npx prisma@latest db migrate --db $DATABASE_URL +npx prisma db migrate --db $DATABASE_URL ``` -`migration status` draws the path between the database's marker and the target, flagging each migration as applied or pending: +`migration status` draws your migration history and marks each migration as applied or pending: ```text -* 925198f @contract -|^ 20260707T1006_add_user_phone 705b1a6 -> 925198f 1 ops > pending -* 705b1a6 @db (db) -|^ 20260707T1005_init - -> 705b1a6 2 ops + applied -* - +○ 925198f @contract +│↑ 20260707T1006_add_user_phone 705b1a6 → 925198f 1 ops ⧗ pending +○ 705b1a6 @db (db) +│↑ 20260707T1005_init ∅ → 705b1a6 2 ops ✓ applied +○ ∅ 1 pending — run `prisma db migrate --to 925198f3cc27` ``` -Read the markers on the right: `@db` is where the database is, `@contract` is where your emitted contract is, and `(db)` is the [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) of that name pointing at the same node. +Read the labels on the right. `@db` is the database's marker, and `@contract` is your contract as of the last `npx prisma contract emit`. `(db)` is the [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) named `db`, which points at the same contract state. A ref is a name for a contract state, stored as a file in `migrations/app/refs/`. -`db migrate --show` is the read-only dry run: it draws the path from the database's position to the target and stops. Nothing touches the database: +`db migrate --show` is a read-only preview. It draws your migration history, marks each migration on the path with `will run`, and lists those migrations in the order they run: ```text +○ 925198f @contract │↑ 20260707T1006_add_user_phone 705b1a6 → 925198f ↑ will run -○ 705b1a6 -│↑ 20260707T1005_init ∅ → 705b1a6 ↑ will run -○ ∅ @db +○ 705b1a6 @db (db) +│↑ 20260707T1005_init ∅ → 705b1a6 +○ ∅ -The following 2 migrations will run: - 20260707T1005_init ∅ → 705b1a6 +The following 1 migration will run: 20260707T1006_add_user_phone 705b1a6 → 925198f ``` -After applying, `migration log` shows the database's own record of what ran: an append-only **ledger** the runner writes alongside the marker: +After applying, `migration log` prints the **ledger**, the database's own list of every migration applied to it: ```text - Applied at Migration Change Ops ----------------------- ------------------------------- -------------------- ------ - 2026-07-07 10:05:32Z 20260707T1005_init - -> 705b1a6 2 ops - 2026-07-07 10:09:55Z 20260707T1006_add_user_phone 705b1a6 -> 925198f 1 ops + Applied at Migration Change Ops +──────────────────────────── ────────────────────────────── ─────────────────── ─────── + 2026-07-07 10:05:32 +00:00 20260707T1005_init ∅ → 705b1a6 2 ops + 2026-07-07 10:09:55 +00:00 20260707T1006_add_user_phone 705b1a6 → 925198f 1 ops ``` ## Choosing a target -With no `--to`, `db migrate` advances toward your emitted contract. To aim somewhere specific, name the target with `--to`, for example: +With no `--to`, `db migrate` moves the database to your contract as of the last `npx prisma contract emit`. To aim somewhere else, name the target with `--to`: ```npm -npx prisma@latest db migrate --to prod --db $DATABASE_URL +npx prisma db migrate --to prod --db $DATABASE_URL ``` -If the graph has branched and more than one tip is reachable, `db migrate` stops and asks for an explicit `--to`. That's the graph protecting you: two feature branches may both be valid futures, and picking one is a human decision. - -`--advance-ref` moves a named ref to the post-apply state in the same step, which is what keeps [`migration plan`](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) incremental. Plain `db migrate` never moves a ref, which is deliberate: a deploy or a CI apply should not move a ref that lives in the repository, so in development you pass `--advance-ref db` and in production you leave it off. The [`db migrate` reference](/cli/db-migrate#options) has the full `--to` grammar (refs, hashes, migration names, `^`) and the flag contract. +`db migrate` always moves toward one target, so a branched migration history does not stop it. When more than one path leads to the target, it picks one, and `db migrate --show` prints the path it will take. The [`db migrate` reference](/cli/db-migrate#options) lists its other options. ## When something goes wrong -As soon as an operation fails, the runner will stop, alert you to what caused the problem and why, as well as what to do to resolve the problem: +When an operation fails, the runner stops and prints what failed, why, and how to fix it: ```text ✖ Operation pgvector.install-vector-extension failed during execution: create extension "vector" (MIGRATION.RUNNER_FAILED) @@ -109,45 +109,45 @@ As soon as an operation fails, the runner will stop, alert you to what caused th Fix: Fix the issue and re-run `prisma db migrate --to ` — previously applied migrations are preserved. ``` -Three properties make failure manageable instead of something to fear: - -- **On PostgreSQL, a failed run leaves nothing behind.** The entire `db migrate` run executes inside one transaction, so when an operation fails, everything from that run rolls back and the database is exactly where it was before you started. Migrations applied in *earlier* runs are untouched. That's what "previously applied migrations are preserved" means. -- **The error is specific.** It names the operation, the phase (precheck, execute, or postcheck), and the check that failed, which is enough to fix the cause without spelunking. -- **Re-running is safe.** Operations are idempotent: before running one, the runner evaluates its postcheck and skips it if the database already satisfies it. A change made outside of migrations doesn't break the run. The runner skips the operation instead. On MongoDB, where cross-collection transactions don't exist, this same mechanism is what makes a partially-applied run converge on retry. See [Rollbacks and recovery](/orm/migrations/rollbacks-and-recovery) for the full failure playbook. +Four things make a failure easy to recover from: -Before running any DDL, `db migrate` also verifies the database's marker is a state the graph knows. A database that was changed outside of migrations fails fast with a marker mismatch instead of getting SQL applied on top of unknown drift. +- **On PostgreSQL, a failed run leaves nothing behind.** The whole `db migrate` run, across every contract space, is one transaction. When an operation fails, everything from that run is rolled back and the database is where it was before you started. Migrations applied in *earlier* runs are untouched, which is what "previously applied migrations are preserved" means. +- **The error is specific.** It names the operation, the step that failed (precheck, execute, or postcheck), and that step's description, which is usually enough to find the cause. +- **Re-running is safe.** Before it runs an operation, the runner evaluates the operation's postcheck and skips the operation if the postcheck already holds. An operation whose work is already done, by an earlier run or by hand, is skipped instead of failing. On MongoDB the runner uses no transaction, so this check is what lets a partly applied run finish when you re-run it. See [Rollbacks and recovery](/orm/migrations/rollbacks-and-recovery) for how to recover from a failure. +- **An unknown database state stops the run.** Before it runs any operation, `db migrate` checks that the database's marker is a contract state in your migration history. If it is not, for example because another tool migrated the database, the run fails with an error whose `code` is `MIGRATION.MARKER_MISMATCH` and no operation runs. ## Development vs. production The commands are the same everywhere: what changes is where the files come from and who runs them. -**In development**, you're the one planning and editing, and you apply immediately: +**In development**, you plan, edit, and apply immediately. Add `--advance-ref db` so the next [`migration plan`](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) starts from what you just applied: ```npm -npx prisma@latest migration plan --name my_change && npx prisma@latest db migrate --advance-ref db +npx prisma migration plan --name my_change && npx prisma db migrate --advance-ref db ``` -**In CI and production**, migrations arrive via your repo, already planned, reviewed, and merged. That's why the [editing rule](/orm/migrations/editing-a-migration) matters: commit `migration.ts` and `ops.json` together, and run `migration check` to catch a stale recompile before deployment. See [Generating a migration](/orm/migrations/generating-a-migration#reviewing-what-you-planned) for its exit codes. The deploy step is: +**In CI and production**, migrations arrive via your repo, already planned, reviewed, and merged. That's why the [editing rule](/orm/migrations/editing-a-migration) matters: commit `migration.ts` and `ops.json` together. Run `migration check` before deployment to catch an `ops.json` or `migration.json` that was edited by hand. It does not read `migration.ts`, so it cannot tell whether you ran `node migration.ts` after your last edit. See [Generating a migration](/orm/migrations/generating-a-migration#reviewing-what-you-planned) for its exit codes. The deploy step is: ```npm -npx prisma@latest migration check # files intact, graph well-formed (offline) -npx prisma@latest db migrate --show --db $DATABASE_URL # log what's about to run -npx prisma@latest db migrate --db $DATABASE_URL +npx prisma migration check # files intact, graph well-formed (offline) +npx prisma db migrate --show --db $DATABASE_URL # log what's about to run +npx prisma db migrate --db $DATABASE_URL ``` -The runner executes only `ops.json`, which is plain data, so your `migration.ts` files, and any TypeScript they import, are never executed with production credentials. +The runner executes only `ops.json`, which is plain data. Your `migration.ts` files, and any TypeScript they import, are never executed with production credentials. -Concurrent deploys are safe: on PostgreSQL the whole apply runs inside a transaction guarded by an advisory lock, so two `db migrate` runs serialize instead of interleaving. On MongoDB, cross-collection DDL transactions don't exist: instead, each migration advances the marker with compare-and-swap, and the runner verifies the resulting schema before committing the marker, so a re-run converges rather than double-applying. +On PostgreSQL, concurrent deploys are safe: the whole run is one transaction that holds an advisory lock, so two `db migrate` runs take turns instead of interleaving. On MongoDB there is no transaction. After a contract space's operations run, the runner checks the database against the contract and then moves the marker with a compare-and-swap. If another run moved the marker first, this run fails with `Marker was modified by another process during migration execution.` ## Extension spaces -If your project uses [database extensions](/orm/extensions/using-extensions) (say pgvector), you'll see more than one *contract space* in the output. Extensions ship their own migrations (for example `CREATE EXTENSION vector`), tracked in `migrations//` next to your app's. One `db migrate` run walks them all (extensions first, then your app) and reports each space separately: +If your project uses [database extensions](/orm/extensions/using-extensions), such as pgvector, you'll see more than one contract space in the output. Extensions ship their own migrations, for example one that runs `CREATE EXTENSION IF NOT EXISTS vector`. They are stored in `migrations//`, next to your app's `migrations/app/`. One `db migrate` run applies them all, extensions first and then your app, and reports each space separately: ```text -✔ Applied 20 operation(s) across 2 contract spaces +✔ Applied 2 migration(s) (20 operation(s)) across 2 contract space(s) Extension space: pgvector - └─ Enable extension "vector" + ├─ Enable extension "vector" + └─ marker 3d2c56a2944685bd21b05bc8a8d73164397df51c014201902932fbe7e80ff1b8 App space ├─ Create table "user" @@ -156,13 +156,13 @@ App space :::note[What's early] -Apply, targeting, preview, refs, the ledger, and multi-space runs all work today. Not built yet: a shadow-database rehearsal, and an apply-time check that `ops.json` still matches `migration.ts`. Until then, `db migrate` runs against the real target, so rehearse with `--show` and a staging database, and run `migration check` in CI to catch a stale `ops.json`. +Apply, targeting, preview, refs, the ledger, and multi-space runs all work today. Not built yet: a shadow-database rehearsal, and an apply-time check that `ops.json` still matches `migration.ts`. Until then, `db migrate` runs against the real target, so preview with `--show`, rehearse on a staging database, and run `migration check` in CI to catch a hand-edited `ops.json`. ::: ## Prompt your coding agent -Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. Ask your agent to: - "Check migration status against staging and apply whatever is pending." - "Preview what `db migrate --to prod` would run and summarize the destructive operations." 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 6cbd3a617f5..b823cdc69b1 100644 --- a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx @@ -1,6 +1,6 @@ --- title: Editing a migration -description: 'A migration is TypeScript you own. Fill in backfills, reorder steps, or drop to raw SQL, then recompile it with one command.' +description: 'A migration is TypeScript you own. Fill in backfills, reorder steps, or write raw SQL, then recompile it with one command.' url: /orm/migrations/editing-a-migration metaTitle: Editing a migration in Prisma ORM metaDescription: 'How to edit a Prisma ORM migration.ts by filling placeholder data transforms, writing typed backfills, using rawSql, and recompiling ops.json with node migration.ts.' @@ -10,35 +10,35 @@ The planner writes a good first draft, but many migrations require the input of The rule that makes this safe: -> **You edit `migration.ts`. Running it regenerates `ops.json`. The runner only ever executes `ops.json`.** +> **You edit `migration.ts`, and running it regenerates `ops.json`. The runner only ever executes `ops.json`.** -After any edit, recompile from inside your project: +After any edit, recompile from your project's root directory: ```bash node migrations/app/20260707T1008_add_display_name/migration.ts ``` ```text -Wrote ops.json + migration.json to migrations/app/20260707T1008_add_display_name +Wrote ops.json + migration.json to /path/to/my-app/migrations/app/20260707T1008_add_display_name ``` -The recompile also **re-attests** the migration: `migration.json` gets a fresh `migrationHash` fingerprinting the compiled output, so any later hand-edit of `ops.json` is detectable. If someone edits `ops.json` directly, or edits `migration.ts` and forgets to recompile, `migration check` fails in CI with a hash mismatch. Commit `migration.ts` and `ops.json` together, like a lockfile and its manifest. +Recompiling also records a new `migrationHash` in `migration.json`, computed from `migration.json` and `ops.json`. If someone hand-edits `ops.json` afterwards, `migration check` fails with a hash mismatch. `migration.ts` is not part of that hash, so `migration check` cannot tell when someone edits `migration.ts` and forgets to recompile. Commit `migration.ts` and `ops.json` together. ## Worked example: making a column required This is the classic case: `User` gets a required `displayName`, but the table already has rows and those rows have no `displayName`. Plan it: ```npm -npx prisma@latest migration plan --name add_display_name +npx prisma migration plan --name add_display_name ``` ```text ⚠ Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit -Open migration.ts and replace each `placeholder(...)` call with your actual query. +Replace each placeholder(...) call in migrations/app/20260707T1008_add_display_name/migration.ts with your query ``` -The planner knew the shape of the fix (add the column nullable, backfill, then tighten) and scaffolded exactly that, leaving the backfill query to you: +The planner writes the three steps this change needs (add the column as nullable, backfill it, then make it required) and leaves the backfill query to you: ```ts title="migration.ts (as planned)" override get operations() { @@ -57,7 +57,7 @@ override get operations() { } ``` -A `dataTransform` takes two closures. `check` asks "are there rows that still need this?", so it must be a row-returning query where *any row* means work remains. The conventional shape is `select('id').where().limit(1)`. `run` performs the change. Fill them with the typed SQL query builder, wired to the contract snapshot the migration imports from `migrations/snapshots/`: +A `dataTransform` takes two closures. `check` asks "are there rows that still need this?", so it must be a row-returning query where *any row* means work remains. The conventional shape is `select('id').where().limit(1)`. `run` performs the change. Fill them with the typed SQL query builder, built from the snapshot of your contract (the `contract.prisma` file that replaced `schema.prisma`) that the migration imports from `migrations/snapshots/`: ```ts title="migration.ts (filled in)" import type { Contract as End } from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract'; @@ -112,14 +112,12 @@ export default class M extends Migration { MigrationCLI.run(import.meta.url, M); ``` -`sql()` needs both a `context` and the adapter's `rawCodecInferer`. `rawCodecInferer` is what works out how to convert a bare JavaScript value you interpolate into a raw fragment. It is a required field, so a call without it does not typecheck and `node migration.ts` fails before writing `ops.json`. Hoisting the execution stack into its own `const` is what makes it reachable as `stack.adapter.rawCodecInferer`; the runtime wires it the same way. +`sql()` needs both a `context` and the adapter's `rawCodecInferer`. `rawCodecInferer` is what works out how to convert a bare JavaScript value you interpolate into a raw SQL fragment. It is a required field, so a call without it does not type-check. Keeping the execution stack in its own `const` lets you pass `stack.adapter.rawCodecInferer`, the same value the Prisma ORM client passes. -These two details should be noted: +Two details matter here: -- **The types come from the migration's end-contract snapshot** (`migrations/snapshots//contract`), not your live app contract. You're type-checking against the schema *as it will exist when this step runs*. That's why the builder happily references `displayName` even though the column doesn't exist yet, and why a migration written months ago keeps compiling after your contract moves on. -- **The query is real application-grade TypeScript.** You can import shared constants, and typos in column names fail the type check instead of failing in production. - -The wiring block at the top (deserializing the contract, building the `db` handle) is the current shape, but expect it to shrink to a one-liner. It's the price of a query builder that's typed against this migration's snapshot instead of your live app client. +- **The types come from the migration's end-contract snapshot** (`migrations/snapshots//contract`), not your app's current contract. You're type-checking against the database *as it will exist when this step runs*. That's why the builder accepts `displayName` even though the column doesn't exist yet, and why a migration written months ago keeps compiling after your contract changes. +- **The query is ordinary TypeScript.** You can import shared constants, and typos in column names fail the type check instead of failing in production. Recompile with `node migration.ts` and inspect what the backfill became: @@ -128,6 +126,7 @@ Recompile with `node migration.ts` and inspect what the backfill became: "id": "data_migration.backfill-user-displayName", "label": "Data transform: backfill-user-displayName", "operationClass": "data", + "target": { "id": "postgres" }, "precheck": [ { "description": "Check backfill-user-displayName has work to do", @@ -152,11 +151,74 @@ Recompile with `node migration.ts` and inspect what the backfill became: } ``` -Your `check` closure became both the precheck (`EXISTS`: is there work?) and the postcheck (`NOT EXISTS`: is it done?). Your `run` became a parameterized `UPDATE`. Note that `"Anonymous"` travels in `params`, through the driver's parameter binder, and is never spliced into SQL text. The reviewer reading your PR sees intent in `migration.ts` and the exact statements in `ops.json`, side by side. +Your `check` closure became both the [precheck](/orm/migrations/how-migrations-work#every-operation-checks-itself) (`EXISTS`: is there work?) and the postcheck (`NOT EXISTS`: is it done?). Your `run` became a parameterized `UPDATE`. `"Anonymous"` is sent in `params` and bound by the database driver, so it is never written into the SQL text. The reviewer reading your PR sees intent in `migration.ts` and the exact statements in `ops.json`, side by side. + +## When a backfill reads a column the migration removes + +Suppose the next migration replaces the boolean `isAdmin` on `User` with a required text column, `role`. The backfill has to read `isAdmin` to decide each row's `role`. The end contract has no `isAdmin`. The start contract has no `role`. If you build `db` from the end contract, TypeScript rejects `f.isAdmin`. If you run `node migration.ts` anyway, it stops with an error before it writes `ops.json`. + +Instead, describe `User` as it is while the backfill runs, in a contract of its own. Copy your contract into the migration directory as `intermediate.prisma`, and give `User` both columns. `role` is optional here because it is still empty: + +```prisma title="intermediate.prisma (the User model)" +model User { + id Int @id + displayName String + isAdmin Boolean + role String? +} +``` + +`npx prisma contract emit` reads only the contract file that a config file names. Add a second config file, `intermediate.config.ts`, next to `intermediate.prisma`. Run `npx prisma contract emit --config intermediate.config.ts` from the migration directory, because paths in a config file are relative to where you run the command. It writes `intermediate.json` and `intermediate.d.ts` next to `intermediate.prisma`. + +```ts title="intermediate.config.ts" +import { definePrismaConfig } from 'prisma/config'; +import { defineConfig as ormConfig } from '@prisma/orm-postgres/config'; +export default definePrismaConfig({ orm: ormConfig({ contract: './intermediate.prisma' }) }); +``` + +```text title="migrations/app/20260708T0900_replace_is_admin/" +├── intermediate.config.ts +├── intermediate.d.ts +├── intermediate.json +├── intermediate.prisma +├── migration.json +├── migration.ts +└── ops.json +``` + +In `migration.ts`, import the intermediate contract and build `db` from it. Pass the same contract to `dataTransform`. If the two differ, `node migration.ts` fails with an error whose `code` is `MIGRATION.DATA_TRANSFORM_CONTRACT_MISMATCH`. These lines replace `endContract`, `db`, and the `operations` getter in the example above: + +```ts title="migration.ts (what changes)" +import type { Contract as Mid } from './intermediate'; +import midContractJson from './intermediate.json' with { type: 'json' }; +const midContract = new PostgresContractSerializer().deserializeContract(midContractJson); +const db = sql({ + context: createExecutionContext({ contract: midContract, stack }), + rawCodecInferer: stack.adapter.rawCodecInferer, +}); +override get operations() { + return [ + this.addColumn({ schema: 'public', table: 'user', column: col('role', 'text', { codecRef: { codecId: 'pg/text@1' } }) }), + this.dataTransform(midContract, 'backfill-user-role', { + check: () => db.public.user.select('id').where((f, fns) => fns.eq(f.role, null)).limit(1), + run: [ + () => db.public.user.update({ role: 'admin' }).where((f, fns) => fns.eq(f.isAdmin, true)), + () => db.public.user.update({ role: 'member' }).where((f, fns) => fns.eq(f.role, null)), + ], + }), + this.setNotNull({ schema: 'public', table: 'user', column: 'role' }), + this.dropColumn({ schema: 'public', table: 'user', column: 'isAdmin' }), + ]; +} +``` + +Type a transform against the end contract when it writes only columns the end contract has and reads only columns it keeps. Use an intermediate contract when it reads a column the migration removes or renames. The alternative is two migrations: the first adds and backfills `role`, and the second drops `isAdmin`. That costs an extra contract state, which exists only so the backfill can be written. + +If `User` has a column that sets itself on every update, such as `updatedAt temporal.updatedAtString()` in the model `npx prisma orm init` writes, the backfill's `update` sets that column too. The value is the time you ran `node migration.ts`, so every row gets that time, and each recompile writes a new one into `ops.json`. In `intermediate.prisma`, declare the column as a plain `TimestamptzString`, and the backfill leaves it unchanged. -## Escape hatch: raw SQL +## Raw SQL [#escape-hatch-raw-sql] -For anything the operation factories don't cover (enabling an extension, `CREATE INDEX CONCURRENTLY`, a vendor-specific statement), use `rawSql`, and keep the same three-phase safety if you can: +For a statement the operation factories don't cover (such as `CREATE INDEX CONCURRENTLY` or a vendor-specific statement), use `rawSql` from `@prisma/orm-postgres/migration`, and give it a precheck and a postcheck if you can: ```ts rawSql({ @@ -174,11 +236,11 @@ rawSql({ }), ``` -The prechecks and postchecks are optional, but they're what makes a failed run resumable and a mistake diagnosable: skipping them trades away most of what this system gives you. If you write the same `rawSql` twice, lift it into a function: the built-in factories are plain functions doing exactly this. +The `precheck` and `postcheck` arrays can be empty, but the runner skips an operation only when its postchecks already pass, and stops when a precheck fails. See [When something goes wrong](/orm/migrations/applying-a-migration#when-something-goes-wrong) for what that means on a failed run. If you write the same `rawSql` twice, move it into a function. ## The same pattern on MongoDB -Data transforms work on MongoDB with the same `check`/`run` shape and the same compilation to precheck/execute/postcheck. Hand-authored Mongo transforms currently build their queries from raw Mongo command shapes rather than the full typed builder: +On MongoDB, you import `dataTransform` from `@prisma/orm-mongo/target/migration`, and `check` puts its query in `{ source }`. It compiles to a precheck, a `run` step, and a postcheck. Hand-written MongoDB transforms build their queries from command objects such as `AggregateCommand` and `RawUpdateManyCommand`, not the typed query builder, because the runner cannot read the builder's update commands back from `ops.json`: ```ts title="migration.ts (MongoDB, excerpt)" import { dataTransform, setValidation } from '@prisma/orm-mongo/target/migration'; @@ -199,35 +261,35 @@ override get operations() { } ``` -The full working migration, including the two query-plan helpers, is in the Prisma ORM repo's [retail-store example](https://github.com/prisma/orm/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts). +The full working migration, including the two functions that build its queries, is in the Prisma ORM repo's [retail-store example](https://github.com/prisma/orm/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts). ## Starting from a blank migration -Sometimes there's no contract change at all: you want a data-only migration, or you'd rather write the whole thing by hand. `migration new` scaffolds an empty migration directory (already attested, as is everything the CLI writes): +Sometimes you want a data-only migration, or you'd rather write the whole thing by hand. `migration new` writes a migration directory with a `migration.ts` that has no operations, an empty `ops.json`, and a `migration.json` that already has a `migrationHash`: ```npm -npx prisma@latest migration new --name backfill_scores +npx prisma migration new --name backfill_scores ``` -Write your operations in the generated `migration.ts`, then compile it the same way: `node migration.ts`. +If your contract has not changed since the last migration, that command fails with an error whose `code` is `MIGRATION.NO_CHANGES`. For a data-only migration, pass the `to` hash from your latest migration's `migration.json`: `npx prisma migration new --name backfill_scores --from `. Then write your operations in the generated `migration.ts` and recompile it the same way: `node migration.ts`. ## Editing checklist 1. Edit `migration.ts`, never `ops.json`. 2. Recompile: `node /migration.ts`. -3. Review the diff of `ops.json`; that's what will run. -4. Verify: `npx prisma@latest migration check`. +3. Review the diff of `ops.json`, because that's what will run. +4. Verify: `npx prisma migration check`. 5. Commit `migration.ts`, `ops.json`, and `migration.json` together. :::note[What's early] -The typed-builder wiring and the raw Mongo command shapes above are the current surface and will get slimmer. Everything shown on this page (the scaffolded placeholder flow, the typed backfill, `rawSql`, the recompile loop) works today, end to end. +Hand-written MongoDB data transforms cannot use the typed query builder for updates, because the runner cannot read those commands back from `ops.json`. Nothing checks that `ops.json` still matches `migration.ts`, so recompile after every edit. ::: ## Prompt your coding agent -Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. Ask your agent to: - "Fill in the placeholder in the latest migration: backfill `displayName` with the user's email prefix." - "Add a data transform to this migration that normalizes existing `phone` values before the unique constraint." diff --git a/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx b/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx index aa842c4a52b..b83392614c3 100644 --- a/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx @@ -1,14 +1,12 @@ --- title: Generating a migration -description: Turn a contract change into a reviewable migration with the migration plan command. +description: Turn a contract change into a migration you can review, with the migration plan command. url: /orm/migrations/generating-a-migration metaTitle: Generating a migration in Prisma ORM -metaDescription: 'A step-by-step tutorial for the migration plan command, from contract change to a planned, reviewable migration, entirely offline.' +metaDescription: 'A step-by-step tutorial for the migration plan command, from a contract change to a planned migration you can review, without connecting to a database.' --- -This tutorial takes a contract change from your editor to a planned migration on disk. Planning is **fully offline**: `migration plan` reads your emitted contract and your existing migrations, and it never connects to a database. This means you can plan on a plane, in CI, or in a sandbox with no credentials. - -Start from a minimal project; you can scaffold one with `npm create prisma@latest`. +This tutorial takes a change to your contract, the `contract.prisma` file that replaced `schema.prisma`, from your editor to a planned migration on disk. Planning is **fully offline**: `migration plan` reads your emitted contract and your existing migrations, and it never connects to a database. This means you can plan on a plane, in CI, or in a sandbox with no credentials. Start from an existing project, or create one with `npm create prisma@latest -- my-app`. ## Your first migration @@ -24,32 +22,30 @@ model User { } ``` -First, emit the contract, which compiles the schema into the `contract.json` artifact that every other command reads: +Emit the contract with `npx prisma contract emit`, which you run after every change to the contract, then plan. `--name` sets the readable part of the directory name, and without it the name ends in `_migration`: ```npm -npx prisma@latest contract emit -``` - -Now plan. `--name` sets the human-readable part of the directory name, and if you omit it, the directory will be called `migration`: - -```npm -npx prisma@latest migration plan --name init +npx prisma contract emit +npx prisma migration plan --name init ``` ```text +│ contract: src/prisma/contract.json +│ migrations: migrations/app +│ name: init + ✔ Planned 2 operation(s) +ℹ No db ref set — planning from an empty database. Run db init, db update, or db sign if a database already exists. -│ +migrations/app/20260707T1005_init ├─ Create schema "public" └─ Create table "user" -from: (baseline) -to: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 -App space → migrations/app/20260707T1005_init - -Next: review migrations/app/20260707T1005_init if needed, then run prisma db migrate. +from: (baseline) +to: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 +app space: migrations/app/20260707T1005_init -DDL preview +ℹ DDL preview CREATE SCHEMA IF NOT EXISTS "public"; CREATE TABLE "public"."user" ( @@ -58,16 +54,19 @@ CREATE TABLE "public"."user" ( "name" text, PRIMARY KEY ("id") ); + +→ Review migrations/app/20260707T1005_init +→ Apply the migration: prisma db migrate ``` Four things to note: -- **The DDL preview is in the output.** You will see the exact SQL before anything exists but files. -- **`from: (baseline)`** means this migration starts from an empty database. It's the root of your [migration graph](/orm/migrations/the-migration-graph). -- **`to:` is your contract's hash.** The migration promises to deliver a database matching exactly the contract that you have emitted. -- **`App space`** is your application's migration lane. Database extensions bring their own lanes. See [extension spaces](/orm/migrations/applying-a-migration#extension-spaces) for more information. +- **The DDL preview** is the SQL the migration runs, shown before anything exists but files. +- **`from: (baseline)`** means this migration starts from an empty database. It is the first migration in your [migration history](/orm/migrations/the-migration-graph). +- **`to:` is your contract's hash**, a short identifier computed from the contract. The migration builds a database that matches exactly the contract you emitted. +- **`app space`** is your app's contract space, a separate migration history: one for your app, and one for each database extension that ships its own migrations. See [extension spaces](/orm/migrations/applying-a-migration#extension-spaces). -The planned migration directory contains the TypeScript source, the compiled operations, and the history marker. The contract snapshots it imports live once per migrations root, under `migrations/snapshots//`: +The migration directory holds `migration.ts`, the operations in `ops.json`, and the metadata in `migration.json`. The contract snapshots that `migration.ts` imports are stored once per migrations directory, under `migrations/snapshots//`: ```text migrations/ @@ -85,6 +84,7 @@ migrations/ Open `migration.ts`. It is a description of the change, so it reads like one: ```ts title="migrations/app/20260707T1005_init/migration.ts" +#!/usr/bin/env -S node import type { Contract as End } from '../../snapshots/705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/contract'; import endContract from '../../snapshots/705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/contract.json' with { type: 'json', @@ -114,121 +114,120 @@ export default class M extends Migration { MigrationCLI.run(import.meta.url, M); ``` -When reading it, note two things: the `col(...)` calls mirror your contract fields, and you never write `codecRef` by hand. The planner fills it in to pin how values convert between TypeScript and Postgres, so treat it as noise when reviewing. - -For a simple change you don't need to touch this file. When you do (to add a data backfill, reorder operations, or drop in raw SQL), refer to [Editing a migration](/orm/migrations/editing-a-migration). +The `col(...)` calls mirror your contract fields. The planner fills in each `codecRef`, which records how that column's values are stored and read. For a simple change you don't need to touch this file. When you need a data backfill, a different operation order, or raw SQL, see [Editing a migration](/orm/migrations/editing-a-migration). ## The second migration: planning a delta -Apply the first migration (covered properly in [Applying a migration](/orm/migrations/applying-a-migration)), then make another change: add a `phone` field: - -```prisma -model User { - id Int @id - email String - name String? - phone String? - - @@map("user") -} -``` - -Emit and plan again. This time, tell the planner where to start from, so it plans the *delta* rather than the whole schema: +Apply the first migration as shown in [Applying a migration](/orm/migrations/applying-a-migration). Then add an optional `phone String?` field to `User`, emit, and plan again. This time, tell the planner where to start from, so it plans only the change rather than the whole database: ```npm -npx prisma@latest contract emit -npx prisma@latest migration plan --name add_user_phone --from 20260707T1005_init +npx prisma contract emit +npx prisma migration plan --name add_user_phone --from 20260707T1005_init ``` ```text +│ contract: src/prisma/contract.json +│ migrations: migrations/app +│ from: 20260707T1005_init +│ name: add_user_phone + ✔ Planned 1 operation(s) -│ +migrations/app/20260707T1006_add_user_phone └─ Add column "phone" to "user" -from: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 -to: 925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72 -App space → migrations/app/20260707T1006_add_user_phone +from: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 +to: 925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72 +app space: migrations/app/20260707T1006_add_user_phone -DDL preview +ℹ DDL preview ALTER TABLE "public"."user" ADD COLUMN "phone" text; + +→ Review migrations/app/20260707T1006_add_user_phone +→ Apply the migration: prisma db migrate ``` -`--from` accepts a migration directory name (as here), a contract hash or unambiguous prefix, a ref name, or `^` meaning "the state *before* that migration". +`--from` accepts a migration directory name, as here, a contract hash or a prefix that matches only one hash, a ref name, `^` for the state *before* that migration, or `@empty` for an empty database. ### The db ref: skipping --from -You don't have to pass `--from` every time: if a [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) named **`db`** exists, planning starts from whatever it points at by default. Four things move it forward: +You don't have to pass `--from` every time. If a [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) named **`db`** exists, planning starts from the contract state it points at. A ref is a name for a contract state, stored as a file in `migrations/app/refs/`. Four things move the `db` ref forward: -- `db init` and `db update` advance it after a successful run, but only when you leave `--db` off and let the connection come from `prisma.config.ts`. Passing `--db` suppresses the advancement unless you also pass `--advance-ref db`. -- `db sign` advances it after a successful signature, with or without `--db`. This is what makes the first plan after adopting an existing database contain only your change. Pass `--no-advance-ref` to sign without moving it. -- `db migrate --advance-ref db` advances it after an apply. Plain `db migrate` never advances anything; that is deliberate, so a deploy or CI apply cannot move a repository ref. +- `db init` and `db update` advance it after a successful run without `--dry-run`, but only when you leave `--db` off and the connection comes from `prisma.config.ts`. With `--db`, they leave it alone unless you also pass `--advance-ref db`. +- `db sign` advances it after a successful signature, with or without `--db`. That is why the first plan after you adopt an existing database starts from that database's contract, not from empty. Pass `--no-advance-ref` to sign without moving it. +- `db migrate --advance-ref db` advances it after the migrations are applied. Plain `db migrate` never advances a ref. - `migration ref set db ` sets it by hand. ```npm -npx prisma@latest db update # advances db on its own -npx prisma@latest db update --db "$DATABASE_URL" --advance-ref db # with --db, ask for it -npx prisma@latest db sign --db "$DATABASE_URL" # advances db on its own -npx prisma@latest db migrate --advance-ref db -npx prisma@latest migration plan --name next_change # starts from the db ref automatically +npx prisma db update # advances db on its own +npx prisma db update --db "$DATABASE_URL" --advance-ref db # with --db, ask for it +npx prisma db sign --db "$DATABASE_URL" # advances db on its own +npx prisma db migrate --advance-ref db +npx prisma migration plan --name next_change # starts from the db ref automatically ``` :::warning[Without `--from` or a `db` ref, planning stops] -If there's no `db` ref and you omit `--from`, `migration plan` refuses with `MIGRATION.PLAN_ORIGIN_UNKNOWN` whenever migrations already exist on disk. Planning from empty there would produce a full `CREATE`-everything package that no database carrying the earlier migrations could apply, so the command stops instead and names the three exits: `migration ref set db `, `--from `, or `--from @empty` to plan from an empty database deliberately. +If there is no `db` ref, you omit `--from`, and migrations already exist in `migrations/app/`, `migration plan` stops and fails with an error whose `code` is `MIGRATION.PLAN_ORIGIN_UNKNOWN`. Planning from an empty database there would write a migration that recreates everything your existing migrations already create. The error names three ways forward: set the `db` ref with `migration ref set db ` or `db update`, pass `--from `, or pass `--from @empty` to plan from an empty database on purpose. -Only an empty `migrations/app/` still plans from empty on its own: that is the first migration in a new project, described above. The command prints `No db ref set — planning from an empty database` under its summary when that happens, so you can tell a fresh first plan from a plan that recreates a database you already have. +Only when `migrations/app/` has no migrations yet does the command still plan from an empty database on its own: that is the first migration in a new project, described above. It then prints `No db ref set — planning from an empty database` under its summary, with a reminder to run `db init`, `db update`, or `db sign` if a database already exists. ::: ### The automatic baseline -When `migrations/app/` is empty but the `db` ref already points at a contract whose snapshot is stored under `migrations/snapshots/` — the usual state after a few `db update` cycles — one `migration plan` run writes a baseline package from nothing to the ref's contract and, when your emitted contract differs from the ref's, a second package with the delta. Expect one or two new directories in `git status`. +Sometimes `migrations/app/` has no migrations yet, but the `db` ref already points at a contract state whose snapshot is stored under `migrations/snapshots/`. That is the usual state after a few `db update` runs. One `migration plan` run then writes a baseline migration from an empty database to the ref's contract state and, when your emitted contract differs from it, a second migration with the change. Expect one or two new directories in `git status`. -The baseline never runs against a database that already carries a marker: the runner starts at the marker and applies only the edges past it. It exists so the ref's contract is a node in the graph and the delta has somewhere to attach. +The baseline never runs against a database that already has a marker, the record in the database of which contract state it currently matches: `db migrate` starts from the marker and applies only the migrations after it. The baseline exists so the ref's contract state is in your migration history and the change has somewhere to attach. The rule is under [Baselines](/orm/migrations/the-migration-graph#baselines). -To retrofit a deployed database you must not touch, do the same thing explicitly: bring the contract source back to the deployed state, `contract emit`, `migration plan --name baseline --from @empty`, then restore your current contract and plan the delta from the baseline. +To create a baseline by hand for a deployed database you must not touch, change the contract back to the deployed state, run `contract emit` and `migration plan --name baseline --from @empty`, then restore your current contract, emit it again, and plan the change with `--from` set to the baseline directory. ## When the planner needs your input -Some changes can't be planned from the schema diff alone. Add a **required** field to a table that already has rows; the planner can write the `ADD COLUMN` and the `SET NOT NULL`, but only you know what the existing rows should contain. Rather than guessing, it scaffolds the decision as a **placeholder**: +Some changes can't be planned from the contract change alone. Add a **required** field with no default to an existing table, and the planner can write the `ADD COLUMN` and the `SET NOT NULL`, but only you know what the existing rows should contain. Rather than guessing, it leaves that decision as a **placeholder**: ```text +│ contract: src/prisma/contract.json +│ migrations: migrations/app +│ name: add_display_name + ⚠ Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit -Open migration.ts and replace each `placeholder(...)` call with your actual query. -Then run: node migrations/app/20260707T1008_add_display_name/migration.ts +from: 925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72 +to: 4b8e2f6a1c9d3e7f5a0b2c4d6e8f1a3b5c7d9e0f2a4b6c8d0e1f3a5b7c9d1e2f +app space: migrations/app/20260707T1008_add_display_name + +→ Replace each placeholder(...) call in migrations/app/20260707T1008_add_display_name/migration.ts with your query +→ Run it to self-emit ops.json and attest the package: node "migrations/app/20260707T1008_add_display_name/migration.ts" ``` -The generated migration sandwiches a `dataTransform` between the schema steps, with `placeholder(...)` where your backfill query goes. Filling it in is explained in [Editing a migration](/orm/migrations/editing-a-migration). +The generated migration puts a `dataTransform` between the `ADD COLUMN` and `SET NOT NULL` steps, with `placeholder(...)` where your backfill query goes. Filling it in is explained in [Editing a migration](/orm/migrations/editing-a-migration). ## Reviewing what you planned -Three offline commands close the loop before anything runs: +Three offline commands let you check the migration before anything runs: ```npm # One migration in detail: operations, metadata, DDL preview -npx prisma@latest migration show 20260707T1006_add_user_phone - -# The whole graph, with your new edge in place -npx prisma@latest migration graph - -# Integrity check: hashes match, files complete, graph well-formed; exits non-zero on failure -npx prisma@latest migration check +npx prisma migration show 20260707T1006_add_user_phone +# The whole migration history, with your new migration in place +npx prisma migration graph +# Integrity check: hashes match, files complete, no unreachable migrations or dangling refs +npx prisma migration check ``` -`migration check` is designed for CI: exit code `0` means clean, `2` means it couldn't resolve what you asked for, `4` means an integrity failure, for example someone hand-edited `ops.json` without re-running `migration.ts`. +In CI, read the exit code of `migration check`: `0` means every check passed, `2` means it could not find the migration or `--space` you named, and `4` means it found an integrity failure, for example a hand-edit to `ops.json` without re-running `migration.ts`. :::note[What's early] -Planning covers tables, columns, indexes, constraints, and the backfill scaffold shown above. Rename inference is not built yet: renaming a field plans as **drop column + add column**, flagged destructive with a data-loss warning. For a true rename, edit the migration and replace the pair with a `rawSql` `ALTER TABLE ... RENAME COLUMN`. There is also no interactive mode: the planner writes its best answer and leaves refinement to you in `migration.ts`. +Planning covers tables, columns, indexes, constraints, and the backfill placeholder shown above. Column renames are not detected yet: renaming a field plans as **drop column + add column**, flagged destructive with a data-loss warning. For a true rename, edit the migration and replace the pair with a `rawSql` operation that runs `ALTER TABLE ... RENAME COLUMN`. There is also no interactive mode: the planner writes its best answer and leaves changes to you in `migration.ts`. ::: ## Prompt your coding agent -Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. Ask your agent to: +Projects created with `npm create prisma@latest` include the [Prisma ORM skills](/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync`. Ask your agent to: - "Add a required `displayName` field to User, emit the contract, and plan the migration." - "Plan a migration named `add-orders-table` and show me its DDL preview before I commit it." @@ -239,4 +238,4 @@ Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/ - [Editing a migration](/orm/migrations/editing-a-migration): fill placeholders, add data steps, write raw SQL - [Applying a migration](/orm/migrations/applying-a-migration): run what you planned - [Studio with Prisma ORM](/studio/prisma-next): once applied, see the same operations as a visual diff in Prisma Studio -- [TypeScript Migrations in Prisma 8](https://www.prisma.io/blog/typescript-migrations-in-prisma-next): the design story behind `migration.ts` +- [TypeScript Migrations in Prisma 8](https://www.prisma.io/blog/typescript-migrations-in-prisma-next): a blog post on the design of `migration.ts` diff --git a/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx b/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx index 504199a1ed8..999df35921e 100644 --- a/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx +++ b/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx @@ -6,33 +6,33 @@ metaTitle: How migrations work in Prisma ORM metaDescription: 'What a Prisma ORM migration is, the plan-review-apply loop, the files inside a migration directory, and why every operation is safe to retry.' --- -A migration is how Prisma ORM changes your database when your contract changes. The **contract** is the schema description you author (a `.prisma` file or TypeScript) plus the `contract.json` artifact that it compiles to. The **schema** is the actual structure of the database, which a migration changes. For example, if you add a field to a model, something has to run `ALTER TABLE` against every database that serves your app: your laptop, staging, production. Migrations are that something, recorded as files in your repo so any change is reviewable, repeatable, and versioned with your code. +A migration is how Prisma ORM changes your database when your contract changes. Your contract is the `contract.prisma` file that replaced `schema.prisma`, and `prisma contract emit` compiles it into `contract.json`. A migration changes the database itself: its tables, columns, and indexes. For example, if you add a field to a model, something has to run `ALTER TABLE` against every database that serves your app: your laptop, staging, production. Migrations are that something, recorded as files in your repo so any change is reviewable, repeatable, and versioned with your code. -The workflow is comprised of a loop which you will run many times a day during development: +The workflow is a loop that you run many times a day during development: -1. **Change your contract**: edit your `.prisma` file, then run `contract emit`, which compiles the contract into the `contract.json` artifact that every other command reads. -2. **Plan a migration**: `migration plan` compares the new contract against your migration history and writes a migration directory under `migrations/`. -3. **Review it**: read the generated TypeScript and the DDL preview. Edit the migration if the change requires a data step, then re-run the file to recompile it. +1. **Change your contract**: edit `contract.prisma`, then run `prisma contract emit`, which writes `contract.json`. The other commands read that file. +2. **Plan a migration**: `migration plan` compares the new contract with the previous contract state and writes a migration directory under `migrations/app/`. +3. **Review it**: read the generated TypeScript and the DDL preview. Edit the migration if the change needs a data step, then run the file again to recompile it. 4. **Apply it**: `db migrate` runs the pending migrations against your database. -In a project with a database connection configured (the [quickstart](/prisma-orm/quickstart/postgresql) provides one), the whole loop consists of three commands. Add an optional `phone String?` field to a model in your `.prisma` file, then: +In a project with a database connection configured (the [quickstart](/prisma-orm/quickstart/postgresql) sets one up), the whole loop is three commands. Add an optional `phone String?` field to a model in `contract.prisma`, then: ```npm -npx prisma@latest contract emit -npx prisma@latest migration plan --name add_user_phone -npx prisma@latest db migrate --advance-ref db +npx prisma contract emit +npx prisma migration plan --name add_user_phone +npx prisma db migrate --advance-ref db ``` -`--advance-ref db` moves the [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs) named `db` to the state you just applied, which is where the *next* `migration plan` starts from. Leave it off and the ref stays where it was, so the next plan starts from that stale state. If no `db` ref exists at all, the plan has no origin and refuses with `MIGRATION.PLAN_ORIGIN_UNKNOWN` rather than write a full-create package. In CI and production you leave it off deliberately, because migrations arrive already planned. +In development, add `--advance-ref db` so the next `migration plan` starts from what you just applied, as [The db ref](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) explains. `migration plan` prepares the SQL for the migration, but doesn't run it: ```text ✔ Planned 1 operation(s) -│ +migrations/app/20260707T1006_add_user_phone └─ Add column "phone" to "user" DDL preview @@ -43,10 +43,10 @@ ALTER TABLE "public"."user" ADD COLUMN "phone" text; and `db migrate` applies the migration to your database and confirms what it applied: ```text -✔ Applied 1 operation(s) across 1 contract space +✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s) ``` -The contract-space count only matters once you add database extensions and can be disregarded until then. If you see the `✔ Applied` line, you've already run the entire workflow. The rest of this page provides further details on what a migration is, and why it looks the way it does. +A contract space is a separate migration history: one for your app, and one for each database extension that ships its own migrations. If you see the `✔ Applied` line, you've run the entire workflow. ## What a migration contains @@ -65,13 +65,13 @@ migrations/ └── contract.d.ts # types for that snapshot; they type-check migration.ts ``` -Contract snapshots live once per migrations root, in `migrations/snapshots/`, keyed by the contract's hash. A migration imports the states it moves between from there (a baseline migration starts from nothing and imports only its end contract), so a contract state shared by several migrations is stored once. +Contract snapshots are kept in `migrations/snapshots/`, one directory per contract hash. A migration imports the contract states it moves between from there, so a contract state shared by several migrations is stored once. A baseline migration starts from nothing, so it imports only its end contract. Three files matter day to day, and each has one job: | File | Purpose | | --- | --- | -| `migration.ts` | The file you edit. It describes the schema and data changes as TypeScript function calls. | +| `migration.ts` | The file you edit. It describes the changes to tables, columns, and data as TypeScript calls. | | `ops.json` | The file Prisma runs. It contains the compiled migration operations as JSON. | | `migration.json` | The file Prisma uses to track where this migration fits in history. | @@ -85,15 +85,15 @@ this.createTable(...) this.dataTransform(...) ``` -Because it is TypeScript, type-checked against the contract snapshots it imports from `migrations/snapshots/`, your editor autocompletes table and column names and catches mistakes before the migration touches a database. +Because it is TypeScript that imports its contract snapshots from `migrations/snapshots/`, your editor catches type mistakes before the migration touches a database. When you want to review or change what the migration does, use this file. For more information, see [Editing a migration](/orm/migrations/editing-a-migration). ### ops.json: the file Prisma runs -`ops.json` is the compiled form of the migration: when `migration.ts` runs, Prisma turns the TypeScript steps into JSON operations. The planner runs it for you, but after an edit, re-run it yourself with `node migration.ts`. The migration runner reads only this file. +`ops.json` is the compiled form of the migration. `migration plan` writes it for you, and after you edit `migration.ts` you run `node migration.ts` to write it again, as [Editing a migration](/orm/migrations/editing-a-migration) explains. The migration runner runs only this file. -That means production never executes your TypeScript: it reads the compiled operations, so no application code runs with production credentials. +That means production never executes `migration.ts`: it reads the compiled operations, so no migration TypeScript runs with production credentials. ```text migration.ts -> ops.json @@ -110,14 +110,14 @@ migration.ts -> ops.json what you author exact operations to run ``` -### migration.json: the history marker +### migration.json: its place in history [#migrationjson-the-history-marker] -`migration.json` records where this migration fits in your database history. Every contract compiles to a deterministic artifact; hashing it gives a short identifier for that exact schema state. A migration records: +`migration.json` records where this migration fits in your migration history. Each contract state is named by its hash, a short identifier computed from the contract. The same contract always produces the same hash. A migration records: -- the schema hash it starts `from` -- the schema hash it moves `to` +- the contract hash it starts `from` +- the contract hash it moves `to` - when it was created -- its own hash, so tampering is detectable +- its own hash, so a hand-edit to `migration.json` or `ops.json` is detected ```json title="migrations/app/20260707T1006_add_user_phone/migration.json" { @@ -129,7 +129,7 @@ what you author exact operations to run } ``` -Read `from` and `to` like a Git commit range: the schema state before, and the schema state after. These hashes are what link migrations into a [graph](/orm/migrations/the-migration-graph) instead of a numbered list. If you want a simple linear migration system, you can ignore the graph entirely. +Read `from` and `to` like a Git commit range: the contract state before, and the contract state after. These hashes link migrations into a [migration history](/orm/migrations/the-migration-graph) that can branch, instead of a numbered list. If your migrations form a straight line, you can ignore the branching entirely. `providedInvariants` lists the `invariantId` values that the migration's operations declare, such as a data transform's, and is empty for most app migrations. Together, the three files make a migration both editable and safe to run: @@ -139,8 +139,6 @@ ops.json Prisma runs the compiled operations migration.json Prisma tracks the migration in history ``` -(`providedInvariants` is used by extension migrations and stays empty for typical app migrations.) - ## Every operation checks itself Inside `ops.json`, each operation runs in three parts, in order: @@ -149,9 +147,10 @@ Inside `ops.json`, each operation runs in three parts, in order: 2. **Execute**: the statements that make the change. 3. **Postcheck**: confirms the change worked after it runs. -Each operation also carries a class, which drives the `(destructive)` flags and data-loss warnings you see in CLI output: +Each operation also has a class, which drives the data-loss warnings you see in CLI output: - **Additive**: safe to apply, like adding a column. +- **Widening**: relaxes a constraint or widens a type. - **Destructive**: can lose data, like dropping a column. - **Data**: changes rows, not structure. @@ -160,6 +159,7 @@ Each operation also carries a class, which drives the `(destructive)` flags and "id": "column.public.user.phone", "label": "Add column \"phone\" to \"user\"", "operationClass": "additive", + "target": { "id": "postgres", "details": { "schema": "public", "objectType": "column", "name": "phone", "table": "user" } }, "precheck": [ { "description": "ensure column \"phone\" is missing", @@ -185,13 +185,13 @@ Each operation also carries a class, which drives the `(destructive)` flags and This structure is what makes Prisma ORM migrations safe in the situations where classic SQL migrations hurt: -- **A migration fails.** On PostgreSQL the whole run is one transaction, so a failure rolls everything back and the database returns to exactly where it was. Fix the cause and re-run. Because the runner skips any operation whose postcheck already holds, re-running never double-applies work that's already true in the database. No commenting out statements, no hand-editing the database. +- **A migration fails.** On PostgreSQL the whole run rolls back, and re-running skips any operation whose postcheck already holds, as [When something goes wrong](/orm/migrations/applying-a-migration#when-something-goes-wrong) explains. - **The database isn't in the state you assumed.** The precheck catches it before the change runs, and the error names the exact operation and the exact check that failed, not a generic SQL error halfway through. - **Someone (or an agent) wrote the migration for you.** The intent (`migration.ts`), the exact SQL (`ops.json`), and the verification for every step are all in the diff, so review is straightforward. ## The commands -These commands live under the [Prisma ORM CLI](/cli), and you run them with `npx prisma@latest `. The planning and inspection commands work offline, because they read files, not your database: +These commands are part of the [Prisma ORM CLI](/cli), and you run them with `npx prisma `. The planning and inspection commands work offline, because they read files, not your database: | Command | What it does | | ------------------------- | ----------------------------------------------------------------------- | @@ -212,31 +212,31 @@ Three commands talk to a database: Note that applying is `db migrate`, not `migration apply`: `migration ...` commands manage the on-disk migration directories, while `db migrate` moves a database. -Here is the whole loop in motion, from idea to typed code against the new schema: +Here is the whole loop in motion, from idea to typed code against the new contract: