diff --git a/.claude/skills/docs-reader-review/SKILL.md b/.claude/skills/docs-reader-review/SKILL.md index f188438e23e..2d16ae4ff98 100644 --- a/.claude/skills/docs-reader-review/SKILL.md +++ b/.claude/skills/docs-reader-review/SKILL.md @@ -27,6 +27,10 @@ Run `scripts/check-plain.sh` on every changed page. It fails on the words in `re Run `scripts/check-staccato.py` on every changed page. It flags paragraphs with three or more consecutive sentences under nine words, the usual signature of a page whose long sentences were split without keeping the connectives. Read each flagged paragraph aloud. Rejoin sentences that are cause and effect, contrast, or condition and result with "because", "so", "but", "while", or a colon, and leave apart the ones that are separate ideas. The script only finds the worst runs; a paragraph of eleven-word sentences that all land the same way is still staccato, so read the whole page for rhythm, not only the hits. Reference entries that are fragments by design (a `Payload:` line, a one-line table note) are not prose and do not count. +### 1c. Read for explanation, not statement + +Read `references/explain-not-state.md` before fixing anything. It names the habit that a plain-language pass and a word budget both produce: true, short sentences that state a fact and never say what it means for the reader. The staccato check flags the mechanical signs (counting lead-ins like "Four things change it:", fragment openers like "One name is special."), but most of the work is the desk test in that file: read each paragraph aloud as if to a colleague, and add the sentence you would say out loud. A page may grow when it gains explanation. Word budgets are for repetition only. + ### 2. Dispatch the reader Hand the page to a fresh reviewer that has no memory of writing it, using `references/reader-persona.md` verbatim as its instructions. The reviewer reads the page once, top to bottom, with nothing else open, and reports: diff --git a/.claude/skills/docs-reader-review/references/banned-terms.md b/.claude/skills/docs-reader-review/references/banned-terms.md index eab50857197..4670a319100 100644 --- a/.claude/skills/docs-reader-review/references/banned-terms.md +++ b/.claude/skills/docs-reader-review/references/banned-terms.md @@ -35,6 +35,24 @@ These words come from the source code and the release notes. Readers do not have Also banned as verbs and phrases: "unblocks", "surfaces", "carries", "wires up", "sits", "lives at" (write "is at"), "spells", "hands back", "rides on", "routes on", "keys on", "the shape" (unexplained), "story" (as in "the `DATABASE_URL` story"). +## Migration vocabulary + +Migrations invite movement metaphors: the database travels, the ref moves, the run goes forward. Readers do not picture a database moving. Say what changes, and what it ends up matching. The metaphor is fine where it carries meaning a literal sentence loses, such as a path through the migration graph, or "roll back", which is the name of the thing. + +| Do not write | Write | Why | +| --- | --- | --- | +| `db migrate` moves your database from where it is to where you want it | `db migrate` applies the migrations you planned, until your database matches your contract | `db migrate` runs migration files. Only `db update` reconciles a database against the contract with no migration | +| moves the database to the contract state | applies migrations until the database matches the contract state | | +| move the database back to an earlier state | the database needs an earlier version of your contract | "roll back" stays: it is the name of the operation | +| the marker moves | `db migrate` updates the marker; the marker still records the last contract state applied | The marker is a record in the database, not a position | +| moves the `db` ref | points the `db` ref at that state; updates the `db` ref | A ref is a name for one contract state | +| the database is behind and has to catch up | the database is several contract states out of date | | +| it takes the shortest path to the contract state | it runs the fewest migrations that end at that state | Keep "path" where the page is about the graph itself | + +`from` and `to` are field names in `migration.json`, so "starts `from`" and "ends at, `to`" are literal, not metaphors. + +`scripts/check-plain.sh` enforces the database, marker and ref rows. "Database schema" is the ordinary term for the tables, columns and constraints, and stays allowed. It does not enforce "path", because the graph page uses that word correctly, or "the database is behind", because sample prompts quote a user's own words. The reader review judges those two. + ## Sentence shapes to avoid - A noun with three or more modifiers: "per-request cursor-enabled serverless facade". diff --git a/.claude/skills/docs-reader-review/references/explain-not-state.md b/.claude/skills/docs-reader-review/references/explain-not-state.md new file mode 100644 index 00000000000..3df923bb494 --- /dev/null +++ b/.claude/skills/docs-reader-review/references/explain-not-state.md @@ -0,0 +1,51 @@ +# Explain, do not state + +## The problem + +Pages written by an agent, and pages that went through word budgets, drift into prose that states facts without explaining them. Each sentence is true, short, and correct, and the reader still has to work out what it means for them. The habit shows up as: + +- **Fragment openers.** A two-to-five word sentence that announces the next one: "One name is special." "Recompile." "Drift shows up in two ways." The announcement carries no information, and the sentence after it has to start over. +- **Counting lead-ins.** A list introduced by how many items it has instead of what the items are for: "Four things change it:", "Two things cause this:", "Three things make a failure easy to recover from:", "'Baseline' means three things:". The count is the writer's bookkeeping. The reader wants to know what kind of thing is coming and why it matters. +- **Mechanism before meaning.** The sentence says what the system does and never says what that means for the reader: "A ref called `db` is what `migration plan` starts from when you do not pass `--from`." True, and the reader still does not know what the `db` ref is for. +- **Facts side by side.** Two sentences that are cause and effect, or condition and result, sit next to each other with no "so", "because", "when", or "if" between them. The reader has to infer the link. +- **Lists doing the explaining.** Reasoning that belongs in a paragraph is chopped into bullets with bold labels, so the connective tissue is gone and each bullet is a spec line. +- **Sentences trimmed to the fact.** Every clause that would have said "which means", "in other words", or "you would notice this when" was cut to meet a budget. + +Word budgets cause the last two directly, and make all the others worse. A budget is for cutting repetition, never for cutting explanation. + +## The model + +The `migration ref` page said: + +> One name is special. A ref called `db` is what `migration plan` starts from when you do not pass `--from`, so keeping it current is what keeps plans incremental. +> +> Four things change it: + +Will rewrote it as: + +> The `db` ref has a special meaning in Prisma 8: it tells the system where you expect your local database to be. When you run a command like `prisma migrate`, if you don't explicitly say `--from`, Prisma will assume you mean from the `db` ref. +> +> The `db` ref will be automatically updated in the following situations: + +What changed: + +1. **Meaning first.** The first sentence says what the thing is for, from the reader's side: it records what you expect about your own database. The mechanism comes second. +2. **Conditions as scenarios.** "When you run X, if you don't say Y, Prisma will assume Z." The reader can picture themselves doing it. Compare "is what X starts from when you do not pass Y", which describes the system from outside. +3. **The lead-in says what the list is.** "will be automatically updated in the following situations" tells the reader what kind of items follow and what they have in common. The count is gone. +4. **Longer, and connected.** Every sentence has a "when", "if", or "so". Nothing is left for the reader to infer. +5. **Plain words at the moments that matter.** "has a special meaning", "tells the system", "will assume you mean". These are how a colleague talks, not how a spec reads. + +## The rules + +1. **Say what it means before you say how it works.** For every term, command, file, or flag a page introduces, the first sentence about it answers "what is this for, from my side of the screen?" The mechanism follows. +2. **Walk conditions as scenarios.** Write "When you run X without Y, Prisma ORM does Z", not "Z happens unless Y is passed to X". Put the reader in the sentence. +3. **Connect adjacent facts.** If two sentences are cause and effect, condition and result, or contrast, join them with "so", "because", "when", "if", "but", or "which means". If they are separate ideas, leave them apart, and check that the paragraph still says why they are next to each other. +4. **Introduce a list by what it is, not how long it is.** Never count the items in the lead-in. Say what the items have in common and why the reader is about to read them: "The `db` ref is updated automatically in the following situations:", "A failure is easy to recover from because:". +5. **No fragment openers.** Fold a two-to-five word announcing sentence into the sentence it announces, or cut it. +6. **Explain in prose, list to enumerate.** A bulleted list is for items of the same kind that the reader will scan. Reasoning, trade-offs, and "this happens because" go in paragraphs. +7. **Budgets cut repetition, not explanation.** A sentence that explains what something means may be 25 words. A page may grow when it gains explanation. A definition repeated three times is still cut. +8. **The desk test.** Read the paragraph aloud as if explaining it to a colleague at the next desk. Wherever you would say "basically, what this means is...", the page needs that sentence. Wherever you would never say the sentence aloud ("Four things change it."), rewrite it. + +## What the checker catches + +`scripts/check-staccato.py` flags runs of short sentences, counting lead-ins, and fragment openers. It cannot judge whether a sentence explains or only states. The reader review and the desk test do that. diff --git a/.claude/skills/docs-reader-review/scripts/check-plain.sh b/.claude/skills/docs-reader-review/scripts/check-plain.sh index 8c8cdef3310..d036687b985 100755 --- a/.claude/skills/docs-reader-review/scripts/check-plain.sh +++ b/.claude/skills/docs-reader-review/scripts/check-plain.sh @@ -9,7 +9,7 @@ # inline code, link targets, pinned heading anchors, href attributes, the front-matter url line, and any line carrying the marker {/* plain-language:defined */}, which # an author adds only when the sentence itself defines the term in plain words. set -u -pat='\bterminal (calls?|methods?|verbs?)\b|\b(read|write|query) terminals?\b|\bcodecs?\b|\benvelopes?\b|\btype position\b|\brelease (line|cycle)\b|\bruns ahead\b|\b(sql|raw|orm) lanes?\b|\blane object\b|\bfacades?\b|\bjunction (model|record|table)\b|\bjoin model\b|\bquery plans?\b|\blower(ed|ing)\b|\bnamespace-qualified\b|\bnamespace coordinate\b|\breducers?\b|\brefinement callback\b|\bdotted codes?\b|\b(CLI|config) envelope\b|\bdev loop\b|\bbrownfield\b|\bgreenfield\b|\bdeterministic\b|\bidempotent\b|\btopology\b|\bcapabilit(y|ies)\b|\bbuffer(s|ed)? (the|every|all)\b|\bmaterializ(e|es|ed)\b|\bunblocks?\b|\bAPI surface\b|\bsurfaces? (the|a|an|as)\b|\bwires? up\b|\blives at\b|\bhands back\b|\brides on\b|\broutes on\b|\bkeys on\b' +pat='\bterminal (calls?|methods?|verbs?)\b|\b(read|write|query) terminals?\b|\bcodecs?\b|\benvelopes?\b|\btype position\b|\brelease (line|cycle)\b|\bruns ahead\b|\b(sql|raw|orm) lanes?\b|\blane object\b|\bfacades?\b|\bjunction (model|record|table)\b|\bjoin model\b|\bquery plans?\b|\blower(ed|ing)\b|\bnamespace-qualified\b|\bnamespace coordinate\b|\breducers?\b|\brefinement callback\b|\bdotted codes?\b|\b(CLI|config) envelope\b|\bdev loop\b|\bbrownfield\b|\bgreenfield\b|\bdeterministic\b|\bidempotent\b|\btopology\b|\bcapabilit(y|ies)\b|\bbuffer(s|ed)? (the|every|all)\b|\bmaterializ(e|es|ed)\b|\bunblocks?\b|\bAPI surface\b|\bsurfaces? (the|a|an|as)\b|\bwires? up\b|\blives at\b|\bhands back\b|\brides on\b|\broutes on\b|\bkeys on\b|\bmoves? (your|the|a) database (to|from) (the|an|a|where|one|its)\b|\bmoves? (your|the|a) database (to|from)[,.]|\bmoves? (your|the|a) database (back|forward)\b|\bmoves? the marker\b|\bmarker moves\b|\bmoves? (the|your) +refs?\b|\bspaces? moves? to\b|\bdatabases? (to )?catch(es)? up\b|\bwhere (the|a|your|my) database is\b' status=0 for f in "$@"; do hits=$(awk ' @@ -19,7 +19,7 @@ for f in "$@"; do /plain-language:defined/ { next } /^url: / { next } { print NR": "$0 } - ' "$f" | sed -E 's/`[^`]*`//g; s/\]\([^)]*\)/]/g; s/\[#[^]]*\]//g; s/href="[^"]*"//g' | grep -E -i "$pat" || true) + ' "$f" | sed -E 's/`(to|from)`/\1/g; s/`[^`]*`//g; s/\]\([^)]*\)/]/g; s/\[#[^]]*\]//g; s/href="[^"]*"//g' | grep -E -i "$pat" || true) if [ -n "$hits" ]; then status=1; echo "== $f"; echo "$hits" | cut -c1-160; fi done if [ $status -eq 0 ]; then echo "plain-language check: clean"; else echo "plain-language check: banned terms found"; fi diff --git a/.claude/skills/docs-reader-review/scripts/check-staccato.py b/.claude/skills/docs-reader-review/scripts/check-staccato.py index eeb2d11ec88..71316db58a7 100755 --- a/.claude/skills/docs-reader-review/scripts/check-staccato.py +++ b/.claude/skills/docs-reader-review/scripts/check-staccato.py @@ -1,10 +1,14 @@ #!/usr/bin/env python3 -"""Flags staccato prose: runs of short, one-clause sentences with no connective between them. +"""Flags staccato prose: runs of short, one-clause sentences with no connective between them, +counting lead-ins ("Four things change it:"), and fragment openers ("One name is special."). Usage: check-staccato.py [more files...] Exit 1 on any hit. -A hit is a paragraph containing three or more consecutive sentences that are each -under MAX_WORDS words. Fenced code, tables, headings, list items, front matter, and +A run hit is a paragraph containing three or more consecutive sentences that are each +under MAX_WORDS words. A lead-in hit is a paragraph ending in a colon that introduces its +list by counting the items. An opener hit is a paragraph whose first sentence has +OPENER_WORDS words or fewer and is followed by more sentences. See +references/explain-not-state.md for why these read badly and what to write instead. Fenced code, tables, headings, list items, front matter, and import lines are skipped. Colons that introduce a code span or example are not treated as sentence ends. The heuristic is deliberately simple: it finds the paragraphs a human should read aloud, and the reader decides whether the sentences @@ -17,6 +21,14 @@ MAX_WORDS = 9 RUN = 3 +OPENER_WORDS = 5 +COUNT = r"(one|two|three|four|five|six|seven|eight|nine|ten|several|a few|a couple of|\d+)" +COUNT_LEADIN = re.compile( + r"(\b" + COUNT + r" (things|commands|cases|reasons|situations|ways|fixes|steps|parts|rules|options|" + r"kinds|forms|labels|files|conditions|causes|places|questions|points)\b[^.:]*:$" + r"|\bmeans " + COUNT + r" things\b|^(here are|there are) " + COUNT + r"\b)", + re.I, +) CODE = re.compile(r"`[^`]*`") LINK = re.compile(r"\]\([^)]*\)") @@ -75,8 +87,13 @@ def check(path): run = run + 1 if is_short else 0 if run == RUN: first = j - RUN + 1 - hits.append((lineno, " | ".join(sents[first : j + 1]))) + hits.append((lineno, "run: " + " | ".join(sents[first : j + 1]))) break + plain = LINK.sub("]", CODE.sub("CODE", para)).strip() + if COUNT_LEADIN.search(plain): + hits.append((lineno, "counting lead-in: " + plain)) + if len(sents) > 1 and len(sents[0].split()) <= OPENER_WORDS and sents[0].endswith("."): + hits.append((lineno, "fragment opener: " + sents[0] + " | " + sents[1])) return hits @@ -89,7 +106,7 @@ def main(argv): print(f"== {path}") for lineno, snippet in hits: print(f"{lineno}: {snippet[:180]}") - print("staccato check: clean" if status == 0 else "staccato check: clipped runs found, read them aloud") + print("staccato check: clean" if status == 0 else "staccato check: hits found, read them aloud (references/explain-not-state.md)") return status diff --git a/apps/docs/content/docs/cli/migration-ref.mdx b/apps/docs/content/docs/cli/migration-ref.mdx index 4a9277c8871..a07645e9011 100644 --- a/apps/docs/content/docs/cli/migration-ref.mdx +++ b/apps/docs/content/docs/cli/migration-ref.mdx @@ -12,14 +12,15 @@ Refs live on disk as `migrations/app/refs/.json`, so they are versioned wi ## The db ref -One name is special. A ref called `db` is what [`migration plan`](/cli/migration-plan) starts from when you do not pass `--from`, so keeping it current is what keeps plans incremental. +The `db` ref has a special meaning: it records the contract state you expect your local database to match. When you run [`migration plan`](/cli/migration-plan) without `--from`, Prisma ORM assumes you mean from the `db` ref, so as long as the ref is kept up to date, each plan contains only your latest change. -Four things move it: +The `db` ref is updated automatically in the following situations: -- [`db init`](/cli/db-init) and [`db update`](/cli/db-update) advance it automatically when you run them without `--db` and let the connection come from `prisma.config.ts`. Passing `--db` suppresses that unless you also pass `--advance-ref db`. -- [`db sign`](/cli/db-sign) advances it after a successful signature, with or without `--db`. `--no-advance-ref` turns that off. -- [`db migrate --advance-ref db`](/cli/db-migrate) advances it after an apply. Plain `db migrate` never advances anything, which is deliberate: a deploy or CI apply should not move a repository ref. -- `migration ref set db ` sets it by hand. +- [`db init`](/cli/db-init) and [`db update`](/cli/db-update) update it when you run them without `--db`, so that the connection comes from `prisma.config.ts`. With `--db`, they leave it alone unless you also pass `--advance-ref db`. +- [`db sign`](/cli/db-sign) updates it after a successful signature, with or without `--db`. `--no-advance-ref` turns that off. +- [`db migrate --advance-ref db`](/cli/db-migrate) updates it after an apply. Plain `db migrate` never touches it, on purpose: a deploy or CI run should not change a file in your repository. + +You can also set it by hand with `migration ref set db `. ## Usage @@ -45,4 +46,4 @@ npx prisma@latest migration status --db "$DATABASE_URL" --to production npx prisma@latest db migrate --db "$DATABASE_URL" --to production ``` -Use refs when environments need named targets instead of always applying to the latest migration on disk. Several commands can also move a ref forward for you after they succeed when you pass `--advance-ref `. +Use refs when you want to name the contract state an environment should match, instead of always applying up to the latest migration on disk. If you pass `--advance-ref ` to a command that changes the database, that command also points the ref at the state it applied, once it succeeds. diff --git a/apps/docs/content/docs/guides/database/schema-changes.mdx b/apps/docs/content/docs/guides/database/schema-changes.mdx index 49b021e1058..d1a12e86461 100644 --- a/apps/docs/content/docs/guides/database/schema-changes.mdx +++ b/apps/docs/content/docs/guides/database/schema-changes.mdx @@ -48,7 +48,7 @@ Bring my local database up to date with the migrations on this branch using Pris ### 1.1. Migrations form a graph -Each migration directory records the contract state it starts `from` and the state it moves the database `to`, as hashes in its `migration.json`. Those links are the migration history. The timestamp in the directory name is for humans; nothing depends on it. That is why two developers can plan migrations from the same state on separate branches and merge without renaming anything. [The migration graph](/orm/migrations/the-migration-graph) explains the model in full. +Each migration directory records the contract state it starts `from` and the state it ends at, `to`, as hashes in its `migration.json`. Those links are the migration history. The timestamp in the directory name is for humans; nothing depends on it. That is why two developers can plan migrations from the same state on separate branches and merge without renaming anything. [The migration graph](/orm/migrations/the-migration-graph) explains the model in full. Three commands answer the questions a team asks most: @@ -73,7 +73,7 @@ Do not commit `.env`. `orm init` already lists it in `.gitignore`. ### 1.3. Where a plan starts -`migration plan` is offline. It never asks the database where it is, so you have to tell it which state to plan from. It resolves the origin in this order: an explicit `--from`, otherwise the ref named `db` in `migrations/app/refs/db.json`, otherwise the empty database. Keep the `db` ref pointing at the state your local database is on by applying with `--advance-ref db`: +`migration plan` is offline. It never asks the database which contract state it matches, so you have to tell it which state to plan from. It resolves the origin in this order: an explicit `--from`, otherwise the ref named `db` in `migrations/app/refs/db.json`, otherwise the empty database. Keep the `db` ref pointing at the state your local database matches by applying with `--advance-ref db`: ```npm npx prisma@latest db migrate --advance-ref db @@ -138,7 +138,7 @@ The `{bin}` in the hint is the CLI's placeholder for however you invoked it. Rea ### 2.1. Pull, check, apply -When a teammate merges a schema change, it arrives as three things: an edited `contract.prisma`, the re-emitted `contract.json` and `contract.d.ts`, and a new migration directory. Pull them, then ask Prisma where your database is: +When a teammate merges a schema change, it arrives as three things: an edited `contract.prisma`, the re-emitted `contract.json` and `contract.d.ts`, and a new migration directory. Pull them, then ask Prisma which contract state your database matches: ```bash git pull @@ -158,7 +158,7 @@ If nothing changed, you see the whole history marked applied: ✔ Up to date ``` -`@db` marks where your database is, `@contract` where the emitted contract is, and `(db)` where the `db` ref points. When migrations are pending, the tree flags each one with `⧗ pending` and a summary line tells you how many. Apply them and move the ref in one step: +`@db` marks the contract state your database matches, `@contract` the emitted contract, and `(db)` the state the `db` ref names. When migrations are pending, the tree flags each one with `⧗ pending` and a summary line tells you how many. Apply them and update the `db` ref in one step: ```npm npx prisma@latest db migrate --advance-ref db @@ -330,7 +330,7 @@ npx prisma@latest contract emit storageHash: cb575cf009f55f442986c9f51655d0b48d40a8157f05427d525fe114e5f90b52 ``` -- `migrations/app/refs/db.json` records where a local database was. Each branch advanced it to its own state, and neither is where your database is. Take either side; you will set it correctly when you apply in section 3.5. +- `migrations/app/refs/db.json` records the contract state a local database last matched. Each branch pointed it at its own state, and neither is the state your database matches now. Take either side; you will set it correctly when you apply in section 3.5. ```bash git checkout --ours migrations/app/refs/db.json @@ -472,9 +472,9 @@ git add -A git commit -m "Merge javier/tag-model and plan merge migrations" ``` -### 3.6. Every database catches up on its own path +### 3.6. Every database runs only the migrations it needs [#36-every-database-catches-up-on-its-own-path] -Ania pulls `main`. Her database is on `9bba644`, so only the migration that adds Javier's table is pending for her: +Ania pulls `main`. Her database matches `9bba644`, so only the migration that adds Javier's table is pending for her: ```npm npx prisma@latest migration status diff --git a/apps/docs/content/docs/orm/core-concepts.mdx b/apps/docs/content/docs/orm/core-concepts.mdx index e4a6290357c..dc0345de8ec 100644 --- a/apps/docs/content/docs/orm/core-concepts.mdx +++ b/apps/docs/content/docs/orm/core-concepts.mdx @@ -159,11 +159,11 @@ Three middleware ship with Prisma ORM at present, and they are in the early stag ## Migrations: a graph of contracts -A **migration** is a recorded change that moves the database's schema from the state one contract describes to the state another describes. It is a directory in your repository containing the change as editable TypeScript (`migration.ts`), the compiled operations that Prisma runs (`ops.json`), and metadata recording which contract hash it starts `from` and moves `to`. +A **migration** is a recorded change to your database. Most migrations change the database schema from what one contract describes to what another describes, so each one records which contract hash it starts `from` and which it ends at, `to`. A migration that only changes rows, such as a backfill, starts and ends at the same contract state. On disk, a migration is a directory in your repository that holds the change as editable TypeScript (`migration.ts`), the compiled operations that Prisma ORM runs (`ops.json`), and that `from` and `to` metadata (`migration.json`). -One command applies them: [`db migrate`](/cli/db-migrate) advances a live database along the recorded migrations. The other `migration ...` commands never touch a database; they create and inspect the migration files in your repository. +Only one of these commands changes a database: [`db migrate`](/cli/db-migrate) applies the recorded migrations to it. The other `migration ...` commands create and inspect the migration files in your repository, and never connect to a database. -Because every migration records its `from` and `to` hashes, the migrations in a repository form a **graph**: contracts are the nodes, migrations are the edges. When two branches each add a migration and both merge, the graph has a fork and a join, and `db migrate` finds a path from wherever a database currently is to wherever you want it to be. No renumbering, no rebasing migration files. +Because every migration records its `from` and `to` hashes, the migrations in a repository form a **graph**: contracts are the nodes, migrations are the edges. When two branches each add a migration and both merge, the graph has a fork and a join, and `db migrate` works out which migrations to run, given the contract state the database matches and the one you name. No renumbering, no rebasing migration files. A **ref** is a named pointer at a contract, such as `production` or `staging`, managed with [`migration ref`](/cli/migration-ref). Refs let deployment commands target an environment by name: `db migrate --to production`. 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..939463c400a 100644 --- a/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/applying-a-migration.mdx @@ -1,176 +1,178 @@ --- 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 applies the migrations you planned, until your database matches your contract, 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: +Once you have planned migrations with `npx prisma migration plan`, `npx prisma db migrate` is the command that runs them against a database, until that database matches your contract. It replaces Prisma ORM 7's [`prisma migrate deploy`](/orm/coming-from-prisma-orm-7#commands), and works on the [supported databases](/orm/supported-databases), PostgreSQL and MongoDB, with SQLite still experimental. ```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. +Prisma ORM decides what to run by comparing two records: the contract you want, and the contract state the database has. Each version of your contract, the `contract.prisma` file, is a **contract state**, and the database itself stores a [**marker**](/orm/migrations/the-migration-graph#terms-used-on-this-page), which is the record of which contract state that database matches. So when you run `db migrate`, it reads the marker to find the contract state the database already matches, and then runs the migrations from that state to the contract you last emitted with [`npx prisma contract emit`](/cli/contract-emit). -By default, the connection comes from `db.connection` in `prisma.config.ts`. Pass `--db` to override it: +`migration status`, `migration log`, and the `db` commands connect to a database using `db.connection` in `prisma.config.ts`, as [The data contract](/orm/contract-authoring/the-data-contract) shows. When you want to work against a different database, production rather than your own machine for example, pass `--db` with that database's connection string, as in `--db "$PRODUCTION_DATABASE_URL"`. -```npm -npx prisma@latest db migrate --db $DATABASE_URL -``` - -A successful run reports every operation it performed and the marker that it left behind: +When a run succeeds, it shows you what it did: each migration's operations, and then the marker it wrote: ```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 +├─ Add column "displayName" to "user" +├─ Data transform: backfill-user-displayName +├─ ⚠ Set NOT NULL on "user"."displayName" +└─ marker e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63 -Next: prisma migration status +⚠ This migration contains destructive operations that may cause data loss. ``` -`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). +Each `├─` line under `App space` is an operation, one step of an app migration. A data transform changes existing rows, here filling in `displayName`, and you write its queries in `migration.ts`, as [Editing a migration](/orm/migrations/editing-a-migration) shows. A `⚠` marks a [destructive](/orm/migrations/how-migrations-work#every-operation-checks-itself) operation, one that removes or changes something that already exists, such as setting `NOT NULL`, which deletes no rows but still counts. `db migrate` runs destructive operations without asking you to confirm, and there is no flag that makes it refuse, so the time to catch one is while you are reviewing the migration rather than while it is running. During review, run `npx prisma migration show ` with the migration's directory name in `migrations/app/`, and it prints the same warning. ## Check before, preview, then apply -When working on shared databases, it helps to always follow these three steps: +Before you apply migrations to staging or production, run the following commands in order, so that you check the migration files, then see what would run, and only then run it: ```npm -# 1. Where is the database, what's pending? -npx prisma@latest migration status --db $DATABASE_URL +# 1. Were any migration files edited by hand or deleted? Needs no database. +npx prisma migration check -# 2. What exactly would run? -npx prisma@latest db migrate --show --db $DATABASE_URL +# 2. Which migrations would run? +npx prisma db migrate --show --db "$PRODUCTION_DATABASE_URL" -# 3. Run it. -npx prisma@latest db migrate --db $DATABASE_URL +# 3. Run them. +npx prisma db migrate --db "$PRODUCTION_DATABASE_URL" ``` -`migration status` draws the path between the database's marker and the target, flagging each migration as applied or pending: +`npx prisma migration status --db "$PRODUCTION_DATABASE_URL"` never changes the database, so you can run it against production whenever you want to know which contract state that database matches. It draws your migration history, marks each migration as applied or pending, and shows which contract state the database matches: ```text -* 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` +○ 925198f @contract +│↑ 20260707T1006_add_user_phone 705b1a6 → 925198f 1 ops ⧗ pending +○ 705b1a6 @db (db) +│↑ 20260707T1005_init ∅ → 705b1a6 2 ops ✓ applied +○ ∅ ``` -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 diagram from the bottom up, because it starts with an empty database and ends with the contract you last emitted: `∅` is an empty database, each `○` line is a contract state shown by the first 7 characters of its hash, and each `│↑` line is a migration from the state below it to the one above. You never have to type a whole hash when you name one of these states, because commands accept any prefix of 6 or more characters that matches one hash. The labels on the right say what points at each state: -`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: +- `@contract` is the contract you last emitted. +- `@db` is the contract state the database's marker records. +- `(db)` is the `db` [ref](/orm/migrations/the-migration-graph#name-important-states-with-refs), a file in `migrations/app/refs/` that names the contract state you last applied in development. + +When migrations are pending, `migration status` also prints a `db migrate --to ` command that you can copy, and if that hash is your `@contract` state you can leave `--to` off, because that is the state `db migrate` runs to anyway. + +`db migrate --show` is the preview, and it changes nothing: it draws the same history, marks each migration it would apply with `will run`, and lists them in the order they would 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 1 migration will run: -The following 2 migrations will run: - 20260707T1005_init ∅ → 705b1a6 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: +`--show` tells you which migrations will run, but not what is inside them, so when you want one migration's operations and its SQL, run `npx prisma migration show 20260707T1006_add_user_phone` instead, as [Reviewing what you planned](/orm/migrations/generating-a-migration#reviewing-what-you-planned) shows. + +After applying, `npx prisma migration log --db "$PRODUCTION_DATABASE_URL"` shows you what that database has actually had applied to it, rather than what your local files say: it prints the **ledger**, the database's own list of every migration applied to it. Like `--show`, it changes nothing: ```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: +By default `db migrate` runs everything that is pending, so when you want to stop part way, name the contract state you want to stop at with `--to`. The example below uses `prod`, a ref created with `npx prisma migration ref set prod `, which points `prod` at the state after that migration: ```npm -npx prisma@latest db migrate --to prod --db $DATABASE_URL +npx prisma db migrate --to prod --db "$PRODUCTION_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. +When the state you want is an earlier one than the database already matches, plan a rollback migration first, as [Rollbacks and recovery](/orm/migrations/rollbacks-and-recovery) shows, because `db migrate` only ever runs migrations you already have. That is also why, if no migrations lead from the state the database matches to the one you name, the run fails with an error whose `code` is `MIGRATION.PATH_UNREACHABLE`. The [`db migrate` reference](/cli/db-migrate#options) lists every form of `--to` and the 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, `db migrate` stops and prints what failed and why: ```text -✖ Operation pgvector.install-vector-extension failed during execution: create extension "vector" (MIGRATION.RUNNER_FAILED) - Why: extension "vector" is not available - Fix: Fix the issue and re-run `prisma db migrate --to ` — previously applied migrations are preserved. +✘ [MIGRATION.RUNNER_FAILED] Operation pgvector.install-vector-extension failed during execution: create extension "vector" + why: extension "vector" is not available + docs: https://docs.prisma.io/docs/orm/v8/reference/error-reference/MIGRATION.RUNNER_FAILED ``` -Three properties make failure manageable instead of something to fear: +Here, an operation from the pgvector [extension package](#extension-spaces) failed because the `vector` PostgreSQL extension is not available on the database server. Install it there, or enable it in your database host's settings, and then run `db migrate` again. -- **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. +On PostgreSQL you can do that without any tidying up first, because a failed run leaves no changes in the database at all. The whole run is one transaction, so everything the failed run did is rolled back, while migrations applied in *earlier* runs stay applied. The error itself tells you where it stopped, because it names the operation and the failing step, which is either the SQL statement or a [check before or after it](/orm/migrations/how-migrations-work#every-operation-checks-itself), such as the check that no row still holds `NULL` before a column becomes `NOT NULL`. And when you fix the problem and run the command again, it does not repeat work that is already done, because `db migrate` skips each operation whose change is already in the database, unless the operation has no postcheck. -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. +MongoDB does not give you that clean slate, because a run there is not one transaction. Operations that finished before the failure stay in the database, and a re-run stops at the precheck of any collection the failed run already created. -## Development vs. production +`db migrate` also refuses to start when the marker records a contract state that none of your migrations starts or ends at, because it cannot then work out which migrations to run. It fails with an error whose `code` is `MIGRATION.MARKER_MISMATCH`, and no operation runs. You see this when the database's contract state was set by something other than the migrations you have on disk, which happens in the following situations: -The commands are the same everywhere: what changes is where the files come from and who runs them. +- **`npx prisma db update`**, which replaces `prisma db push` for local development and preview environments. Before your next contract change, run `npx prisma migration plan --from --name ` with your most recent migration's directory name. The new migration ends at the state `db update` applied, as [Drift](/orm/migrations/rollbacks-and-recovery#drift-when-the-database-isnt-where-migrations-left-it) shows. +- **A migration from a Git branch you have not merged**, which a shared database such as staging already applied. Merge that branch so that you have the migration too, and if your branch also added migrations, plan merge migrations, as [A worked example](/orm/migrations/the-migration-graph#a-worked-example) shows. -**In development**, you're the one planning and editing, and you apply immediately: +A database that already had tables gets its marker from `npx prisma db sign`, so `db migrate` refuses to run against it until a migration in your history ends at the signed state, as [The automatic baseline](/orm/migrations/generating-a-migration#the-automatic-baseline) shows. For a database Prisma ORM 7 migrated, follow [Transfer migration ownership](/guides/upgrade-prisma-orm/postgresql#4-transfer-migration-ownership) in the upgrade guide. -```npm -npx prisma@latest migration plan --name my_change && npx prisma@latest db migrate --advance-ref db -``` +## Development vs. production + +The command is the same everywhere, and what changes is the work you do around it. -**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 development**, you emit the contract, plan a migration, review it, and then apply it: ```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 contract emit +npx prisma migration plan --name my_change +# Review the new migration directory before you apply it. +npx prisma db migrate --advance-ref db ``` -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. +`--advance-ref db` is what keeps your local development in step, because it points the `db` ref at the state you just applied, so the next `migration plan` starts from there. You only want that on your own machine, so leave the flag off in CI and production, and see [The db ref](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) for what happens without it and how to fix a missed flag. If you edit `migration.ts` while reviewing the migration, recompile it with `node migrations/app//migration.ts`, which is safe to do at any point because recompiling only rewrites files and never connects to a database, as [Editing a migration](/orm/migrations/editing-a-migration) explains. -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. +**In CI and production**, you never plan a migration, because migrations arrive through your repository, already reviewed and merged. For that to work, commit each migration's directory, the directories `migration plan` added under `migrations/snapshots/`, one per contract state, and `contract.prisma`, `contract.json`, and `contract.d.ts`. Deploy with the three commands in [Check before, preview, then apply](#check-before-preview-then-apply). The point of the `migration check` step is to tell you that what you are about to apply is still what was reviewed: it recomputes each migration's hash from its `ops.json` and [`migration.json`](/orm/migrations/how-migrations-work#migrationjson-the-history-marker) and compares it with the stored hash, which catches a hand edit to `ops.json`, though not a `migration.ts` that was edited without recompiling. When it fails with an error whose `code` is `MIGRATION.CHECK_HASH_MISMATCH`, either restore the migration's files from Git, or move the hand edit into `migration.ts` and recompile. + +If two deployments can run at once, what happens next depends on your database. On PostgreSQL, when two `db migrate` runs start against the same database, one of them waits for the other. On MongoDB, if another run updates the marker while yours is running, yours fails with `Marker was modified by another process during migration execution.` When that happens, run `npx prisma migration status` to see which contract state the database matches, and then run `db migrate` again. ## 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 a [Prisma ORM extension package](/orm/extensions/using-extensions), such as pgvector support, the output shows more than one **contract space**, which is a separate migration history: your app has one, and so does each extension package that ships migrations. `migration plan` writes an extension's migrations to `migrations//`, such as `migrations/pgvector/`, and you have to commit that directory, because `db migrate` fails without it. A single run applies every space and reports each one separately, while `--to` applies only to your app. What an extension space ends up matching is decided by what you committed rather than by what is in `node_modules`, because `db migrate` applies the migrations in that committed directory until the database matches the newest state there, not the package version you have installed: ```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" - └─ ... +├─ Create table "user" +└─ ... ``` :::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`. +Applying, targeting, previewing, refs, the ledger, and applying several contract spaces in one run all work today. Not built yet: a rehearsal that applies migrations to a throwaway copy of the database first, and a check at apply time that `ops.json` still matches `migration.ts`. Until then, preview with `--show` and rehearse on a staging database. ::: ## 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." +- "List what `db migrate --to prod` would run, then run `migration show` on each migration and summarize the destructive operations." - "Apply the pending migrations and advance the `db` ref." ## See also -- [Rollbacks and recovery](/orm/migrations/rollbacks-and-recovery): when you need to go back instead of forward -- [The migration graph](/orm/migrations/the-migration-graph): markers, refs, and pathfinding +- [Rollbacks and recovery](/orm/migrations/rollbacks-and-recovery): when the database needs an earlier contract state +- [The migration graph](/orm/migrations/the-migration-graph): markers, refs, and how migrations connect - [Generating a migration](/orm/migrations/generating-a-migration): producing what `db migrate` runs -- [Studio with Prisma ORM](/studio/prisma-next): read the ledger you just wrote as a visual timeline, with the executed SQL and a schema diff per migration +- [Studio with Prisma ORM](/studio/prisma-next): read the ledger as a visual timeline, with the executed SQL and a schema diff per migration 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..3c05534c847 100644 --- a/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx +++ b/apps/docs/content/docs/orm/migrations/editing-a-migration.mdx @@ -1,44 +1,46 @@ --- 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.' --- -The planner writes a good first draft, but many migrations require the input of a human (or agent) for key decisions: what to put in existing rows when a column becomes required, which order to run two steps in, one statement the planner has no factory for. In Prisma ORM you make those changes by editing `migration.ts` (ordinary TypeScript with autocomplete and type checking) and recompiling it. You never hand-edit SQL files or `ops.json`. +Each of your app's migrations is a directory in `migrations/app/`, and the file you edit inside it is `migration.ts`, a TypeScript file that describes the change as a list of calls such as `this.addColumn(...)`. `npx prisma migration plan` writes the first draft of that file for you, but it can only work out the parts that follow from your contract. The decisions that depend on your data are left to you: what value existing rows should get, which order the steps should run in, and how to write a statement that has no method of its own. -The rule that makes this safe: - -> **You edit `migration.ts`. Running it regenerates `ops.json`. The runner only ever executes `ops.json`.** - -After any edit, recompile from inside your project: +The file that actually runs against your database is not the one you edit. `npx prisma db migrate`, which replaces [`prisma migrate deploy`](/orm/coming-from-prisma-orm-7), runs the SQL in each migration's `ops.json`, and fixing that SQL by hand does not work, because Prisma ORM notices: `npx prisma migration check` fails and `npx prisma db migrate` refuses to run. Edit `migration.ts` instead and then recompile it, which means running the file itself with Node.js from your project root. Recompiling rewrites both `ops.json` and `migration.json` for you, and never connects to a database: ```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. +You can run the file like this because the Prisma ORM CLI already needs Node.js 22.18 or later, and those versions run `migration.ts` directly, with no build step. Run it from your project root: if you change into the migration directory first and run it from there, the command fails with an error whose `code` is `CONFIG.FILE_NOT_FOUND`. + +The other file recompiling rewrites, `migration.json`, is what makes a hand-edit detectable. It records the contract state the migration starts `from`, the state it ends at, `to`, and a hash of the migration. `npx prisma migration check` recomputes that hash from `ops.json` and `migration.json` and compares it with the stored one, so any change to either file that did not come from a recompile no longer matches. The check never reads `migration.ts`, which is why an edit you make there counts for nothing until you recompile. + +Editing a migration only changes what happens on databases that have not applied it yet, because a database that already applied it will not apply it again. When you need to change a database that has already applied the migration, plan a new migration for the change instead. When you only want to try your edit against a development database that already applied it, drop and recreate that database with your own tools, because Prisma ORM 8 has no replacement for `prisma migrate reset`. Then run `npx prisma db migrate --advance-ref db`, which points the [`db` ref](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) at the state you just applied. The `db` ref is the name Prisma ORM keeps for the contract state you last applied in development, and it lives in a file under `migrations/app/refs/`. ## 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: +Say you add a required `displayName` field to `User` in your contract, the `contract.prisma` file that replaced `schema.prisma`. The rows already in your `user` table have no `displayName`, so you have to decide what those rows get. Start by running `npx prisma contract emit`, which replaces `prisma generate` and writes `contract.json` and `contract.d.ts`, and then plan the migration as [Generating a migration](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) describes: ```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. +from: 925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72 +to: e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63 +app space: migrations/app/20260707T1008_add_display_name ``` -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: +In that output, `self-emit` is what the CLI calls recompiling, and `app space` is the directory the new migration was written to. `migration plan` writes the change as three steps rather than one, because the column is required and the table already has rows: `addColumn` adds the column as nullable, `dataTransform` holds the backfill you write, and `setNotNull` makes the column required. The table is called `user` because the table name is the model name with a lowercase first letter, unless the model sets `@@map`: ```ts title="migration.ts (as planned)" override get operations() { @@ -57,24 +59,21 @@ 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` is where you say what should happen to the rows already in the table, and it takes two callbacks. Each one returns a query built with the [`sql` query builder](/orm/reference/sql-query-builder), not a SQL string. `check` returns the rows that still need the backfill, which is how Prisma ORM tells whether there is any work left to do, and `run` is the query that changes them. Inside each `where`, `f` holds the table's columns and `fns` holds [functions](/orm/reference/sql-query-builder#expressions-and-functions) such as `eq`, which is why `fns.eq(f.displayName, null)` becomes `IS NULL`. When you want to set a column from another column, pass [`update()`](/orm/reference/sql-query-builder#update) a callback instead of a plain object: `update((f) => ({ displayName: f.email }))`. The imports at the top of the file below come from snapshots, which are copies of your contract that `migration plan` saves in `migrations/snapshots//`, and this migration imports the snapshot from before it and the one from after it, so the queries you write are typed against the contract you mean. ```ts title="migration.ts (filled in)" +#!/usr/bin/env -S node import type { Contract as End } from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract'; -import endContractJson from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract.json' with { - type: 'json', -}; +import endContractJson from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract.json' with { type: 'json' }; import type { Contract as Start } from '../../snapshots/925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72/contract'; -import startContractJson from '../../snapshots/925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72/contract.json' with { - type: 'json', -}; +import startContractJson from '../../snapshots/925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72/contract.json' with { type: 'json' }; import { Migration, MigrationCLI, col } from '@prisma/orm-postgres/migration'; import postgresAdapter from '@prisma/orm-postgres/adapter/runtime'; import { sql } from '@prisma/orm-postgres/builder/runtime'; import { createExecutionContext, createSqlExecutionStack } from '@prisma/orm-postgres/family-runtime'; import postgresTarget, { PostgresContractSerializer } from '@prisma/orm-postgres/target/runtime'; -const endContract = new PostgresContractSerializer().deserializeContract(endContractJson); +const endContract = new PostgresContractSerializer().deserializeContract(endContractJson); const stack = createSqlExecutionStack({ target: postgresTarget, adapter: postgresAdapter }); const db = sql({ @@ -112,22 +111,16 @@ 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. - -These two details should be noted: - -- **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. +If you compare that with the file `migration plan` wrote, the differences are all things you add or rename. The planned file's migration import also brings in `placeholder`, and it does not have the four other `@prisma/orm-postgres` imports or the `endContract`, `stack`, and `db` statements, which only build queries and never connect to a database, so you write those in yourself. The planned file also names its snapshot `contract.json` imports `endContract` and `startContract`, renamed here to `endContractJson` and `startContractJson` so that the name `endContract` is free for the deserialized contract. The only other thing you touch is the `placeholder(...)` calls, which become your `check` and `run` queries. The rest of each operation, such as `codecRef`, stays exactly as `migration plan` wrote it. -Recompile with `node migration.ts` and inspect what the backfill became: +Now recompile the file with `node migrations/app/20260707T1008_add_display_name/migration.ts`. If you left a `placeholder(...)` call in it, recompiling fails with an error whose `code` is `MIGRATION.UNFILLED_PLACEHOLDER`, and `npx prisma db migrate` fails as well, because `ops.json` is still empty. Once it recompiles, read the `UPDATE` it produced in `ops.json` and check that it sets the value you meant on the rows you meant: ```json title="ops.json (the compiled dataTransform)" { "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 +145,103 @@ 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. +There is one case where a correct-looking backfill still leaves you stuck, and it involves timestamps that Prisma ORM fills in for you. A column declared as `temporal.updatedAtString()` gets the current time whenever a row is created or updated, the way `@updatedAt` did in Prisma ORM 7. The `User` in this example has no such column, but the starter contract from `npx prisma orm init` does have one, called `updatedAt`. On a model like that, your backfill's `update` also sets `updatedAt`, and that is enough to make `npx prisma migration check` fail right after every recompile with an error whose `code` is `MIGRATION.CHECK_HASH_MISMATCH`, so `npx prisma db migrate` refuses to run. -## Escape hatch: raw SQL +The way out is to write the backfill against a contract of your own rather than the real one. Build `db` from an intermediate contract and pass that same contract to `dataTransform`, which the next section works through in full. In that contract, make `displayName` optional and declare `updatedAt` as `TimestamptzString`, a type Prisma ORM does not set for you, so the backfill leaves that column alone. -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: +## When a backfill reads a column the migration removes + +Sooner or later a backfill has to read a column that the same migration takes away. Suppose the next migration replaces the boolean `isAdmin` on `User` with a required text column, `role`. Change your contract, run `npx prisma contract emit`, and plan the migration as in the `displayName` example. The backfill reads `isAdmin` to decide each row's `role`, and that is where the trouble starts, because the end contract has no `isAdmin` and the start contract has no `role`. If you build `db` from the end contract, TypeScript rejects `f.isAdmin`, and recompiling fails with an error that does not name the column, so the cause is hard to spot. + +What you need instead is a contract that describes the database as it is while the backfill runs, and you write that one yourself. Copy your whole contract into the migration directory as `intermediate.prisma`, then in its `User` model add `isAdmin` back and make `role` optional, because at that point no row has a `role` yet. + +```prisma title="intermediate.prisma (User, other fields left out)" +model User { + id Int @id + displayName String + isAdmin Boolean + role String? +} +``` + +In the same migration directory, add `intermediate.config.ts`, a second config file that names `intermediate.prisma` as the contract and needs no database connection: + +```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' }) }); +``` + +Emit the intermediate contract from inside the migration directory, because `intermediate.config.ts` reads its paths from the directory you run the command in. Commit all four `intermediate.*` files along with the migration, since the migration no longer recompiles without them. + +```bash +cd migrations/app/20260708T0900_replace_is_admin +npx prisma contract emit --config intermediate.config.ts +cd ../../.. +``` + +```text title="migrations/app/20260708T0900_replace_is_admin/" +├── intermediate.config.ts +├── intermediate.d.ts +├── intermediate.json +├── intermediate.prisma +├── migration.json +├── migration.ts +└── ops.json +``` + +Now fill in this migration's own planned `migration.ts`, rather than copying the `displayName` file, whose snapshot imports point at other contracts. The listing below builds `db` from the intermediate contract and passes that same contract to `dataTransform`, and those two have to be the same contract: if you pass a different one, `node migration.ts` fails with an error whose `code` is `MIGRATION.DATA_TRANSFORM_CONTRACT_MISMATCH`. The listing also moves the planned `dropColumn` for `isAdmin` to after the backfill, so `isAdmin` is still there when the backfill reads it, and its two `run` callbacks run in the order you list them, so the second one sets `member` only on the rows the first one left empty. + +```ts title="migration.ts (filled in)" +#!/usr/bin/env -S node +import type { Contract as End } from '../../snapshots//contract'; +import endContractJson from '../../snapshots//contract.json' with { type: 'json' }; +import type { Contract as Start } from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract'; +import startContractJson from '../../snapshots/e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63/contract.json' with { type: 'json' }; +import type { Contract as Mid } from './intermediate'; +import midContractJson from './intermediate.json' with { type: 'json' }; +import { Migration, MigrationCLI, col } from '@prisma/orm-postgres/migration'; +import postgresAdapter from '@prisma/orm-postgres/adapter/runtime'; +import { sql } from '@prisma/orm-postgres/builder/runtime'; +import { createExecutionContext, createSqlExecutionStack } from '@prisma/orm-postgres/family-runtime'; +import postgresTarget, { PostgresContractSerializer } from '@prisma/orm-postgres/target/runtime'; + +const stack = createSqlExecutionStack({ target: postgresTarget, adapter: postgresAdapter }); +const midContract = new PostgresContractSerializer().deserializeContract(midContractJson); + +const db = sql({ + context: createExecutionContext({ contract: midContract, stack }), + rawCodecInferer: stack.adapter.rawCodecInferer, +}); + +export default class M extends Migration { + override readonly endContractJson = endContractJson; + override readonly startContractJson = startContractJson; + + 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' }), + ]; + } +} + +MigrationCLI.run(import.meta.url, M); +``` + +If you would rather not keep an intermediate contract around, you can reach the same result by splitting the change into two migrations. First add `role` to your contract as optional and plan, and in that migration add a `dataTransform` that sets `role` from `isAdmin`, with `db` built from its end contract, which still describes both columns. Then remove `isAdmin`, make `role` required, and plan again. That second plan writes a `handle-nulls-user-role` placeholder, which you fill in with what should happen to rows where `role` is still empty. Splitting the change needs no `intermediate.*` files at all, unless `User` has a `temporal.updatedAtString()` column. + +## Raw SQL [#escape-hatch-raw-sql] + +When the statement you need has no method of its own, such as `COMMENT ON`, you write the SQL yourself. Import `rawSql` from `@prisma/orm-postgres/migration` and add the call to the `operations` array alongside the other operations. You also say what kind of change the statement is, by setting `operationClass` to one of the [four classes](/orm/migrations/how-migrations-work#every-operation-checks-itself): `additive`, `widening`, `destructive`, or `data`. `npx prisma db migrate` runs all four of them, and adds a data-loss warning for `destructive`. Give each operation its own `id`, because error messages name it and Prisma ORM does not check that it is unique, so two operations sharing an `id` leave you unable to tell which one an error is about. The `label` is the text the CLI prints. One limit matters before you write any SQL: one `npx prisma db migrate` run on PostgreSQL is [one transaction](/orm/migrations/applying-a-migration#when-something-goes-wrong), so `rawSql` cannot run `CREATE INDEX CONCURRENTLY`, or anything else that has to run outside a transaction. For an index, use `this.createIndex`, which is a plain `CREATE INDEX` and blocks writes while it builds. The example below is only an illustration, because it enables the `pgcrypto` PostgreSQL extension, which you would really do with `this.installExtension`: ```ts rawSql({ @@ -174,11 +259,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. +Give each operation a precheck and a postcheck if you can, because they are what lets Prisma ORM decide whether your SQL needs to run at all, and whether it did what you meant. Each check is a query whose first row must have `true` in its first column. `npx prisma db migrate` runs the postcheck twice for each operation. It runs it first before the operation, where a passing postcheck means the change is already there, so the operation is skipped. When the postcheck does not pass there, the precheck runs, and if it is not true the run stops. The postcheck then runs again after the operation, and if it fails that time the run stops. That leaves two cases worth remembering: an empty `precheck` lets the operation run, and an empty `postcheck` never passes before the operation, which means that operation runs on every `npx prisma db migrate` that applies its migration. ## 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: +The same backfill pattern works on MongoDB, with different imports and a differently shaped `check`. Import `dataTransform` from `@prisma/orm-mongo/target/migration`, and write its `check` as an object whose `source` is a callback that returns the query. The example below needs no `db` statements, because it builds its queries directly: `AggregateCommand` for the `check` query and `RawUpdateManyCommand` for the `run` update, both from `@prisma/orm-mongo/query-ast/execution`. A call such as `new RawUpdateManyCommand('products', { status: { $exists: false } }, { $set: { status: 'active' } })` takes the collection name, a filter, and an update. Here those two queries come back from the helpers `existingProductsWithoutStatus` and `backfillRun`, and each of them takes the end contract's `storageHash`, because every MongoDB query records the hash of the contract it was built for. The `setValidation` step is in the listing only because this migration also adds fields to `products`, and the collection's validator has to include them. Both helpers are in the full [retail-store example](https://github.com/prisma/orm/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts): ```ts title="migration.ts (MongoDB, excerpt)" import { dataTransform, setValidation } from '@prisma/orm-mongo/target/migration'; @@ -199,35 +284,34 @@ 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). - ## 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): +Not every migration comes from a contract change. When you want a data-only migration, which changes rows and leaves your contract state as it is, or a migration you intend to write entirely by hand, run `npx prisma migration new`. It writes an empty `ops.json`, a `migration.json`, and a `migration.ts` that already has the snapshot imports, the class, and an empty `operations` array. Nothing was planned for you, so there is no `placeholder` import to remove, but you do have to add the `@prisma/orm-postgres` imports and the `endContract`, `stack`, and `db` statements from the filled-in `displayName` listing. + +Because a data-only migration starts and ends at the contract state your database already matches, you have to tell the command which state that is, by passing `--from` with your contract's hash. `npx prisma migration graph` marks that hash `@contract`. The [`migration new` reference](/cli/migration-new) lists the rest of the options: ```npm -npx prisma@latest migration new --name backfill_scores +npx prisma migration new --name backfill_scores --from ``` -Write your operations in the generated `migration.ts`, then compile it the same way: `node migration.ts`. +Write your operations in the new `migration.ts` and recompile it. Until you do, the migration is still the empty one the command wrote, so `npx prisma migration check` and `npx prisma migration plan` both fail. ## 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`. -5. Commit `migration.ts`, `ops.json`, and `migration.json` together. +1. Edit `migration.ts`, never `ops.json`, then recompile it. +2. Review the diff of `ops.json`, the file that runs. +3. Run `npx prisma migration check`, which prints `✔ All checks passed` when nothing is wrong. +4. Commit `migration.ts`, `ops.json`, `migration.json`, the `migrations/snapshots//` directories that `migration.ts` imports, and your `contract.json` and `contract.d.ts`. If the migration has `intermediate.*` files, commit those too. :::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 query builder for updates. No command notices a `migration.ts` edit that you did not recompile, 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), instruction files 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." @@ -235,6 +319,6 @@ Projects scaffolded with `create-prisma@latest` install [Prisma ORM skills](/ai/ ## See also -- [Generating a migration](/orm/migrations/generating-a-migration): where the scaffold comes from +- [Generating a migration](/orm/migrations/generating-a-migration): where the first draft of `migration.ts` comes from - [Applying a migration](/orm/migrations/applying-a-migration): run the edited migration -- [Data Migrations in Prisma 8](https://www.prisma.io/blog/data-migrations-in-prisma-next): the design story for `dataTransform` +- [Data Migrations in Prisma 8](https://www.prisma.io/blog/data-migrations-in-prisma-next): how `dataTransform` was designed diff --git a/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx b/apps/docs/content/docs/orm/migrations/generating-a-migration.mdx index aa842c4a52b..e6f89c977c3 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,14 @@ --- title: Generating a migration -description: Turn a contract change into a reviewable migration with the migration plan command. +description: Turn a change to your contract 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 tutorial for the migration plan command, from a contract change to a 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. +This tutorial takes a change you have made to your contract, the `contract.prisma` file that replaced `schema.prisma`, and turns it into a migration that you read and approve before it runs against a database. To create a project first, see the [quickstart](/prisma-orm/quickstart/postgresql). -Start from a minimal project; you can scaffold one with `npm create prisma@latest`. +Two commands do the work, and you run them in this order every time you change your contract. `npx prisma contract emit` replaces `prisma generate`: it reads `contract.prisma` and writes `contract.json` and `contract.d.ts` next to it, so that everything downstream reads your latest edit and not the version before it. `npx prisma migration plan` then reads `contract.json` and writes a new migration for the change, without applying it. Planning never connects to a database, which means you can run it in CI or in a sandbox with no database credentials. To work out what your change is a change from, `migration plan` looks by default at the `db` [ref](/orm/migrations/the-migration-graph#terms-used-on-this-page), a file in `migrations/app/refs/` that records the contract version you last applied in development. ## Your first migration @@ -24,32 +24,30 @@ model User { } ``` -First, emit the contract, which compiles the schema into the `contract.json` artifact that every other command reads: +Run both commands, passing `--name init` so that the new migration directory is named `_init`: ```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" ( @@ -60,35 +58,27 @@ CREATE TABLE "public"."user" ( ); ``` -Four things to note: +Read the rest of that output closely, because it tells you what was planned and where it was written: -- **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. +- **`Planned 2 operation(s)`** counts the steps in the migration, which are listed underneath the directory name, and the DDL preview below them is the SQL those steps run. +- **`to:`** is the hash of your contract. Each version of your contract is a contract state, and Prisma ORM identifies a state by that hash rather than by a version number. +- **`from: (baseline)`** tells you this is a baseline migration, meaning a first migration that goes from an empty database to a contract state. What Prisma ORM 7 called baselining, marking an existing database as already migrated, is a separate command in Prisma ORM 8, `npx prisma db sign`. +- **`app space:`** is the directory the new migration was written to. Your app's migrations are in `migrations/app/`, and each [Prisma ORM extension package](/orm/extensions/using-extensions) that ships migrations gets a directory of its own beside it. Prisma ORM looks for `migrations/` in the directory you run commands from, which is normally the project root. -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 `No db ref set` line is telling you that nothing in this project records which contract state your database matches, so `migration plan` assumed an empty database and planned from there. That assumption is right for a new project. If you already have a database, tell Prisma ORM what is in it before you plan anything, and which command does that depends on what the database holds: -```text -migrations/ -├── app/ -│ └── 20260707T1005_init/ -│ ├── migration.ts -│ ├── ops.json -│ └── migration.json -└── snapshots/ - └── 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/ - ├── contract.json - └── contract.d.ts -``` +- If Prisma ORM 7 migrated the database, follow the steps in the [upgrade guide](/guides/upgrade-prisma-orm/postgresql#4-transfer-migration-ownership) first. +- If the database is empty, you need nothing: `npx prisma db migrate` creates the tables for you, though it does not create the database itself. +- If every table in the database already matches your contract, run [`npx prisma db sign`](/cli/db-sign) and then read [The automatic baseline](#the-automatic-baseline) below. `db sign` checks the tables against your contract and then writes the [marker](/orm/migrations/the-migration-graph#terms-used-on-this-page), which is the record in the database of which contract state it matches. +- If the database has only some of your tables, run [`npx prisma db init`](/cli/db-init). It adds what is missing and nothing else, and it stops if matching your contract would need a change that could lose data, such as dropping a column. +- If this is a development or preview database that you want to match your contract without keeping migration files for it, run [`npx prisma db update`](/cli/db-update). Unlike `db init`, it will make changes that could lose data, so it asks you to type the database name first. -Open `migration.ts`. It is a description of the change, so it reads like one: +A migration is a directory, and there is no SQL file in it. It holds three files instead: `migration.ts`, which is the one you read and sometimes edit, plus `ops.json` and `migration.json`, which are what `db migrate` reads when it applies the migration. Commit all three, along with `contract.prisma`, `contract.json`, `contract.d.ts`, and any new directories under `migrations/snapshots/`, which are saved copies of your contract. The change itself is in `migration.ts`, written as a list of method calls: ```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', -}; +import endContract from '../../snapshots/705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/contract.json' with { type: 'json' }; import { Migration, MigrationCLI, col, primaryKey } from '@prisma/orm-postgres/migration'; export default class M extends Migration { @@ -114,121 +104,127 @@ 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). +Everything the migration will do is in that `operations` list, so reading it tells you exactly what will happen, and editing it is how you change what happens. A change as simple as this one needs no edit, and `ops.json` is never a file you edit by hand. [Editing a migration](/orm/migrations/editing-a-migration) covers the cases where you do edit `migration.ts` and recompile it, such as adding a data backfill, reordering operations, or running raw SQL. ## 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: +Apply that first migration to your development database by running `npx prisma db migrate --advance-ref db`. This is the first command here that connects to a database, and it uses [`db.connection`](/orm/contract-authoring/the-data-contract) in `prisma.config.ts` unless you pass a connection string with `--db`. The `--advance-ref db` part is what records the result: it points the `db` ref at the contract state you just applied, so the next time you run `migration plan` it starts from there and plans only what has changed since. -```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: +Now add an optional `phone String?` field to `User`, and run both commands again: ```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 ``` ```text +│ contract: src/prisma/contract.json +│ migrations: migrations/app +│ 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; ``` -`--from` accepts a migration directory name (as here), a contract hash or unambiguous prefix, a ref name, or `^` meaning "the state *before* that migration". +To plan from a contract state other than the one the `db` ref names, say which one with `--from`, as in `--from 20260707T1005_init`. When you give `--from` a migration directory name, you are naming the contract state that exists once that migration has run. The [`migration plan` reference](/cli/migration-plan#options) lists every form the flag accepts. ### 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: +Once your first migration is applied, every change you make after that follows the same routine, which does the job `migrate dev` did in Prisma ORM 7 whenever you wanted migration files out of it, as the [command table](/orm/coming-from-prisma-orm-7#commands) shows: -- `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. -- `migration ref set db ` sets it by hand. +1. Edit your contract. +2. Run `npx prisma contract emit`. If you skip this, `migration plan` reads the `contract.json` from before your edit, so it either prints `No changes detected` or plans a migration that is missing your latest change. +3. Run `npx prisma migration plan --name `. +4. Review what it planned with `npx prisma migration show `, where `` is the new migration's directory name. If you edit `migration.ts`, you then have to [recompile it](/orm/migrations/editing-a-migration) by running `node migrations/app//migration.ts` from your project root, which rewrites `ops.json` and `migration.json` to match what you edited. +5. Apply it. In development that is `npx prisma db migrate --advance-ref db`, and in CI and production it is `npx prisma db migrate` without the flag. -```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 -``` +The step that is easy to get out of order is the last one, and the symptom is a migration that repeats work you already planned. If you plan a second migration before you have applied the first one with `--advance-ref db`, the `db` ref still names the older contract state, so Prisma ORM plans the first migration's changes a second time. To fix that, delete the directory of the repeated migration, run [`npx prisma migration ref set db `](/cli/migration-ref) with the name of the directory whose changes it repeated, and plan again. Deleting the directory is how you discard any migration that has not run on a database, and the matching directory under `migrations/snapshots/` can stay. -:::warning[Without `--from` or a `db` ref, planning stops] +`db migrate --advance-ref db` is not the only command that updates the `db` ref. When `npx prisma db sign`, `db init`, and `db update` change the database configured in `prisma.config.ts`, they point the `db` ref at the new state as well. `db sign` is the one to watch, because it updates the ref even when you pass `--db` to sign a different database, so add `--no-advance-ref` when you do. -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. +When you leave `--from` off, what `migration plan` does depends on whether `migrations/app/` already has migrations in it and whether the `db` ref exists. The combinations are: -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. +| Migrations in `migrations/app/` | `db` ref | What `migration plan` does | +| --- | --- | --- | +| None | None | Plans from an empty database | +| None | Exists | Writes a baseline migration, then your change | +| Some | Exists | Plans from the state the `db` ref points at | +| Some | None | Stops with an error | -::: +That last row is an error because Prisma ORM has migrations but nothing recording which contract state your database matches, and it will not guess. The error prints the commands that resolve it: one sets the `db` ref, one plans with `--from`, and one plans with `--from @empty`. ### 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`. +If you started from an existing database, `db sign` and `db update` leave you in the second row of that table: you have no migration files yet, but the `db` ref names a contract state. What that means in practice is that you should run `migration plan` before you ever run `db migrate`. Planning writes a baseline migration ending at the state your database already matches, plus a second migration for your own change if `contract.json` has changed since. You need that baseline because `db migrate` fails against the database until one migration's `to:` hash matches the marker, and it never runs the baseline there, since the marker already records that the database matches that state. [Baselines](/orm/migrations/the-migration-graph#baselines) has the full rule. -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. +There is one situation where that automatic baseline is not the one you want. If a deployed database, such as production, matches an older version of your contract than your development database does, a baseline planned the usual way would end at your development database's contract state, which is not the state production matches. Plan the baseline from the older version instead: -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. +1. Change your contract back to the deployed version, for example with `git show : > `. +2. Run `npx prisma contract emit`. +3. Run `npx prisma migration plan --name baseline --from @empty`. +4. Run `npx prisma db sign --db "$PRODUCTION_DATABASE_URL" --no-advance-ref` against the deployed database. It writes the marker there, and `--no-advance-ref` leaves your `db` ref alone. +5. Restore your current contract and run `npx prisma contract emit` again. +6. Plan the change with `npx prisma migration plan --name --from `, the baseline's directory name. + +None of these steps changes your `db` ref. ## 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 cannot be planned in full, because the answer depends on your data rather than on your contract. Adding a **required** field with no default to a table that already has rows is the common one: `migration plan` writes the `ADD COLUMN` and the `SET NOT NULL`, but only you know what the rows that already exist should contain, so it leaves that part for you to fill in as a **placeholder**. To see one, apply `add_user_phone`, then add a required `displayName String` field to `User` and run both commands again: + +```npm +npx prisma contract emit +npx prisma migration plan --name add_display_name +``` ```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: e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63 +app space: migrations/app/20260707T1008_add_display_name ``` -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). +Open the new `migration.ts` and you will find a `dataTransform` step between the `ADD COLUMN` and the `SET NOT NULL`, which is the step that updates the rows already in the table. The two `placeholder(...)` calls inside it mark where your queries go: the first finds the rows that still need a `displayName`, and the second fills the value in. Write both with the [SQL query builder](/orm/reference/sql-query-builder), as [Editing a migration](/orm/migrations/editing-a-migration) shows, and then recompile the file, which is the step the output above calls self-emit. Nothing can be applied until you do, because `migration plan` leaves `ops.json` empty when it writes placeholders, so `db migrate` fails and changes nothing. ## Reviewing what you planned -Three offline commands close the loop before anything runs: +You can inspect a planned migration before anything runs, with these commands, none of which connects to a database: ```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, no files missing, every migration starts from empty or where another ends, refs point at known states +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`. +`migration check` is the one to run in CI, where you read its exit code rather than its output. `0` means every check passed, `4` means an integrity failure such as a missing file or an `ops.json` that someone edited by hand, and `2` means it could not find a migration you named. There is one thing it cannot tell you, because it never reads `migration.ts`: whether you remembered to recompile after your last edit. You can check that yourself by staging the migration with `git add` and then recompiling it, because if `git status` afterwards shows no unstaged change to `ops.json` or `migration.json`, they were already up to date. This does not work for a backfill on a model with an [`updatedAt` column](/orm/migrations/editing-a-migration#worked-example-making-a-column-required). :::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. What it cannot do yet is notice that you renamed something: if you rename an optional field, it plans the change as **drop column + add column**, and `migration plan` prints a warning that the change can lose data. Take that warning seriously, because `db migrate` runs destructive operations without asking, so read `migration show ` before you apply anything. To rename a column and keep the data in it, edit `migration.ts`, replace that pair of operations with a [`rawSql` operation](/orm/migrations/editing-a-migration#escape-hatch-raw-sql) that runs `ALTER TABLE ... RENAME COLUMN`, and recompile. One more difference from Prisma ORM 7: `migration plan` never asks you questions the way `migrate dev` did. ::: ## 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`. The skills are instruction files your agent reads, so you can ask it 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 +235,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..ecf3548fc38 100644 --- a/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx +++ b/apps/docs/content/docs/orm/migrations/how-migrations-work.mdx @@ -1,63 +1,76 @@ --- title: How migrations work -description: 'Change your contract, plan a migration, review it, apply it. Every step is checked before and after it runs.' +description: 'Change your contract, plan a migration, review it, apply it. Operations can check the database before and after they run.' url: /orm/migrations/how-migrations-work 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.' +metaDescription: 'What a Prisma ORM migration is, the plan-review-apply loop, and how operations check the database.' --- -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 `npx prisma orm init` puts it in `src/prisma/`. After you edit it, run `npx prisma contract emit`, which replaces `prisma generate`. Next to `contract.prisma` it writes the files the rest of Prisma ORM reads: `contract.json`, which the migration commands read, and `contract.d.ts`, which your client's types come from. -The workflow is comprised of a loop which you will run many times a day during development: +Prisma ORM 8 supports PostgreSQL and MongoDB. SQLite is experimental, and MySQL is not supported, as [Supported databases](/orm/supported-databases) lists. + +You run this loop many times a day: -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. -4. **Apply it**: `db migrate` runs the pending migrations against your database. +1. **Change your contract**: edit `contract.prisma`, then run `npx prisma contract emit`. +2. **Plan a migration**: `migration plan` works out what has to change in the database by comparing your new contract with an earlier contract state, and writes what it finds as a migration directory under `migrations/app/`. A contract state is one version of your contract, named by its hash, so it is how Prisma ORM refers to your contract as it stood at a particular moment. Unless you tell it otherwise, the earlier state it compares against is the `db` ref, the file `migrations/app/refs/db.json` naming the contract state you last applied in development. +3. **Review it**: before anything touches the database, run `npx prisma migration show ` to see the operations and SQL that `db migrate` will run. If you want the migration to change rows as well, edit `migration.ts` as [Editing a migration](/orm/migrations/editing-a-migration) shows, and then recompile it by running `node migrations/app//migration.ts` from your project root, which rewrites `ops.json` and `migration.json` from your edits without needing a database connection. You need nothing extra for that, because the Prisma ORM CLI already requires Node.js 22.18 or later, which runs `migration.ts` directly. +4. **Apply it**: `db migrate` starts by reading the database's marker, the record in the database of which contract state it matches, so it knows how much of your history the database has already seen, and then runs the migrations from that state to your current contract. In development, add `--advance-ref db`, which points the `db` ref at the state you just applied, so the next `migration plan` starts from the state your database actually matches. -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: +With a database connection set up, as in the [quickstart](/prisma-orm/quickstart/postgresql), add an optional `phone String?` field to `User`, then run: ```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 migration show 20260707T1006_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. - -`migration plan` prepares the SQL for the migration, but doesn't run it: +`migration plan` prepares the SQL but doesn't run it: ```text ✔ Planned 1 operation(s) -│ +migrations/app/20260707T1006_add_user_phone └─ Add column "phone" to "user" -DDL preview +from: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 +to: 925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72 +app space: migrations/app/20260707T1006_add_user_phone + +ℹ DDL preview ALTER TABLE "public"."user" ADD COLUMN "phone" text; ``` -and `db migrate` applies the migration to your database and confirms what it applied: +A table name is the model name with a lowercase first letter unless the model sets `@@map`, so `User` becomes `"user"` and `UserProfile` becomes `"userProfile"`. If your database was created by Prisma ORM 7, it has a `"User"` table instead, so [the upgrade guide](/guides/upgrade-prisma-orm/postgresql#24-edit-the-inferred-contract) maps the old names with `@@map` before you plan a migration for it. + +The `app space:` line tells you which migration history the new directory was written into. A contract space is a separate migration history, so your own migrations stay in `migrations/app/` while each [Prisma ORM extension package](/orm/migrations/applying-a-migration#extension-spaces) that ships migrations, such as pgvector, keeps its migrations in its own directory under `migrations/`. + +`db migrate` applies the migration 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. +The first time you run `migration plan`, there are no migrations and no `db` ref on disk yet, so there is no earlier state to compare against and Prisma ORM plans as if the database were empty. If you want it to start somewhere else, pass `--from`, such as `--from 20260707T1006_add_user_phone` for the state after that migration. The [`migration plan` reference](/cli/migration-plan#options) lists the other forms `--from` accepts. + +If you apply a migration without `--advance-ref db`, or plan a second migration before you apply the first, the `db` ref still names the older contract state, so the next `migration plan` compares against that older state and plans changes you have already planned once. [The db ref](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) shows how to delete the duplicate migration. + +`db migrate` needs a database to talk to, and it takes that from `db.connection` in [`prisma.config.ts`](/orm/contract-authoring/the-data-contract), such as `db: { connection: process.env["DATABASE_URL"]! }`, unless you pass a URL with the `--db` flag. In CI and production, run plain `npx prisma db migrate`. ## What a migration contains -A migration is a directory under `migrations/app/`, named with a timestamp and a slug: +A migration directory is named with a timestamp and the `--name` you passed. `migrations/` is in the directory you run commands from, normally your project root: ```text migrations/ ├── app/ │ └── 20260707T1006_add_user_phone/ -│ ├── migration.ts # the file you edit -│ ├── ops.json # the file Prisma runs +│ ├── migration.ts # the change, as TypeScript +│ ├── ops.json # the operations, as JSON │ └── migration.json # where this migration fits in history └── snapshots/ └── / @@ -65,178 +78,114 @@ 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. - -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. | -| `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. | +After `migration plan`, commit the new migration directory, any new directories under `migrations/snapshots/`, `contract.prisma`, `contract.json`, and `contract.d.ts`. ### migration.ts: the file you edit -`migration.ts` is the authoring file and contains the migration steps as ordinary TypeScript: - -```ts -this.addColumn(...) -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. - -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). +`migration plan` writes the change as `migration.ts`, a TypeScript class with one call per step, such as `this.addColumn(...)` for a new column, so you can read and change it like any other TypeScript file. [Editing a migration](/orm/migrations/editing-a-migration) explains how to change it. ### 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. - -That means production never executes your TypeScript: it reads the compiled operations, so no application code runs with production credentials. - -```text -migration.ts -> ops.json -you edit Prisma runs -``` - -The two files are committed side by side and work like `package.json` and `package-lock.json`: - -```text -package.json -> package-lock.json -what you ask for exact resolved result - -migration.ts -> ops.json -what you author exact operations to run -``` +`migration plan` also writes the same operations as JSON in `ops.json`, and `ops.json` is what `db migrate` actually runs, never `migration.ts`. That is why recompiling matters: if you edit `migration.ts` and don't recompile it, `db migrate` runs the old `ops.json` rather than your edit, and neither it nor `migration check` warns you. -### 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: -- the schema hash it starts `from` -- the schema hash it moves `to` +- the contract hash it starts `from` +- the contract hash it ends at, `to` - when it was created -- its own hash, so tampering is detectable - -```json title="migrations/app/20260707T1006_add_user_phone/migration.json" -{ - "from": "705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5", - "to": "925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72", - "providedInvariants": [], - "createdAt": "2026-07-07T10:06:55.937Z", - "migrationHash": "4b57fa2141c8ad94476d5de66c451468fd6c864210481ad00ac89b259491fbcf" -} -``` - -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. +- its own hash, `migrationHash`, which `npx prisma migration check` reads -Together, the three files make a migration both editable and safe to run: +Do not edit `migration.json` or `ops.json` by hand, because both are written for you from `migration.ts`. Edit `migration.ts` and recompile instead. -```text -migration.ts you author the change -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.) +Your [migration history](/orm/migrations/the-migration-graph) branches when two people plan migrations from the same contract state on separate branches, because each of those migrations records the same hash as the state it starts `from`. [A worked example](/orm/migrations/the-migration-graph#a-worked-example) shows the two migrations to plan after the merge, one per branch. ## Every operation checks itself -Inside `ops.json`, each operation runs in three parts, in order: +Inside `ops.json`, an operation is not only the change itself: it also carries the checks that decide whether the change needs to run and whether it worked. Here are the parts of the operation that adds the `phone` column: + +- **Precheck**: confirms the database is in the state the change expects, here that `"user"` has no `phone` column. +- **Execute**: the statements that make the change, here `ALTER TABLE "public"."user" ADD COLUMN "phone" text`. +- **Postcheck**: confirms the change is in the database, here that the `phone` column exists. Not every operation has a postcheck, and because the postcheck is what lets Prisma ORM tell that a change is already done, an operation without one is never skipped. -1. **Precheck**: confirms the database is in the expected state before the change runs. -2. **Execute**: the statements that make the change. -3. **Postcheck**: confirms the change worked after it runs. +When `db migrate` reaches an operation, it runs the postcheck before the change as well as after, because if the postcheck already passes then the change is in the database and there is nothing to do, so the operation is skipped. If the postcheck does not pass, `db migrate` runs the precheck, then the statements, then the postcheck, and it stops the run if either check fails. Once the operations are done, it checks the database against the contract before it updates the marker, which catches a database that satisfied every individual operation but still doesn't match what you declared. If `"user"` already had a `phone` column of another type, for instance, the postcheck skips the operation and that last check fails the run. -Each operation also carries a class, which drives the `(destructive)` flags and data-loss warnings you see in CLI output: +Each operation also has an `operationClass`, which says what kind of change the operation makes to the database and which you choose yourself when you write a raw SQL operation: -- **Additive**: safe to apply, like adding a column. -- **Destructive**: can lose data, like dropping a column. +- **Additive**: adds something new, like a column. +- **Widening**: loosens a constraint or lets a type accept more values, like dropping `NOT NULL` from a column. +- **Destructive**: removes or changes something that exists, like dropping a column, and can lose data. - **Data**: changes rows, not structure. -```json title="One operation from ops.json" -{ - "id": "column.public.user.phone", - "label": "Add column \"phone\" to \"user\"", - "operationClass": "additive", - "precheck": [ - { - "description": "ensure column \"phone\" is missing", - "sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"information_schema\".\"columns\" WHERE (\"table_schema\" = $1 AND \"table_name\" = $2 AND \"column_name\" = $3)) AS \"result\"", - "params": ["public", "user", "phone"] - } - ], - "execute": [ - { - "description": "add column \"phone\"", - "sql": "ALTER TABLE \"public\".\"user\" ADD COLUMN \"phone\" text" - } - ], - "postcheck": [ - { - "description": "verify column \"phone\" exists", - "sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"information_schema\".\"columns\" WHERE (\"table_schema\" = $1 AND \"table_name\" = $2 AND \"column_name\" = $3)) AS \"result\"", - "params": ["public", "user", "phone"] - } - ] -} -``` +Destructive operations are the only class that gets a warning, and it is only a warning: nothing stops and waits for your answer. `migration plan` prints `This migration contains destructive operations that may cause data loss.` when it writes the migration, and `npx prisma migration show ` prints it for any migration on disk, so you see it while reviewing. `db migrate` runs those operations without asking and prints the warning again afterwards. + +Not every `db migrate` run gets to the end, and what you do next depends on how it stopped. -This structure is what makes Prisma ORM migrations safe in the situations where classic SQL migrations hurt: +A migration can fail while it runs, and what that leaves behind depends on your database. On PostgreSQL, one `db migrate` run is one transaction, so a failure leaves the database as it was before the run started. On MongoDB, a run is not one transaction, so the operations that finished before the failure stay in the database. [When something goes wrong](/orm/migrations/applying-a-migration#when-something-goes-wrong) covers recovery. -- **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. -- **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. +A run also ends early when the database isn't in the state your migration assumed, because a precheck then stops the run before the change is made, for example when a migration sets `NOT NULL` on a column that still holds `NULL`. The error names the operation and the check that failed, so you know exactly which assumption was wrong. Fill those rows in `migration.ts`, recompile, and run `db migrate` again, as [Recovery](/orm/migrations/rollbacks-and-recovery#recovery-when-a-migration-fails-partway) shows. ## 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: +The migration commands are part of the [Prisma ORM CLI](/cli) and run as `npx prisma `. The commands in this table read only files, so they need no database connection: | Command | What it does | | ------------------------- | ----------------------------------------------------------------------- | -| `migration plan` | [Generate a migration](/orm/migrations/generating-a-migration) from your contract changes | -| `migration new` | Scaffold an empty migration for [manual authoring](/orm/migrations/editing-a-migration#starting-from-a-blank-migration) | -| `migration show ` | Print one migration's operations, DDL preview, and metadata | +| [`migration plan`](/cli/migration-plan) | [Generate a migration](/orm/migrations/generating-a-migration) from your contract changes | +| [`migration new`](/cli/migration-new) | Write an empty migration for a [data-only or hand-written change](/orm/migrations/editing-a-migration#starting-from-a-blank-migration) | +| [`migration show `](/cli/migration-show) | Print one migration's operations, DDL preview, and metadata | | `migration list` | List every on-disk migration | -| `migration graph` | Draw the [migration graph](/orm/migrations/the-migration-graph) | -| `migration check` | Verify migration files and graph integrity (useful in CI) | +| `migration graph` | Draw your [migration history](/orm/migrations/the-migration-graph) as a graph | +| `migration check` | Check that each migration's `migrationHash` still matches its files and no files are missing, for example in CI | -Three commands talk to a database: +`` is a migration's directory name, its path under `migrations/app/`, or the first 6 or more characters of its `migrationHash`. + +The commands in this table connect to a database: | Command | What it does | | ------------------ | ---------------------------------------------------------------------------- | -| `db migrate` | [Apply pending migrations](/orm/migrations/applying-a-migration) | -| `migration status` | Show where the database is in the graph and which migrations are pending | +| [`db migrate`](/cli/db-migrate) | [Apply pending migrations](/orm/migrations/applying-a-migration) | +| [`migration status`](/cli/migration-status) | Show which contract state the database matches and which migrations are pending | | `migration log` | Show the history of migrations the database has actually applied | -Note that applying is `db migrate`, not `migration apply`: `migration ...` commands manage the on-disk migration directories, while `db migrate` moves a database. +If you used Prisma ORM 7, these commands replace its migrate commands: + +- `migrate dev` becomes `migration plan`, then `db migrate --advance-ref db`. To make a development database match the contract without migration files, it becomes `db update`, which changes the database directly and can point the `db` ref at the new state, as [The db ref](/orm/migrations/generating-a-migration#the-db-ref-skipping---from) lists. +- `migrate deploy` becomes `db migrate`. +- `migrate reset` has no equivalent. Drop and recreate the database with your own tools, then run `npx prisma db migrate --advance-ref db` to run your migrations again, or `npx prisma db init` to create what your contract declares without running them. +- `migrate diff` becomes `migration show `, which prints one migration, or `db update --dry-run`, which shows the operations that would make a database match the contract. +- Baselining, marking an existing database as already migrated, is now [`npx prisma db sign`](/cli/db-sign), which checks that the tables match your contract before it writes the marker. For a database Prisma ORM 7 migrated, follow [Prisma ORM 7 to 8 (PostgreSQL)](/guides/upgrade-prisma-orm/postgresql#4-transfer-migration-ownership). -Here is the whole loop in motion, from idea to typed code against the new schema: +[Coming from Prisma ORM 7](/orm/coming-from-prisma-orm-7#commands) has the full table, including `migrate resolve`. + +Here is the whole loop, from idea to typed code: