diff --git a/apps/docs/content/docs/ai/tools/skills.mdx b/apps/docs/content/docs/ai/tools/skills.mdx index acc43857a6..58bf21e3dc 100644 --- a/apps/docs/content/docs/ai/tools/skills.mdx +++ b/apps/docs/content/docs/ai/tools/skills.mdx @@ -63,6 +63,31 @@ Prisma Postgres workflows across Console, CLI, Management API, and SDK. Covers `npx create-db`, Console operations, programmatic provisioning via the Management API, and the `@prisma/management-api-sdk`. Use this when creating or managing Prisma Postgres databases. +## Available skills for Prisma Next + +[Prisma Next](/orm/next) ships its own skill set, versioned with the product. Projects scaffolded with `create-prisma` install them automatically; add them to an existing project with: + +```npm +npx skills add prisma/prisma-next/skills +``` + +| Skill | What your agent uses it for | +| --- | --- | +| `prisma-next` | Route a general Prisma Next question to the right skill below | +| `prisma-next-quickstart` | First-touch setup and orientation after scaffolding | +| `prisma-next-contract` | Edit the schema: models, relations, enums, namespaces, extensions, then emit the contract | +| `prisma-next-queries` | Write queries in the right lane: the ORM API, the SQL query builder, or the MongoDB pipeline builder | +| `prisma-next-runtime` | Wire `db.ts`, middleware, and environment configuration | +| `prisma-next-migrations` | Author migrations: `db update` vs `migration plan`, and filling in data transforms | +| `prisma-next-migration-review` | Review migrations on deploy and reconcile concurrent migration work | +| `prisma-next-build` | Integrate with the build tool (the Vite contract-emit plugin today) | +| `prisma-next-debug` | Read structured error envelopes (`PN-*` codes) and route to the fix | +| `prisma-next-upgrade` | Move a project between Prisma Next releases | +| `prisma-next-extension-upgrade` | Upgrade extension packs alongside the core | +| `prisma-next-feedback` | File a bug or feature request, or route a question to the Prisma Discord | + +The Prisma Next docs reference these in each page's "Prompt your coding agent" section, with prompts that map to the page you are reading. + ## Useful commands List available skills before installing: diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 906e0ee094..d52b2043e8 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -71,7 +71,7 @@ model Post { ## 5. Apply and query -Run `prisma-next db init` (or `prisma-next db update` on an existing database). The extension ships its own migration, so this step runs `CREATE EXTENSION IF NOT EXISTS vector` for you. Then query with the operations the extension adds: +Run `prisma-next db init` (or `prisma-next db update` on an existing database). The extension ships its own migration, so this step runs `CREATE EXTENSION IF NOT EXISTS vector` for you. If `db init` reports a contract-space layout violation instead, run `prisma-next migration plan` once: it materializes the extension's baseline migration under `migrations//`, and `db init` then proceeds. Then query with the operations the extension adds: ```ts title="src/prisma/similarity-search.ts" const plan = db.sql.public.post diff --git a/apps/docs/content/docs/orm/next/index.mdx b/apps/docs/content/docs/orm/next/index.mdx index 4b94e89e42..5e7cf74458 100644 --- a/apps/docs/content/docs/orm/next/index.mdx +++ b/apps/docs/content/docs/orm/next/index.mdx @@ -25,7 +25,7 @@ Prisma's current architecture tightly couples three layers: the schema language, - **Better queries**: a cleaner API with simpler nested queries, custom collection methods on your models, and streaming results, plus a low-level, type-safe SQL query builder for the cases that need raw control. - **Extensible by design**: a minimal core exposed through a public SPI. Everything around it, including Postgres support itself, is an extension, so you can add databases, query builders, data types, and middleware. -- **Rethought migrations**: graph-based migrations that resolve branch conflicts automatically and make partial failures safe to retry, with both schema and data migrations written in TypeScript and validated against your contract. +- **Rethought migrations**: [graph-based migrations](/orm/next/migrations/the-migration-graph) that resolve branch conflicts automatically and make partial failures safe to retry, with both schema and data migrations written in TypeScript and validated against your contract. - **AI-agent friendly**: your schema compiles to a machine-readable contract, every query produces a structured, inspectable plan, and middleware adds compile-time guardrails. The installers also register agent skills so your editor's AI assistant can drive Prisma Next changes safely. ## How it works diff --git a/apps/docs/content/docs/orm/next/meta.json b/apps/docs/content/docs/orm/next/meta.json index f35013acbb..9ba57eeb71 100644 --- a/apps/docs/content/docs/orm/next/meta.json +++ b/apps/docs/content/docs/orm/next/meta.json @@ -11,6 +11,8 @@ "...contract-authoring", "---Fundamentals---", "...fundamentals", + "---Migrations---", + "...migrations", "---Middleware---", "...middleware", "---Extensions---", diff --git a/apps/docs/content/docs/orm/next/migrations/applying-a-migration.mdx b/apps/docs/content/docs/orm/next/migrations/applying-a-migration.mdx new file mode 100644 index 0000000000..f3ebb37e22 --- /dev/null +++ b/apps/docs/content/docs/orm/next/migrations/applying-a-migration.mdx @@ -0,0 +1,182 @@ +--- +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, from targeting refs and hashes and previewing with --show to 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 +``` + +`App space` is your application's migration lane, one of the run's *contract spaces*; projects that use database extensions gain additional spaces, covered in [Extension spaces](#extension-spaces) below. + +## 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 draws the path from the database's position to the target and stops. Nothing touches the database: + +```text +│↑ 20260707T1006_add_user_phone 705b1a6 → 925198f ↑ will run +○ 705b1a6 +│↑ 20260707T1005_init ∅ → 705b1a6 ↑ will run +○ ∅ @db + +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: + +```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 `^` 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#the-db-ref-skipping---from) 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 ` — 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, 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 that snuck in out-of-band doesn't break the run; it 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. That's why the [editing rule](/orm/next/migrations/editing-a-migration) matters: commit `migration.ts` and `ops.json` together, and let `migration check` (exit codes `0`/`2`/`4`, see [Generating a migration](/orm/next/migrations/generating-a-migration#reviewing-what-you-planned)) catch a stale recompile before deploy. 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](/orm/next/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 `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 + +Projects scaffolded with `create-prisma` install [Prisma Next skills](/ai/tools/skills#available-skills-for-prisma-next) for your coding agent. Ask your agent to: + +- "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 diff --git a/apps/docs/content/docs/orm/next/migrations/editing-a-migration.mdx b/apps/docs/content/docs/orm/next/migrations/editing-a-migration.mdx new file mode 100644 index 0000000000..45475fbcce --- /dev/null +++ b/apps/docs/content/docs/orm/next/migrations/editing-a-migration.mdx @@ -0,0 +1,236 @@ +--- +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 by filling placeholder data transforms, writing typed backfills, using rawSql, and recompiling 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 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. + +## 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().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 startContractJson 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({ + context: createExecutionContext({ + contract: endContract, + stack: createSqlExecutionStack({ target: postgresTarget, adapter: postgresAdapter }), + }), +}); + +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('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*. 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 Early Access shape; 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. + +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 that `"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. + +## 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: + +```ts title="migration.ts (MongoDB, excerpt)" +import { dataTransform, setValidation } from '@prisma-next/target-mongo/migration'; + +override get operations() { + const storageHash = this.endContract.storage.storageHash; + const productsValidator = this.endContract.collection.products.validator; + return [ + setValidation('products', productsValidator.jsonSchema, { + validationLevel: productsValidator.validationLevel, + validationAction: productsValidator.validationAction, + }), + dataTransform('backfill-product-status', { + check: { source: () => existingProductsWithoutStatus(storageHash) }, + run: () => backfillRun(storageHash), + }), + ]; +} +``` + +The full working migration, including the two query-plan helpers, is in the Prisma Next repo's [retail-store example](https://github.com/prisma/prisma-next/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, like everything the CLI writes): + +```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.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 and the raw Mongo command shapes above are the current Early Access 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. + +::: + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install [Prisma Next skills](/ai/tools/skills#available-skills-for-prisma-next) for your coding agent. 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." +- "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` diff --git a/apps/docs/content/docs/orm/next/migrations/generating-a-migration.mdx b/apps/docs/content/docs/orm/next/migrations/generating-a-migration.mdx new file mode 100644 index 0000000000..4bc0bd51cc --- /dev/null +++ b/apps/docs/content/docs/orm/next/migrations/generating-a-migration.mdx @@ -0,0 +1,217 @@ +--- +title: Generating a migration +description: Turn a contract change into a reviewable migration with prisma-next migration plan. +url: /orm/next/migrations/generating-a-migration +metaTitle: Generating a migration in Prisma Next +metaDescription: A step-by-step tutorial for prisma-next migration plan, from contract change to a planned, reviewable migration, entirely offline. +badge: early-access +--- + +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; it never connects to a database, so you can plan on a plane, in CI, or in a sandbox with no credentials. + +Start from a minimal project; `npx create-prisma@next` scaffolds one. + +## Your first migration + +Say your contract has a single model: + +```prisma title="contract.prisma" +model User { + id Int @id + email String + name String? + + @@map("user") +} +``` + +First, emit the contract. This compiles the schema into the `contract.json` artifact that every other command reads: + +```bash +npx prisma-next contract emit +``` + +Now plan. `--name` sets the human-readable part of the directory name; without it the directory is called `migration`: + +```bash +npx prisma-next migration plan --name init +``` + +```text +✔ Planned 2 operation(s) + +│ +├─ Create schema "public" +└─ Create table "user" + +from: null +to: sha256:705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 +App space → migrations/app/20260707T1005_init + +Next: review migrations/app/20260707T1005_init if needed, then run prisma-next migrate. + +DDL preview + +CREATE SCHEMA IF NOT EXISTS "public"; +CREATE TABLE "public"."user" ( + "email" text NOT NULL, + "id" int4 NOT NULL, + "name" text, + PRIMARY KEY ("id") +); +``` + +Four things to notice: + +- **The DDL preview is right there.** You see the exact SQL before anything exists but files. +- **`from: null`** means this migration starts from an empty database; it's the root of your [migration graph](/orm/next/migrations/the-migration-graph). +- **`to:` is your contract's hash.** The migration promises to deliver a database matching exactly the contract you just emitted. +- **`App space`** is your application's migration lane. Database extensions bring their own lanes; see [extension spaces](/orm/next/migrations/applying-a-migration#extension-spaces). + +The planned migration directory contains the TypeScript source, the compiled operations, and the contract snapshots: + +```text +migrations/app/20260707T1005_init/ +├── migration.ts +├── ops.json +├── migration.json +└── end-contract.json (+ end-contract.d.ts) +``` + +Open `migration.ts`. It reads like a description of the change, because it is one: + +```ts title="migrations/app/20260707T1005_init/migration.ts" +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import { Migration, MigrationCLI, col, primaryKey } from '@prisma-next/postgres/migration'; + +export default class M extends Migration { + override readonly endContractJson = endContract; + + override get operations() { + return [ + this.createSchema({ schema: 'public' }), + this.createTable({ + schema: 'public', + table: 'user', + columns: [ + col('email', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }), + col('id', 'int4', { notNull: true, codecRef: { codecId: 'pg/int4@1' } }), + col('name', 'text', { codecRef: { codecId: 'pg/text@1' } }), + ], + constraints: [primaryKey(['id'])], + }), + ]; + } +} + +MigrationCLI.run(import.meta.url, M); +``` + +Two notes on reading it: 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; 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), that's [Editing a migration](/orm/next/migrations/editing-a-migration). + +## The second migration: planning a delta + +Apply the first migration (covered properly in [Applying a migration](/orm/next/migrations/applying-a-migration)), then make another change: add a `phone` field: + +```prisma +model User { + id Int @id + email String + name String? + phone String? + + @@map("user") +} +``` + +Emit and plan again. This time, tell the planner where to start from, so it plans the *delta* rather than the whole schema: + +```bash +npx prisma-next contract emit +npx prisma-next migration plan --name add_user_phone --from 20260707T1005_init +``` + +```text +✔ Planned 1 operation(s) + +│ +└─ Add column "phone" to "user" + +from: sha256:705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5 +to: sha256:925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72 +App space → migrations/app/20260707T1006_add_user_phone + +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". + +### The db ref: skipping --from + +Passing `--from` every time gets old. The planner's default is smarter: if a [ref](/orm/next/migrations/the-migration-graph#refs-naming-places-in-the-graph) named **`db`** exists, planning starts from whatever it points at. Keep it advanced as part of applying: + +```bash +npx prisma-next migrate --advance-ref db +npx prisma-next migration plan --name next_change # starts from the db ref automatically +``` + +:::warning[Without `--from` or a `db` ref, plans start from empty] + +If there's no `db` ref and you omit `--from`, `migration plan` plans from an **empty database**: a full `CREATE`-everything migration, not a delta. If you expected a one-column change and got a wall of `CREATE TABLE`, this is why. Pass `--from` or set up the `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, and the planner can write the `ADD COLUMN` and the `SET NOT NULL`, but only you know what the existing rows should contain. Rather than guessing, it scaffolds the decision as a **placeholder**: + +```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. +Then run: node migrations/app/20260707T1008_add_display_name/migration.ts +``` + +The generated migration sandwiches a `dataTransform` between the schema steps, with `placeholder(...)` where your backfill query goes. Filling it in is the subject of [Editing a migration](/orm/next/migrations/editing-a-migration). + +## Reviewing what you planned + +Three offline commands close the loop before anything runs: + +```bash +# One migration in detail: operations, metadata, DDL preview +npx prisma-next migration show 20260707T1006_add_user_phone + +# The whole graph, with your new edge in place +npx prisma-next migration graph + +# Integrity check: hashes match, files complete, graph well-formed — exits non-zero on failure +npx prisma-next 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`. + +:::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`. + +::: + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install [Prisma Next skills](/ai/tools/skills#available-skills-for-prisma-next) for your coding agent. Ask your agent to: + +- "Add a required `displayName` field to User, emit the contract, and plan the migration." +- "Plan a migration named `add-orders-table` and show me its DDL preview before I commit it." +- "Run `migration check` and explain any integrity failures." + +## See also + +- [Editing a migration](/orm/next/migrations/editing-a-migration): fill placeholders, add data steps, write raw SQL +- [Applying a migration](/orm/next/migrations/applying-a-migration): run what you planned +- [TypeScript Migrations in Prisma Next](https://www.prisma.io/blog/typescript-migrations-in-prisma-next): the design story behind `migration.ts` diff --git a/apps/docs/content/docs/orm/next/migrations/how-migrations-work.mdx b/apps/docs/content/docs/orm/next/migrations/how-migrations-work.mdx new file mode 100644 index 0000000000..f831f7390f --- /dev/null +++ b/apps/docs/content/docs/orm/next/migrations/how-migrations-work.mdx @@ -0,0 +1,236 @@ +--- +title: How migrations work +description: Change your contract, plan a migration, review it, apply it. Every step is checked before and after it runs. +url: /orm/next/migrations/how-migrations-work +metaTitle: How migrations work in Prisma Next +metaDescription: What a Prisma Next migration is, the plan-review-apply loop, the files inside a migration directory, and why every operation is safe to retry. +badge: early-access +--- + +A migration is how Prisma Next 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 it compiles to; the **schema** is the database's actual structure, and migrations are what move it. You add a field to a model, and 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 the change is reviewable, repeatable, and versioned with your code. + +The workflow is a loop you will run many times a day in development: + + + +1. **Change your contract**: edit your `.prisma` file, then run `prisma-next contract emit`, which compiles the schema into the `contract.json` that every other command reads. +2. **Plan a migration**: `prisma-next 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 needs a data step, then re-run the file to recompile it. +4. **Apply it**: `prisma-next migrate` runs the pending migrations against your database. + +In a project with a database connection configured (the [quickstart](/next/quickstart/postgresql) gives you one), the whole loop is three commands. Add an optional `phone String?` field to a model in your `.prisma` file, then: + +```bash +npx prisma-next contract emit +npx prisma-next migration plan --name add_user_phone +npx prisma-next migrate +``` + +`migration plan` prepares the SQL for the migration, but doesn't run it: + +```text +✔ Planned 1 operation(s) + +│ +└─ Add column "phone" to "user" + +DDL preview + +ALTER TABLE "public"."user" ADD COLUMN "phone" text; +``` + +and `migrate` applies the migration to your database and confirms what it applied: + +```text +✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s) +``` + +The contract-space count only matters once you add database extensions; ignore it for now. If you see the `✔ Applied` line, you've already run the entire workflow. The rest of this page covers additional detail: what a migration is, and why it looks the way it does. + +## What a migration contains + +A migration is a directory under `migrations/app/`, named with a timestamp and a slug: + +```text +migrations/ +└── app/ + └── 20260707T1006_add_user_phone/ + ├── migration.ts # the file you edit + ├── ops.json # the file Prisma runs + ├── migration.json # where this migration fits in history + ├── start-contract.json # snapshot of the contract before + ├── end-contract.json # snapshot of the contract after + └── *-contract.d.ts # types for those snapshots; they type-check migration.ts +``` + +Three files matter day to day. 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. | + +### migration.ts: the file you edit + +`migration.ts` is the authoring file. It contains the migration steps as ordinary TypeScript: + +```ts +this.addColumn(...) +this.createTable(...) +this.dataTransform(...) +``` + +Because it is TypeScript, type-checked against the contract snapshots sitting next to it, your editor autocompletes table and column names and catches mistakes before the migration touches a database. + +Use this file when you want to review or change what the migration does. See [Editing a migration](/orm/next/migrations/editing-a-migration). + +### ops.json: the file Prisma runs + +`ops.json` is the compiled form of the migration. When `migration.ts` runs (the planner does this for you; after an edit, re-run it with `node migration.ts`), Prisma turns the TypeScript steps into JSON operations. 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.json: the history marker + +`migration.json` records where this migration fits in your database history. Every contract compiles to a deterministic artifact, and hashing it gives a short identifier for that exact schema state. A migration records: + +- the schema hash it starts `from` +- the schema hash it moves `to` +- when it was created +- its own hash, so tampering is detectable + +```json title="migrations/app/20260707T1006_add_user_phone/migration.json" +{ + "from": "sha256:705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5", + "to": "sha256:925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72", + "providedInvariants": [], + "createdAt": "2026-07-07T10:06:55.937Z", + "migrationHash": "sha256: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/next/migrations/the-migration-graph) instead of a numbered list. If you want a simple linear migration system, you can ignore the graph entirely. + +Together, the three files make a migration both editable and safe to run: + +```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.) + +## Every operation checks itself + +Inside `ops.json`, each operation has three parts: a **precheck** that confirms the database is in the expected state, the **execute** statements that make the change, and a **postcheck** that confirms the change worked. Each operation also carries a class: **additive** (safe, like adding a column), **destructive** (can lose data, like dropping one), or **data** (changes rows, not structure). The class is what drives the `(destructive)` flags and data-loss warnings you'll see in CLI output. + +```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"] + } + ] +} +``` + +This structure is what makes Prisma Next migrations safe in the situations where classic SQL migrations hurt: + +- **A migration fails.** On PostgreSQL the whole run is one transaction, so a failure rolls everything back: the database returns to exactly where it was while you fix the cause and re-run. And 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 some agent) wrote the migration for you.** The intent (`migration.ts`), the exact SQL (`ops.json`), and the verification for every step are all in the diff, so review is straightforward. + +## The commands + +Everything lives under the `prisma-next` CLI. Planning and inspection are offline; they read files, not your database: + +| Command | What it does | +| ------------------------- | ----------------------------------------------------------------------- | +| `migration plan` | [Generate a migration](/orm/next/migrations/generating-a-migration) from your contract changes | +| `migration new` | Scaffold an empty migration for [manual authoring](/orm/next/migrations/editing-a-migration#starting-from-a-blank-migration) | +| `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/next/migrations/the-migration-graph) | +| `migration check` | Verify migration files and graph integrity (useful in CI) | + +Three commands talk to a database: + +| Command | What it does | +| ------------------ | ---------------------------------------------------------------------------- | +| `migrate` | [Apply pending migrations](/orm/next/migrations/applying-a-migration) | +| `migration status` | Show where the database is in the graph and which migrations are pending | +| `migration log` | Show the history of migrations the database has actually applied | + +Note that applying is `prisma-next migrate`, not `migration apply`: `migration ...` commands manage the on-disk migration directories, while `migrate` moves a database. + +Here is the whole loop in motion, from idea to typed code against the new schema: + +