references/migrations.md
references/migrations.mdBrowse 76 files
47,265 bytes
Token encoding: o200k_base
Snapshot fac8604
Prisma 8 — Migration Authoring
Edit your data contract. Prisma 8 plans the migration. You fill in any data transforms.
The three-step user model:
- You edit your data contract. (
references/contract.md) - Prisma 8 plans the migration for you. ← this skill
- If a data transform is needed, you edit
migration.tsand self-emit. ← this skill
Once the contract changes, you choose how the change reaches the database. This skill covers the two paths (db update and migration plan + db migrate), the migration-package contract, the migration.ts authoring API, and the failure modes you recover from without leaving the loop.
Targets. Migration authoring is first-class for Postgres and Mongo. The CLI reads the target from prisma.config.ts (set during prisma orm init --target …). Migration commands do not accept a --target flag — use a config scoped to the target you need. Examples below call out target-specific imports, markers, factories, and transaction behavior where they diverge.
When to Use
- User edited the contract and wants to apply the change to the DB.
- User wants to author a migration with a data transform.
- User wants to run pending migrations against a local DB.
- User hit
MIGRATION.HASH_MISMATCH,MIGRATION.UNFILLED_PLACEHOLDER, or a partially-applied migration. - User mentions: migrate, migration, db push, db update,
prisma migrate dev,prisma migrate deploy, drift, hash mismatch, data backfill.
When Not to Use
- User wants to know what migrations will run on deploy / on merge, or to manage refs and invariants →
references/migration-review.md. - User is deciding where a plan should chain from, saw
from: (baseline)unexpectedly, is setting up migrations for a deploy-first (Composer / CD-managed) project, or is retrofitting migrations onto an existing database →references/migration-model.md. - User wants to edit the contract →
references/contract.md. - User wants a deeper read of a single structured error envelope →
references/debug.md.
Key Concepts
db update(quick path). Reads the emitted contract, diffs against the live DB, applies the change. Optional--dry-runprints the plan without executing. A destructive operation is applied only with consent: interactively you type the database name; non-interactively pass--confirm <database>(--yesdoes not grant it). Writes no migration directory. Operations needing data transforms are not handled by this path —db updateexcludes thedataoperation class entirely and short-circuits where a data transform would be required. Use only against a database that has no shared history with anyone else (your local dev DB).migration plan(formal path). Reads the emitted contract, diffs it against a resolved origin — explicit--from, else thedbref, else the empty database; there is no "head of the graph" to chain from (seereferences/migration-model.md) — and writes a new migration package undermigrations/app/<YYYYMMDDTHHMM>_<snake_slug>/. If any operation needs a data transform, the package'smigration.tscontainsplaceholder(...)calls you fill in.- The
app/segment in migration paths is the consuming application's contract-space id. Every migration you author lives undermigrations/app/. Extensions your contract depends on get their own sibling directories (migrations/<extension-space-id>/) — those are managed by the extension package and you don't write into them. Theapp/segment lands automatically the first time you runmigration plan/db initagainst an app-level config. - Migration package files (inside each
migrations/app/<dir>/):migration.json— manifest (metadata +migrationHash).ops.json— canonical operation list. Content-addressed;migrationHashis computed over this.migration.ts— TypeScript authoring source, framework-rendered bymigration plan(ormigration new). You edit specific holes in it (see Fill a placeholder below) and re-emitops.json/migration.jsonby running it.
- Contract snapshots.
migration.tsimports its bookend contracts from the shared, content-addressed store atmigrations/snapshots/<hex>/contract.json+contract.d.ts(<hex>is the contract's 64-hex storage hash) — not from files inside the migration package. - Self-emit. Running
node migrations/app/<dir>/migration.tsregeneratesops.jsonandmigration.jsonfrom the (possibly edited) TS source. This is the only supported way to update an existing migration package after edits. migration.tsshape. Framework-rendered. A classM extends Migration<Start, End>(from@internal/postgres/migrationon Postgres,@prisma/orm-mongo/target/migrationon Mongo — see the framing block below) that assigns the two snapshot imports tostartContractJson/endContractJson(Startisneverand there is nostartContractJsonon a baseline) and has anoperationsgetter returning an array of operation values. On Postgres the operation factories are methods on the base class (this.addColumn({...}),this.setNotNull({...}),this.dataTransform(...)) taking one options object; free helpers likecol(...)build the column descriptors they take. On Mongo they are free factories (createIndex(...),dataTransform(...)) imported besideMigration. The file ends withMigrationCLI.run(import.meta.url, M)so executing it self-emits.placeholder(slot). A sentinel the planner emits into the renderedmigration.ts(from the same.../migrationimport asMigration) wherever a data transform is needed. Callingplaceholder(...)at emit time throwsMIGRATION.UNFILLED_PLACEHOLDERwithmeta.slotnaming the hole. The user replaces the() => placeholder(...)arrow with a real query-plan closure (Postgres) or fillsdataTransform({ check, run })sources (Mongo — see Fill a placeholder), then self-emits.this.dataTransform(endContract, name, { check, run }). The data-transform factory.checkis a rowset query whose presence-of-any-row signals "work remains";runis one or more mutation queries that perform the backfill. Both are lazy closures returning query-plans built againstendContract. The runner wrapscheckasEXISTS(...)for precheck andNOT EXISTS(...)for postcheck, so the same closure asserts both "there is work" and "the work is done".pendingPlaceholders. A boolean field on the JSON result ofmigration plan.truemeans the package was written but contains unfilled placeholders —db migratewill throwMIGRATION.UNFILLED_PLACEHOLDERuntil you editmigration.tsand self-emit.migrationHash. Content-addressed identity of a migration package.MIGRATION.HASH_MISMATCHfires when the stored hash inmigration.jsondisagrees with the hash recomputed from the on-disk files (almost always: someone editedmigration.tswithout self-emitting).- Marker. Records "this database is at contract hash X for space Y". Postgres: a row in
prisma_contract.marker. Mongo: a document in the_prisma_migrationscollection (keyed by space). Each successful migration advances the marker once schema verification passes for that space.db signwrites the marker from the current contract hash, but only after a schema-verification pass succeeds (it will not sign a database whose live schema disagrees with the contract). - Apply atomicity. Postgres: one
db migraterun is one transaction — the runner issues a singleBEGIN, applies every pending migration for every contract space, then oneCOMMIT; any failure issuesROLLBACKfor the whole run, so the marker stays where it was before the command. Mongo: DDL ops (createCollection,createIndex,collMod,setValidation, …) are not wrapped in a multi-document transaction; the runner applies ops, verifies the live schema against the destination contract, and advances the marker only on verify-pass (resumable across spaces — see the MongoDB family doc). Ordinary DDL +dataTransformflows stay consistent; partial state from failed mid-migration runs is diagnosed withdb verify/db schema, not assumed away. - Operation classes. Every operation declares an
operationClass:additive,widening,data, ordestructive. The CLI surfaces these in the plan preview and in JSON output. There is nolong-runningclass and the framework does not emitCREATE INDEX CONCURRENTLY— operations stay transactional.
migration.ts is framework-rendered, not hand-authored
Files under migrations/<space-id>/<timestamp>/migration.ts (for your own app, <space-id> is always app/) are rendered for you by the framework — prisma migration plan writes a populated package whenever the contract changes, and prisma migration new writes an empty scaffold when you want to author operations directly. You do not write these files from scratch. You edit specific holes the framework leaves behind — chiefly replacing placeholder("<slot>") sentinels (Postgres) or filling dataTransform({ check, run }) pipeline slots (Mongo) — then self-emit.
Postgres rendered imports point at @internal/postgres/migration (or @internal/sqlite/migration for SQLite projects): one line carrying Migration, MigrationCLI, col, placeholder, rawSql, and any other free helper the operations need.
Mongo rendered imports point at one module too, @prisma/orm-mongo/target/migration, which carries Migration, MigrationCLI, placeholder, and the operation factories (createIndex, dataTransform, …). Raw command classes for data transforms come from @prisma/orm-mongo/query-ast/execution.
Treat the rendered import lines as framework-managed on both targets:
- Leave them where they are. Don't rewrite them to a different path; the framework's renderer is the authoritative shape and any change you make by hand will be reverted (and may trip
MIGRATION.HASH_MISMATCH) the next time the package is re-rendered or self-emitted. - If you need an additional helper symbol, add it to the existing rendered import line rather than introducing a second import from a different subpath.
- The "user code imports only from
@internal/<target>" convention applies to your own modules (queries, runtime setup, contract authoring). The framework-renderedmigration.tsscaffold is the framework's surface, not yours; the rule is suspended for that one file.
Diagnostic codes you route on
| Code | Source | Move |
|---|---|---|
MIGRATION.UNFILLED_PLACEHOLDER | Throwing placeholder(...) at emit time | Open migration.ts, replace the placeholder("<slot>") call named by meta.slot with the real query closure, self-emit. |
MIGRATION.FILE_MISSING | Reading a migration package | migration.ts, migration.json, or ops.json is absent. Recover from version control, re-emit via migration.ts, or run prisma migration new for a fresh one. |
MIGRATION.INVALID_DEFAULT_EXPORT | Loading migration.ts | The file's default export is not a Migration subclass or factory function. Restore the planner-emitted scaffold from version control or re-run migration plan for a clean package. |
MIGRATION.DATA_TRANSFORM_CONTRACT_MISMATCH | Building a data-transform query plan | The query builder was instantiated with a contract reference different from the endContract passed to this.dataTransform(...). Use the endContract imported at module scope for both. |
MIGRATION.HASH_MISMATCH Migration package is corrupt | db migrate (or any read of the package) | ops.json / migration.json were edited without self-emitting. Run node migrations/app/<dir>/migration.ts to re-emit, then re-run db migrate. |
MIGRATION.DESTRUCTIVE_CHANGES | db update run non-interactively without consent | Re-run with --confirm <database> (the database name from the connection), or --dry-run to preview. |
CONTRACT.MARKER_MISMATCH | db verify (finding, exit 4) | The marker disagrees with the contract hash (Postgres: prisma_contract.marker; Mongo: _prisma_migrations). The DB is at a different contract version than the code thinks. Either run a migration forward, or — if the DB is correct and the marker is stale after a manual fix-up — run db sign. |
CONTRACT.MARKER_MISSING | db verify (finding, exit 4), runtime startup (warning) | The DB has no marker yet. Run prisma db init --db <url> to baseline an empty database, db update --db <url> to apply the current contract directly, or db sign --db <url> if the schema already matches the contract. |
Decision — which path do you take?
| Situation | Path | Why |
|---|---|---|
| Local dev, schema in flux | db update | Fast, interactive, no migration files. |
| Shared branch with other developers | migration plan + db migrate | Replayable, reviewable, content-hashed. |
| Anything reaching production | migration plan + db migrate | Production must run a reviewed, hashed migration. |
| Adding a column that needs a backfill | migration plan (writes placeholder), edit migration.ts, self-emit, then db migrate | db update does not author data transforms; the formal path does. |
| Recovering from drift (DB diverged from contract) | db sign after manual fix, or migration plan if PN can plan the fix | Depends on which side is right. See Recover from drift below. |
Dev → ship transition (the db ref pattern)
Example — iterate locally with db update, then publish the first real migration:
pnpm prisma db init --db $DATABASE_URL
pnpm prisma contract emit && pnpm prisma db update --db $DATABASE_URL
pnpm prisma contract emit && pnpm prisma migration plan --name add_feature
pnpm prisma db migrate --db $DATABASE_URL
pnpm prisma db verify --db $DATABASE_URL
The db ref is a named pointer at migrations/app/refs/db.json — just { hash, invariants }. It records which contract hash the project's dev database has been brought up to — the offline planner's stand-in for "where is my local DB?" without opening a connection at plan time. The contract it names resolves through the shared content-addressed store at migrations/snapshots/<hex>/contract.json by that hash, the same store every migration graph node resolves through.
What db init / db update / db sign write. When run against the project's default --db URL (no explicit --db flag), db init and db update implicitly advance the db ref: they write-if-absent the post-command contract IR into the snapshot store, then write the ref's pointer. Override the ref name with --advance-ref <name>. When you pass --db <non-default-url>, ref advancement is suppressed unless --advance-ref is explicit — reconciling a different database is not the same as checkpointing this project's dev state. db sign also advances the db ref after a successful signature, writing the signed contract into the snapshot store first; --advance-ref <name> overrides the name, and --db does not suppress it — sign never mutates the schema, and adoption is normally done against the real database via --db. The only opt-out is --no-advance-ref, which signs without writing any ref or snapshot — what a CI or deployment pipeline, or a dev checkout re-signing a production database, usually wants.
The on-disk layout is just the pointer:
migrations/app/refs/
└── db.json # { "hash": "<hex>", "invariants": [] }
First migration plan after dev iteration. migration plan defaults --from to the db ref (and, when no db ref exists at all, falls back to planning from an empty database only while the migration graph is empty — the human output then adds a muted notice beneath the summary, No db ref set — planning from an empty database. Run db init, db update, or db sign if a database already exists., and the JSON document carries fromDefaulted: true; over a non-empty graph there is no fallback: the command refuses with MIGRATION.PLAN_ORIGIN_UNKNOWN; see references/migration-model.md § The trap). When the on-disk migration graph is still empty and the db ref points at a non-null hash with a store entry (typical after one or more db update cycles), the planner emits two bundles instead of one:
- Baseline:
null → from-hash(introducesfrom-hashas a graph node) - Delta:
from-hash → current_contract
Both land on disk in one invocation — expect two new directories in git status. db migrate then finds a path through the baseline and applies the delta. This closes the dev → ship trap where a single-bundle plan referenced a hash that was not yet a graph node and produced an unapplyable migration (MIGRATION.PATH_UNREACHABLE at apply time).
The forgot-the-flag pitfall. After the graph is non-empty, the default db ref may point past the graph tip (the ref advanced on every db update while you iterated, but you never committed migrations). The next implicit-default migration plan refuses with MIGRATION.HASH_NOT_IN_GRAPH and names reachable refs that point at graph nodes.
Recovery when you see MIGRATION.HASH_NOT_IN_GRAPH on plan:
# Option A — plan from a graph node explicitly
pnpm prisma migration plan --from production --name my_change
# Option B — realign the db ref to a graph-node hash, then plan with the default
pnpm prisma migration ref set db <graph-node-hash>
pnpm prisma migration plan --name my_change
If the db ref's pointer is itself missing and the hash isn't a graph node either (MIGRATION.SNAPSHOT_MISSING), create it with migration ref set db <hash> or advance it with db update --advance-ref db.
After plain db migrate. db migrate does not implicitly advance the db ref (production-shaped commands stay explicit). The live marker advances while the ref may lag. Refresh with db update (no-op on DB when already current) or db migrate --advance-ref db in the same invocation.
When to switch paths. Use db update while the schema is in flux on a solo dev database. Switch to migration plan + db migrate when the change needs a reviewable, replayable migration — typically before opening a PR or touching any shared environment. The db ref bridges the two: it captures dev iteration state on disk so the first formal plan knows where you left off.
Graph-node rule (plan time). Any hash used as a from end — explicit --from, default db ref, or ref name — must already be a node in the on-disk migration graph once the graph is non-empty. The auto-baseline two-bundle emission is the one exception: it applies only on an empty graph with a non-null ref-resolved from and an available store entry. If the ref's pointer is missing and the hash isn't a graph node either, plan refuses with MIGRATION.SNAPSHOT_MISSING instead.
Apply-time complement. db migrate reads the live marker before DDL. If the marker hash is not a graph node, the command refuses with MIGRATION.MARKER_MISMATCH — catching drift the offline planner cannot see. This is separate from MIGRATION.MARKER_NOT_IN_HISTORY, which fires later during the runner's graph walk when the marker is off the path being traversed. See references/migration-review.md for the full diagnostic catalog.
db is a default ref name, not a reserved one. The framework overwrites it on the next dev cycle; you may migration ref set db <hash> explicitly and accept that a subsequent db update replaces it when run against the default URL.
Canonical detail: Migration System § Contract resolution through the snapshot store, § migration plan, § Recovery affordances, ADR 218 — Refs with paired contract snapshots and universal graph-node invariant (TML-2629, its paired-snapshot part superseded — see the ADR's Status note), and ADR 240 — Contract snapshots live in a content-addressed store.
Workflow — db update (quick path)
The concept: db update resolves the destination (emitted contract) against the live DB and applies the difference. Preview with --dry-run. Destructive ops need consent: interactively the command asks you to type the database name; with --no-interactive (CI) it reads --confirm <database> instead, and refuses with MIGRATION.DESTRUCTIVE_CHANGES if neither is given. --yes accepts prompt defaults and never grants this consent. The path excludes operations of the data class entirely — if the diff requires a data transform, db update fails with a planning error and you switch to migration plan to author the transform.
Run after a contract edit:
pnpm prisma contract emit
# Postgres: --db postgresql://...
# Mongo: --db mongodb://... (dev scaffolds often need ?replicaSet=rs0)
pnpm prisma db update --db $DATABASE_URL --dry-run
pnpm prisma db update --db $DATABASE_URL
db update already verifies schema and advances the marker on success — a follow-up db verify is redundant on the happy path. Use db verify only when you need a standalone diagnostic (see Verify contract vs DB).
Inspect the JSON output to drive the next move:
pnpm prisma db update --db $DATABASE_URL --json
The JSON contains plan.operations[] with each operationClass, plus (in apply mode) execution.operationsExecuted and the post-apply marker.storageHash. If the command failed because of destructive operations, the error envelope's meta.destructiveOperations[] lists exactly what would have been dropped.
Workflow — migration plan + db migrate (formal path)
The concept: migration plan writes a new migration package on disk. If the planner needed any data transforms, the package is pending — migration.ts holds placeholder(...) calls until you fill them in. db migrate runs every pending package in graph order — on Postgres inside one transaction for the whole run; on Mongo op by op with verify-gated marker advancement (see Apply atomicity above).
Plan a change:
pnpm prisma contract emit
pnpm prisma migration plan --name <snake_slug>
Read the result. The JSON shape exposes the queryable signals:
dir— the path of the new package (e.g.migrations/app/20260515T1200_add_user_email/).pendingPlaceholders—trueifmigration.tsstill containsplaceholder(...)calls.operations[].operationClass— for spottingdestructiveanddataops.preview.statements— family-agnostic textual preview.
Inspect the package (the <target> positional is required — a directory name, hash or hash prefix, ref, or path):
pnpm prisma migration show <dirName-or-migrationHash-prefix>
pnpm prisma migration show migrations/app/20260515T1200_add_user_email
migration show displays a single migration package. To see the ordered list of migrations that would run — across all contract spaces — use db migrate --show:
# Online: reads the live DB marker as the origin.
pnpm prisma db migrate --show --db $DATABASE_URL
# Offline: hypothetical path from any ref or hash.
pnpm prisma db migrate --show --from <hash-or-ref> --to <hash-or-ref>
db migrate --show is read-only and never writes to the DB or the migration graph. Use it before applying to confirm the execution order.
Fill in any data transforms (see Fill a placeholder), self-emit if you edited migration.ts, then:
pnpm prisma db migrate --db $DATABASE_URL
db migrate runs without prompting — destructive-op confirmation lives on db update, not here. Review destructive ops in the plan output or in migration show before applying.
Workflow — Fill a placeholder
The concept: the planner can detect that a data transform is needed but not what it should do. It writes a typed scaffold and stops; you fill the transform, then self-emit.
Postgres
The planner can detect that a data transform is needed (e.g. backfilling a new NOT NULL column with no default) but not what it should do. You fill check and run closures with real query plans built against endContract.
The scaffold the planner emits looks like:
// migrations/app/20260515T1200_add_user_name/migration.ts
import { col, Migration, MigrationCLI, placeholder } from '@internal/postgres/migration';
import type { Contract as End } from '../../snapshots/93f07d1b…c9e1e5a2/contract';
import endContract from '../../snapshots/93f07d1b…c9e1e5a2/contract.json' with { type: 'json' };
import type { Contract as Start } from '../../snapshots/f62a4154…d07dddc/contract';
import startContract from '../../snapshots/f62a4154…d07dddc/contract.json' with { type: 'json' };
export default class M extends Migration<Start, End> {
override readonly startContractJson = startContract;
override readonly endContractJson = endContract;
override get operations() {
return [
this.addColumn({
schema: 'public',
table: 'user',
column: col('name', 'text', { codecRef: { codecId: 'pg/text@1' } }),
}),
this.dataTransform(endContract, 'backfill-user-name', {
check: () => placeholder('backfill-user-name:check'),
run: () => placeholder('backfill-user-name:run'),
}),
this.setNotNull({ schema: 'public', table: 'user', column: 'name' }),
];
}
}
MigrationCLI.run(import.meta.url, M);
(examples/prisma-8-demo/migrations/app/20260810T1108_add_post_engagement_counters/migration.ts is a committed rendered package to compare against.)
Replace both placeholder(...) calls with query-plan closures built from endContract. The check closure must return a rowset query whose presence of any row signals "work remains" — conventionally <table>.select('id').where(<violation predicate>).limit(1). Scalar/aggregate shapes (count(*), bool_and(...)) silently break the contract: the runner wraps check twice (EXISTS(...) for precheck, NOT EXISTS(...) for postcheck), and a query that always returns one row makes EXISTS always true and NOT EXISTS always false.
Build the query builder against endContract so the storage hashes line up — using a different contract reference raises MIGRATION.DATA_TRANSFORM_CONTRACT_MISMATCH. The cheapest way to get a typed SQL builder over the end contract is the façade itself: postgres<End>({ contractJson: endContract }) connects lazily, so constructing it inside migration.ts opens no connection; its sql is the builder and its contract is the validated contract to hand to this.dataTransform. The filled-in shape is the rendered scaffold above with only the two placeholder(...) arrows replaced (the operation list, including the this.setNotNull({...}) the planner rendered after the transform, stays as rendered):
import { col, Migration, MigrationCLI } from '@internal/postgres/migration';
import postgres from '@internal/postgres/runtime';
import type { Contract as End } from '../../snapshots/93f07d1b…c9e1e5a2/contract';
import endContract from '../../snapshots/93f07d1b…c9e1e5a2/contract.json' with { type: 'json' };
import type { Contract as Start } from '../../snapshots/f62a4154…d07dddc/contract';
import startContract from '../../snapshots/f62a4154…d07dddc/contract.json' with { type: 'json' };
const { sql: db, contract } = postgres<End>({ contractJson: endContract });
export default class M extends Migration<Start, End> {
override readonly startContractJson = startContract;
override readonly endContractJson = endContract;
override get operations() {
return [
this.addColumn({
schema: 'public',
table: 'user',
column: col('name', 'text', { codecRef: { codecId: 'pg/text@1' } }),
}),
this.dataTransform(contract, 'backfill-user-name', {
check: () => db.public.user.select('id').where((f, fns) => fns.eq(f.name, null)).limit(1),
run: () => db.public.user.update({ name: '' }).where((f, fns) => fns.eq(f.name, null)),
}),
this.setNotNull({ schema: 'public', table: 'user', column: 'name' }),
];
}
}
MigrationCLI.run(import.meta.url, M);
Self-emit:
node migrations/app/20260515T1200_add_user_name/migration.ts
Self-emit regenerates ops.json and recomputes migrationHash in migration.json. The next db migrate will see a consistent package.
Mongo
Mongo dataTransform operations are free factories taking { check, run } objects whose source / run return Mongo query-plan shapes (often RawAggregateCommand / RawUpdateManyCommand from @prisma/orm-mongo/query-ast/execution). The planner may leave placeholder(...) inside those sources until you fill them. A rendered package binds its bookends through startContractJson / endContractJson exactly as on Postgres; a hand-authored migration new package may instead override describe() with the from / to hashes from its migration.json, as below. Everything comes from one import:
import { createIndex, dataTransform, Migration, MigrationCLI } from '@prisma/orm-mongo/target/migration';
import { RawAggregateCommand, RawUpdateManyCommand } from '@prisma/orm-mongo/query-ast/execution';
class M extends Migration {
override describe() {
return { from: '<hex>', to: '<hex>' };
}
override get operations() {
return [
createIndex('users', [{ field: 'name', direction: 1 }]),
dataTransform('lowercase-user-name', {
check: {
source: () => ({
collection: 'users',
command: new RawAggregateCommand('users', [
{ $match: { name: { $regex: '[A-Z]' } } },
{ $limit: 1 },
]),
meta: { target: 'mongo', storageHash: '…', lane: 'mongo-pipeline', paramDescriptors: [] },
}),
},
run: () => ({
collection: 'users',
command: new RawUpdateManyCommand(
'users',
{ name: { $exists: true } },
[{ $set: { name: { $toLower: '$name' } } }],
),
meta: { target: 'mongo', storageHash: '…', lane: 'mongo-raw', paramDescriptors: [] },
}),
}),
];
}
}
export default M;
MigrationCLI.run(import.meta.url, M);
Self-emit the same way: node migrations/app/<dir>/migration.ts.
Workflow — Author a migration by hand
The concept: the same Migration class shape lets you author operations directly when the planner has nothing to plan (a custom data fix, an extension install, a baseline). Even here you don't write the file from scratch — migration new renders an empty package for you, and you edit the operations getter inside it, then self-emit.
pnpm prisma migration new --name <snake_slug>
On Postgres the operations are methods on the Migration base class, each taking one options object (this.addColumn({ schema, table, column })); only helpers such as col(...) and rawSql(...) are imported, on the rendered @internal/postgres/migration line. On Mongo the operations are free factories imported from @prisma/orm-mongo/target/migration. The authoritative list for either target is the base class / module's declaration file in your node_modules.
Postgres operations (representative set, all this.<name>({...})):
- Tables:
createTable,dropTable. - Columns:
addColumn(column: col(name, nativeType, { codecRef })),dropColumn,alterColumnType,setNotNull,dropNotNull,setDefault,dropDefault. - Constraints:
addPrimaryKey,addForeignKey,addUnique,addCheckConstraint,renameCheckConstraint,dropCheckConstraint,dropConstraint. - Indexes:
createIndex,renameIndex,dropIndex. - Enums:
createNativeEnumType,addNativeEnumValue,dropNativeEnumType. - Row-level security:
enableRowLevelSecurity,disableRowLevelSecurity,createRlsPolicy,renameRlsPolicy,dropRlsPolicy. - Dependencies:
createSchema,installExtension. - Free helpers on the import line:
col,primaryKey,unique,foreignKey,checkExpression,lit,fn(column and constraint descriptors),createExtension, and the raw escape hatchrawSql({ id, label, operationClass, target, precheck, execute, postcheck, ... }). - Data transforms:
this.dataTransform(endContract, name, { check, run }).
Mongo factories (from @prisma/orm-mongo/target/migration):
- Collections:
createCollection,dropCollection,validatedCollection,setValidation. - Indexes:
createIndex,dropIndex. - Collection options:
collMod. - Data transforms:
dataTransform(name, { check, run })(free factory;check/runuse Mongo query-plan shapes).
Self-emit (node migrations/app/<dir>/migration.ts) after each edit.
Workflow — Inspect the live schema
The concept: db schema is read-only and never writes files. It prints the live schema as a tree by default or as JSON with --json. Use it during planning and as part of verification.
pnpm prisma db schema --db $DATABASE_URL
pnpm prisma db schema --db $DATABASE_URL --json > schema.json
There is no built-in filter flag — pipe the JSON through jq (or your favourite JSON tool) if you only want one table.
Workflow — Verify contract vs DB (diagnostic)
The concept: db verify is a standalone diagnostic — not a routine step after db update or db migrate on the happy path (those commands already verify and advance the marker when they succeed). Reach for db verify when you suspect drift or need to prove the DB matches the contract:
- Following manual SQL or ad-hoc edits outside Prisma 8.
- When restoring a database from backup.
- If a
db migratefails or partially applies (especially on Mongo, where DDL is resumable rather than transaction-wrapped). - When
CONTRACT.MARKER_MISMATCH/CONTRACT.MARKER_MISSINGsurfaces at runtime or from another command.
Modes:
- Default — full verification (schema + marker).
--marker-only— skip schema verification, only check the marker.--schema-only— skip marker verification, only check schema satisfies contract.--strictadds: schema elements not present in the contract are an error (default is "DB may have extras").
pnpm prisma db verify --db $DATABASE_URL
db verify exits 0 when everything matches, 4 when it ran and found something, and 2 only when it could not run. Findings ride the completed envelope as error diagnostics: CONTRACT.MARKER_MISMATCH, CONTRACT.MARKER_MISSING, CONTRACT.TARGET_MISMATCH, CONTRACT.SCHEMA_VERIFICATION_FAILED (with meta.issues naming the drifted paths).
Workflow — Re-sign the marker
The concept: db sign rewrites the marker to the current contract hash and moves the db ref to it (--advance-ref <name> overrides the ref name; --db does not suppress the ref write; --no-advance-ref skips it). Use after a manual repair where the DB is the source of truth and the marker is stale. db sign performs a schema-verify first and refuses to sign a DB whose schema disagrees with the contract — so a successful sign always means the schema matches and the marker is now correct.
pnpm prisma db sign --db $DATABASE_URL
Workflow — Recover from drift
The concept: drift means db verify reports the live DB schema doesn't match what the marker says it should be. Two valid moves, picked by which side is correct:
- The contract is right; the DB is wrong → run a migration. Either
db update(quick path, dev DB only) ormigration plan+db migrate(everywhere else). - The DB is right; the contract or marker is wrong → edit the contract to match the DB (see
references/contract.md), emit, thendb signto refresh the marker. The sign also moves thedbref to the signed hash; when the migration graph is non-empty and that hash is not a graph node, the next defaultmigration planrefuses withMIGRATION.HASH_NOT_IN_GRAPH(see The forgot-the-flag pitfall above for the recovery).
The diagnostic that reveals which side is right:
pnpm prisma db schema --db $DATABASE_URL --json
pnpm prisma db verify --db $DATABASE_URL --json
Use db verify to confirm which side is wrong, then re-run it after either branch until it returns ok with no diagnostics.
Workflow — Recover from a partially-applied migration
The concept: on Postgres, the whole db migrate run is one transaction — a failure anywhere rolls back every migration the run had applied, and the marker stays where it was before the command. On Mongo, DDL is resumable with verify-gated marker advancement; diagnose with db verify / db schema, fix the failed package's migration.ts, self-emit, and re-run db migrate.
Failures that can leak partial state: Mongo DDL that partially applied before verify failed, and external side-effects (calls out to other systems from a run closure). On Postgres nothing runs outside the transaction — rawSql(...) steps are ordinary steps inside it and roll back with the rest.
Diagnose:
pnpm prisma db verify --db $DATABASE_URL --json
pnpm prisma db schema --db $DATABASE_URL --json
Fix and re-run db migrate:
node migrations/app/<dir>/migration.ts
pnpm prisma db migrate --db $DATABASE_URL
If the failure was an out-of-band side-effect that left external systems half-changed, repair those by hand before re-applying.
Workflow — Recover from MIGRATION.HASH_MISMATCH
The concept: migrationHash is content-addressed. A mismatch means migration.json's stored hash disagrees with the hash recomputed from ops.json (and metadata). The cause is almost always: someone edited migration.ts and forgot to self-emit. The remediation is to self-emit the offending package.
node migrations/app/<dir>/migration.ts
pnpm prisma db migrate --db $DATABASE_URL
If self-emit itself fails (e.g. the contract has moved on and the operations no longer make sense against the migration's end contract), the package is stale. Either restore it from version control or delete it and re-plan with migration plan.
Workflow — Resolve a destructive-operation prompt (db update only)
The concept: when db update would drop columns or tables, it stops and asks before applying. The prompt is db update-specific — db migrate does not prompt and runs whatever the migration package contains, so review the plan or call migration show before db migrate.
When db update reports destructive operations interactively, the warning lists them. The prompt is:
Apply destructive changes? This cannot be undone.
Routing:
- Answer yes if the data is no longer needed.
- Answer no, then either:
- Re-shape the migration via
migration planand hand-editmigration.tsto preserve the data (e.g. copy-to-new-column, then drop), or - Skip the destructive operation by reverting the contract change.
- Re-shape the migration via
Interactively, consent is typing the database name back (the prompt names it). In non-interactive contexts (CI, --no-interactive), the destructive-op response is returned as MIGRATION.DESTRUCTIVE_CHANGES — meta.destructiveOperations[] lists what would have been dropped. Re-run with --confirm <database> to grant consent (--yes does not), or address each operation individually.
Common Pitfalls
- Using
db updateagainst shared or production databases. Never. The change leaves no migration history. Usemigration plan+db migrate. - Skipping a data transform. Leaving
placeholder(...)inmigration.tsmakes the nextdb migratethrowMIGRATION.UNFILLED_PLACEHOLDER. Fill every placeholder slot and self-emit. - Editing
ops.jsondirectly. It's the canonical artifact, not the authoring source. Editmigration.ts, then self-emit. - Forgetting to self-emit after editing
migration.ts. The nextdb migrateeither uses the staleops.json(if you only added comments) or fails withMIGRATION.HASH_MISMATCH(if you changed operations). Always self-emit. - Routine
db verifyafter a successfuldb updateordb migrate. Redundant on the happy path — reservedb verifyfor drift diagnosis (manual edits, restore, faileddb migrate). - Aggregate
checkclosure in Postgresthis.dataTransform. Returningcount(*)orbool_and(...)breaks the precheck/postcheck contract — both sides resolve to constants. Use a rowset shape:select('id').where(<violation>).limit(1). - Two contract references in one migration. Building a query plan against a different contract than the one passed to
this.dataTransform(endContract, ...)raisesMIGRATION.DATA_TRANSFORM_CONTRACT_MISMATCH. Always importendContractonce at module scope and use the same reference. - Calling Postgres operations as free functions.
addColumn('public', 'user', {...})does not exist as an import; the operations arethis.addColumn({ schema, table, column })and friends on theMigrationbase class, withcol(...)building the column. Onlycol,rawSql,placeholder,Migration, andMigrationCLIare imported. - Renaming and expecting the planner to detect it (Postgres). Prisma 8 has no in-contract rename hint today; the planner emits a destructive drop+add. Hand-edit
migration.tsto rewrite the destructive op as arawSql({ ... })that issuesALTER TABLE ... RENAME COLUMN ...(or use the two-migration keep / backfill / drop pattern), then self-emit. Seereferences/contract.md§ Edit a field — rename. - Planning with no
dbref and no--fromin a project that already has migrations. The origin falls through to the empty database, which would make the plan a full-create migration;migration planrefuses withMIGRATION.PLAN_ORIGIN_UNKNOWNrather than writing it. Pick the exit that matches your intent — the error lists them, andreferences/migration-model.md§ The trap explains which to choose. - Hand-authoring
migration.tsfrom a blank file, or rewriting the rendered import line. Migration files are framework-rendered — letprisma migration plan(ormigration new) render the package, then edit only the holes the framework leaves for you. On Postgres leave the rendered@internal/postgres/migration(or@internal/sqlite/migration) import path alone; on Mongo leave@prisma/orm-mongo/target/migrationas rendered. Add symbols to the existing import line rather than introducing new import paths.
What Prisma 8 doesn't do yet
- Runtime-apply migrations. Prisma 8 doesn't apply pending migrations from your app's startup code (the "Drizzle pattern" for serverless / edge). Workaround: run
prisma db migratefrom your deploy pipeline before the app starts. If you need runtime-apply built-in, file a feature request via thereferences/feedback.mdskill. - Seeds-as-first-class. Prisma 8 doesn't ship a
prisma db seedequivalent. Workaround: write a TypeScript script that imports yourdbinstance and runs your setup queries; invoke it frompackage.json's scripts. If you need first-class seeding, file a feature request via thereferences/feedback.mdskill. - Migration squashing. Prisma 8 doesn't squash older migrations into a baseline. They accumulate; for very large histories, manual baseline-and-truncate is the path. If you need built-in squashing, file a feature request via the
references/feedback.mdskill. - In-contract rename hints. The planner cannot detect that a field rename is a rename rather than a drop+add. Workaround: hand-edit
migration.tsto issue aRENAME COLUMNviarawSql(...), or use a keep / backfill / drop pattern across two migrations. If you need a contract-level rename hint, file a feature request via thereferences/feedback.mdskill.
Graph and history commands
After planning or applying, you can inspect the migration graph offline:
pnpm prisma migration list— enumerate all on-disk migrations, rendered as a graph tree. Supports--legend(print the glyph key),--ascii(pipe-safe glyphs), and--json.pnpm prisma migration log --db $DATABASE_URL— flat chronological table of applied migrations, read from the live DB. Supports--asciiand--json.
For the full graph topology: pnpm prisma migration graph (also supports --legend, --ascii, --dot, --json).
@@control and DDL scope
Objects whose @@control policy excludes them from Prisma 8's managed surface are omitted from planned DDL. The four policies are: managed (Prisma plans and applies DDL), tolerated (object may exist, no DDL emitted), external (object is expected to exist, no DDL), observed (Prisma reads but never writes). Declare @@control(managed|tolerated|external|observed) in your schema; see references/contract.md and packages/2-sql/2-authoring/contract-psl/README.md for authoring syntax.
Telemetry
The CLI collects anonymous usage data by default. To opt out, set PRISMA_DISABLE_TELEMETRY=1 or DO_NOT_TRACK=1 in your environment. See docs/Telemetry.md for the full opt-out reference.
Checklist
- Contract emitted (
contract.json+contract.d.tscurrent). - Chose the right path:
db update(local dev) vsmigration plan+db migrate(anything shared). - For
migration plan: confirmed the output'sfrom:line names the intended origin — not(baseline)over an existing graph (references/migration-model.md). - For
migration plan: ranmigration show <dir>to review beforedb migrate. - Filled every
placeholder(...)inmigration.ts(if any), built againstendContract. -
checkclosures are rowset queries, not scalar aggregates. - Self-emitted (
node migrations/app/<dir>/migration.ts) after editing the TS. - Ran
db migrate(ordb update) and saw it complete. - Used
db verifyonly when diagnosing drift — not as a routine post-apply step. - Did NOT use
db updateagainst a shared or production database. - Did NOT edit
ops.jsondirectly. - Did NOT skip a destructive-op prompt without inspecting
meta.destructiveOperations[]; granted consent with the database name (or--confirm <database>), not--yes.
Referenced from SKILL.md
Source excerpt starting at line 46.SKILL.mdView in source ↗461. **Edit your data contract.** ([`references/contract.md`](references/contract.md))472. **The system plans the migrations for you.** ([`references/migrations.md`](references/migrations.md))483. **If you need data migrations, you edit `migration.ts` and execute it.** ([`references/migrations.md`](references/migrations.md))
Source excerpt starting at line 61.SKILL.mdView in source ↗61| Edit the data contract | [`references/contract.md`](references/contract.md) | schema, models, fields, attributes, relations, indexes, enums, value objects (composite types), type aliases, namespaces (Postgres schemas), cross-contract foreign keys (cross-space FK), polymorphic types (`@@discriminator` / `@@base`), extension namespaces (`pgvector.Vector(...)`, `postgis.Geometry(...)`), `prisma.config.ts` / `definePrismaConfig` / `ormConfig`, `prisma contract emit`, PSL, `contract.prisma`, `contract.ts`, `contract.json`, `contract.d.ts`, `@internal/postgres/config`, `@internal/postgres/contract-builder`, `@internal/mongo/config`, `extensions:`, pgvector, postgis, paradedb, Temporal / `temporal-polyfill` / `RUNTIME.TEMPORAL_UNAVAILABLE`, `@@control`, control policy (managed / tolerated / external / observed), soft delete, validations, callbacks |62| Author migrations | [`references/migrations.md`](references/migrations.md) | `db update` vs `migration plan`, `db migrate`, `migration new`, `migration show`, `db update --dry-run`, `db verify`, `db sign`, data migration, `dataTransform`, placeholder sentinels in framework-rendered `migration.ts`, `MIGRATION.HASH_MISMATCH`, `MIGRATION.UNFILLED_PLACEHOLDER`, `MIGRATION.DESTRUCTIVE_CHANGES` / `--confirm <database>`, schema drift |63| Migration graph, refs, plan origin | [`references/migration-model.md`](references/migration-model.md) | migration graph, refs, `migration ref set` / `list` / `delete`, the `db` ref, `--advance-ref`, `--no-advance-ref`, `migration plan --from`, `from: (baseline)` in plan output, greenfield / from-scratch plan, baseline, first migration before deploy (Composer / CD-managed databases), chaining migrations, retrofitting migrations onto an existing database, `MIGRATION.HASH_NOT_IN_GRAPH`, `MIGRATION.PATH_UNREACHABLE` at plan/chain time |
Source excerpt starting at line 82.82- *"Do you want to edit your data contract (add a model / field / relation), or work with the database (migrations, queries)?"* → [`references/contract.md`](references/contract.md) vs the others.83- *"Is this about authoring a migration, or about reviewing what's going to run on deploy?"* → [`references/migrations.md`](references/migrations.md) vs [`references/migration-review.md`](references/migration-review.md). If it's about where a plan starts, refs, or an unexpected from-scratch plan → [`references/migration-model.md`](references/migration-model.md).84- *"Is this about wiring Prisma 8 into your build tool (Vite / Next.js / …), or about wiring `db.ts` and middleware at runtime?"* → [`references/build.md`](references/build.md) vs [`references/runtime.md`](references/runtime.md).