Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "docs",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["--filter", "docs", "exec", "next", "dev", "--port", "3105"],
"port": 3105
}
]
}
5 changes: 1 addition & 4 deletions apps/docs/content/docs/orm/next/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,5 @@
"title": "Next",
"defaultOpen": true,
"root": true,
"pages": [
"---Introduction---",
"index"
]
"pages": ["---Introduction---", "index", "---Migrations---", "...migrations"]
}
167 changes: 167 additions & 0 deletions apps/docs/content/docs/orm/next/migrations/applying-a-migration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
title: Applying a migration
description: prisma-next migrate walks the graph from where your database is to where you want it — with a preview, a checkpoint at every step, and safe retries.
url: /orm/next/migrations/applying-a-migration
metaTitle: Applying a migration in Prisma Next
metaDescription: How prisma-next migrate applies migrations — targeting refs and hashes, previewing with --show, checking status, and running safely in development and production.
badge: early-access
---

Applying is the one step that touches a database, and it has one command:

```bash
npx prisma-next migrate
```

Note the verb: it's `migrate`, not `migration apply`. The `migration ...` subcommands manage files on disk; `migrate` moves a database. It reads where the database currently is (its **marker**), finds a path through the [migration graph](/orm/next/migrations/the-migration-graph) to the target, and applies each migration along the path — running every operation's precheck, execute, and postcheck as it goes.

The connection comes from `db.connection` in `prisma-next.config.ts`, or `--db` to override:

```bash
npx prisma-next migrate --db $DATABASE_URL
```

A successful run reports every operation it performed and the marker it left behind:

```text
✔ 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: sha256:e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63

Next: prisma-next migration status
```

## Check before, preview, then apply

The habit worth building — especially against shared databases — is a three-step rhythm:

```bash
# 1. Where is the database, what's pending?
npx prisma-next migration status --db $DATABASE_URL

# 2. What exactly would run?
npx prisma-next migrate --show --db $DATABASE_URL

# 3. Run it.
npx prisma-next migrate --db $DATABASE_URL
```

`migration status` draws the path between the database's marker and the target, flagging each migration as applied or pending:

```text
* 925198f @contract
|^ 20260707T1006_add_user_phone 705b1a6 -> 925198f 1 ops > pending
* 705b1a6 @db (db)
|^ 20260707T1005_init - -> 705b1a6 2 ops + applied
* -

1 pending — run `prisma-next migrate --to 925198f3cc27`
```

Read the markers on the right: `@db` is where the database is, `@contract` is where your emitted contract is, and `(db)` is the [ref](/orm/next/migrations/the-migration-graph#refs-naming-places-in-the-graph) of that name pointing at the same node.

`migrate --show` is the read-only dry run: it prints the migrations that would execute and stops. Nothing touches the database.

After applying, `migration log` shows the database's own record of what ran — an append-only **ledger** the runner writes alongside the marker:

```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
```

## Choosing a target

With no `--to`, `migrate` advances toward your emitted contract. To aim somewhere specific, `--to` accepts the same reference grammar as everywhere else — a ref name, a contract hash or prefix, a migration directory name, or `<dir>^` for the state before a migration:

```bash
npx prisma-next migrate --to prod --db $DATABASE_URL # a ref
npx prisma-next migrate --to sha256:e6b5c28 --db $DATABASE_URL # a hash prefix
npx prisma-next migrate --to 20260707T1005_init --db $DATABASE_URL
```

If the graph has branched and more than one tip is reachable, `migrate` refuses to guess 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. Advancing one called `db` is what keeps [`migration plan`](/orm/next/migrations/generating-a-migration#skipping---from-the-db-ref) incremental:

```bash
npx prisma-next migrate --advance-ref db
```

## When something goes wrong

The runner stops at the first failing operation and tells you which one, why, and what to do:

```text
✖ Operation pgvector.install-vector-extension failed during execution: create extension "vector" (PN-RUN-3000)
Why: extension "vector" is not available
Fix: Fix the issue and re-run `prisma-next migrate --to <contract>` — previously applied migrations are preserved.
```

Three properties make failure boring instead of terrifying:

- **On PostgreSQL, a failed run leaves nothing behind.** The entire `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 — 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 that snuck in out-of-band doesn't break the run — it just becomes a skip. 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/next/migrations/rollbacks-and-recovery) for the full failure playbook.

Before running any DDL, `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.

## Development vs. production

The commands are the same everywhere; what changes is where the files come from and who runs them.

**In development**, you're the one planning and editing, and you apply immediately:

```bash
npx prisma-next migration plan --name my_change && npx prisma-next migrate --advance-ref db
```

**In CI and production**, migrations arrive via your repo, already planned, reviewed, and merged. The deploy step is:

```bash
npx prisma-next migration check # files intact, graph well-formed (offline)
npx prisma-next migrate --show --db $DATABASE_URL # log what's about to run
npx prisma-next migrate --db $DATABASE_URL
```

A production nicety that falls out of the design: the runner executes only `ops.json` — plain data. Your `migration.ts` files, and any TypeScript they import, are never executed with production credentials.

Concurrent deploys are safe: on PostgreSQL the whole apply runs inside a transaction guarded by an advisory lock, so two `migrate` runs serialize instead of interleaving. On MongoDB — where cross-collection DDL transactions don't exist — 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.

## Extension spaces

If your project uses database 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/<extension>/` next to your app's. One `migrate` run walks them all — extensions first, then your app — and reports each space separately:

```text
✔ Applied 2 migration(s) (20 operation(s)) across 2 contract space(s)

Extension space: pgvector
└─ Enable extension "vector"

App space
├─ 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 (`migrate` runs against the real target; use `--show` and a staging database), and an apply-time check that `ops.json` still matches `migration.ts` (today that's `migration check`'s job — run it in CI).

:::

## Prompt your coding agent

- "Check migration status against staging and apply whatever is pending."
- "Preview what `migrate --to prod` would run and summarize the destructive operations."
- "Apply the pending migrations and advance the `db` ref."

## See also

- [Rollbacks and recovery](/orm/next/migrations/rollbacks-and-recovery): when you need to go back instead of forward
- [The migration graph](/orm/next/migrations/the-migration-graph): markers, refs, and pathfinding
- [Generating a migration](/orm/next/migrations/generating-a-migration): producing what `migrate` runs
207 changes: 207 additions & 0 deletions apps/docs/content/docs/orm/next/migrations/editing-a-migration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
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.
url: /orm/next/migrations/editing-a-migration
metaTitle: Editing a migration in Prisma Next
metaDescription: How to edit a Prisma Next migration.ts — fill placeholder data transforms, write typed backfills, use rawSql, and recompile ops.json with node migration.ts.
badge: early-access
---

The planner writes a good first draft, but plenty of real migrations need a human (or agent) decision: what to put in existing rows when a column becomes required, which order two steps must run in, one statement the planner has no factory for. In Prisma Next you make those changes by editing `migration.ts` — ordinary TypeScript with autocomplete and type checking — and recompiling it. You never hand-edit SQL files, and you never hand-edit `ops.json`.

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:

```bash
node migrations/app/20260707T1008_add_display_name/migration.ts
```

```text
Wrote ops.json + migration.json to migrations/app/20260707T1008_add_display_name
```

The recompile also re-attests the package: `migration.json` gets a fresh `migrationHash` covering the compiled output. 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.

## 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:

```bash
npx prisma-next 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.
```

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:

```ts title="migration.ts (as planned)"
override get operations() {
return [
this.addColumn({
schema: 'public',
table: 'user',
column: col('displayName', 'text', { codecRef: { codecId: 'pg/text@1' } }),
}),
this.dataTransform(endContract, 'backfill-user-displayName', {
check: () => placeholder('backfill-user-displayName:check'),
run: () => placeholder('backfill-user-displayName:run'),
}),
this.setNotNull({ schema: 'public', table: 'user', column: 'displayName' }),
];
}
```

A `dataTransform` takes two closures. `check` asks "are there rows that still need this?" — it must be a row-returning query where *any row* means work remains (the conventional shape is `select('id').where(<violation>).limit(1)`). `run` performs the change. Fill them with the typed SQL query builder, wired to the contract snapshot sitting next to the migration:

```ts title="migration.ts (filled in)"
import type { Contract as End } from './end-contract';
import endContractJson from './end-contract.json' with { type: 'json' };
import type { Contract as Start } from './start-contract';
import startContract from './start-contract.json' with { type: 'json' };
import { Migration, MigrationCLI, col } from '@prisma-next/postgres/migration';
import postgresAdapter from '@prisma-next/adapter-postgres/runtime';
import { sql } from '@prisma-next/sql-builder/runtime';
import { createExecutionContext, createSqlExecutionStack } from '@prisma-next/sql-runtime';
import postgresTarget, { PostgresContractSerializer } from '@prisma-next/target-postgres/runtime';

const endContract = new PostgresContractSerializer().deserializeContract(endContractJson);

const db = sql<End>({
context: createExecutionContext({
contract: endContract,
stack: createSqlExecutionStack({ target: postgresTarget, adapter: postgresAdapter }),
}),
});

export default class M extends Migration<Start, End> {
override readonly endContractJson = endContract;
override readonly startContractJson = startContract;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

override get operations() {
return [
this.addColumn({
schema: 'public',
table: 'user',
column: col('displayName', 'text', { codecRef: { codecId: 'pg/text@1' } }),
}),
this.dataTransform(endContract, 'backfill-user-displayName', {
check: () =>
db.public.user
.select('id')
.where((f, fns) => fns.eq(f.displayName, null))
.limit(1),
run: () =>
db.public.user
.update({ displayName: 'Anonymous' })
.where((f, fns) => fns.eq(f.displayName, null)),
}),
this.setNotNull({ schema: 'public', table: 'user', column: 'displayName' }),
];
}
}

MigrationCLI.run(import.meta.url, M);
```

Two details worth pausing on:

- **The types come from the migration's own contract snapshot** (`./end-contract`), not your live app contract. You're type-checking against the schema *as it will exist when this step runs* — which is 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.

Recompile with `node migration.ts` and inspect what the backfill became:

```json title="ops.json (the compiled dataTransform)"
{
"id": "data_migration.backfill-user-displayName",
"label": "Data transform: backfill-user-displayName",
"operationClass": "data",
"precheck": [
{
"description": "Check backfill-user-displayName has work to do",
"sql": "SELECT EXISTS (SELECT \"id\" AS \"id\" FROM \"public\".\"user\" WHERE \"displayName\" IS NULL LIMIT 1) AS ok",
"params": []
}
],
"execute": [
{
"description": "Run backfill-user-displayName",
"sql": "UPDATE \"public\".\"user\" SET \"displayName\" = $1 WHERE \"displayName\" IS NULL",
"params": ["Anonymous"]
}
],
"postcheck": [
{
"description": "Verify backfill-user-displayName resolved all violations",
"sql": "SELECT NOT EXISTS (SELECT \"id\" AS \"id\" FROM \"public\".\"user\" WHERE \"displayName\" IS NULL LIMIT 1) AS ok",
"params": []
}
]
}
```

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 `"Anonymous"` travels in `params`, through the driver's parameter binder, 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.

## Escape hatch: raw SQL

For anything the operation factories don't cover — enabling an extension, `CREATE INDEX CONCURRENTLY`, a vendor-specific statement — use `rawSql`, and keep the same three-phase safety if you can:

```ts
rawSql({
id: 'extension.pgcrypto',
label: 'Enable extension "pgcrypto"',
operationClass: 'additive',
target: { id: 'postgres' },
precheck: [
{ description: 'not yet enabled', sql: "SELECT NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto')" },
],
execute: [{ description: 'enable it', sql: 'CREATE EXTENSION IF NOT EXISTS pgcrypto' }],
postcheck: [
{ description: 'now enabled', sql: "SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto')" },
],
}),
```

The prechecks and postchecks are optional — but they're what makes a failed run resumable and a mistake diagnosable, so 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.

## 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, attested package:

```bash
npx prisma-next migration new --name backfill_scores
```

Write your operations in the generated `migration.ts`, then compile it the same way: `node migration.ts`.

## Editing checklist

1. Edit `migration.ts` — never `ops.json`.
2. Recompile: `node <migration-dir>/migration.ts`.
3. Review the diff of `ops.json` — that's what will run.
4. Verify: `npx prisma-next migration check`.
5. Commit `migration.ts`, `ops.json`, and `migration.json` together.

:::note[What's early]

The typed-builder wiring at the top of the filled example (deserializing the contract, building the `db` handle) is more ceremony than we want; expect it to shrink. On MongoDB, hand-authored data transforms currently use raw Mongo command shapes rather than the full typed builder. `rawSql` and the scaffolded placeholder flow shown here work today, end to end.

:::

## Prompt your coding agent

- "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."
- "Recompile the migration I just edited and show me the ops.json diff."

## See also

- [Generating a migration](/orm/next/migrations/generating-a-migration): where the scaffold comes from
- [Applying a migration](/orm/next/migrations/applying-a-migration): run the edited migration
- [Data Migrations in Prisma Next](https://www.prisma.io/blog/data-migrations-in-prisma-next): the design story for `dataTransform`
Loading
Loading