Expand-and-contract migrations
When you change a production schema, you want the data to stay consistent and the app to keep running. The expand and contract pattern gets you there in two reviewable steps: first add the new column and copy the data across (expand), then remove the old column once nothing reads it (contract). In this guide you replace a published boolean on a Post model with a status enum.
In Prisma ORM the data move is not a separate script. A migration is a TypeScript file, and the backfill is one more operation in it, a dataTransform that runs in the same transaction as the schema change. Every command and output below was run against a local PostgreSQL database.
- Node.js 24 or later
- A PostgreSQL connection string for a development database, and one for the database you deploy to
- Git, for the branch-per-migration workflow
To delegate this guide to your coding agent, copy the prompt below and hand it over:
Perform an expand-and-contract migration with Prisma ORM, following https://www.prisma.io/docs/guides/database/data-migration.md.
1. In a project with a `Post` model that has a `published Boolean @default(false)` field, make sure Prisma ORM is set up (`npx prisma@latest orm init --yes --target postgres --authoring psl` if it is not), `DATABASE_URL` is in `.env`, and the current contract is applied as a migration (`npx prisma contract emit`, `npx prisma migration plan --name init`, `npx prisma db migrate --advance-ref db`). Run `npx prisma@latest init` so the Prisma agent skills are installed, and use them.
2. Expand: add `enum Status { @@type("pg/text@1") Draft = "Draft" InProgress = "InProgress" InReview = "InReview" Published = "Published" }` and `status Status @default(Draft)` to `Post`. Keep `published`. Run `contract emit` and `migration plan --name add_status`.
3. Open the planned `migration.ts` and append a `this.dataTransform(endContract, 'backfill-post-status', { check, run })` operation built with the typed SQL builder against the migration's own end contract: `check` selects `id` from `Post` where `published = true AND status != 'Published'` with `limit(1)`, `run` updates those rows to `status = 'Published'`. Recompile with `node <migration-dir>/migration.ts`, confirm `ops.json` contains a `data` operation, then apply with `npx prisma db migrate --advance-ref db`. Verify with a query that every row with `published = true` now has `status = 'Published'`.
4. Update application code to read and write `status` instead of `published`.
5. Contract: remove `published` from the contract, run `contract emit`, `migration plan --name drop_published`, review the destructive warning, and apply with `npx prisma db migrate --advance-ref db`.
6. For the production database, run `npx prisma db migrate --db "$PRODUCTION_DATABASE_URL"`, adding `--show` first if you want to preview the route. Do not use `db update` against production.If you already have a Prisma ORM project whose contract is applied through migrations, skip to step 2. This step builds the starting point: a Post model with a published boolean, an init migration, and three rows of data.
Create a project and add Prisma ORM to it:
mkdir expand-contract && cd expand-contractbun init
bunx prisma@latest orm init --yes --target postgres --authoring pslpnpm init
pnpm dlx prisma@latest orm init --yes --target postgres --authoring pslyarn init
yarn dlx prisma@latest orm init --yes --target postgres --authoring pslnpm init
npx prisma@latest orm init --yes --target postgres --authoring pslorm init writes prisma.config.ts, src/prisma/contract.prisma, src/prisma/db.ts, and .env.example, installs @prisma/orm-postgres and the prisma CLI, and emits the contract once. There is nothing to generate afterwards: the runtime reads the emitted contract.json directly, so there is no prisma generate step and no client package to install.
npm init -y sets "type": "commonjs", and orm init warns that the generated db.ts needs ES modules. Change it to "type": "module" in package.json.
Put your development connection string in .env. Both prisma.config.ts and db.ts load it:
DATABASE_URL="postgres://user:password@localhost:5432/expand_contract"Replace the generated contract with the starting model:
// use prisma-8
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
}Emit the contract, plan the first migration, and apply it. --advance-ref db records where your development database is, so the next migration plan knows what to diff against:
orm init writes prisma.config.ts, src/prisma/contract.prisma, src/prisma/db.ts, and .env.example, installs @prisma/orm-postgres and the prisma CLI, and emits the contract once. There is nothing to generate afterwards: the runtime reads the emitted contract.json directly, so there is no prisma generate step and no client package to install.
npm init -y sets "type": "commonjs", and orm init warns that the generated db.ts needs ES modules. Change it to "type": "module" in package.json.
Put your development connection string in .env. Both prisma.config.ts and db.ts load it:
DATABASE_URL="postgres://user:password@localhost:5432/expand_contract"Replace the generated contract with the starting model:
// use prisma-8
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
}Emit the contract, plan the first migration, and apply it. --advance-ref db records where your development database is, so the next migration plan knows what to diff against:
bunx prisma contract emit
bunx prisma migration plan --name init
bunx prisma db migrate --advance-ref dbpnpm prisma contract emit
pnpm prisma migration plan --name init
pnpm prisma db migrate --advance-ref dbyarn prisma contract emit
yarn prisma migration plan --name init
yarn prisma db migrate --advance-ref dbnpx prisma contract emit
npx prisma migration plan --name init
npx prisma db migrate --advance-ref db✔ Applied 1 migration(s) (2 operation(s)) across 1 contract space(s)
App space
├─ Create schema "public"
├─ Create table "Post"
└─ marker e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fde
✔ Advanced ref "db" → e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fdeSeed a few posts so the migration has data to move. Node.js 24 runs TypeScript directly:
import { db } from "./prisma/db.ts";
await db.orm.public.Post.create({ title: "Hello World", content: "First post", published: true });
await db.orm.public.Post.create({ title: "Draft ideas", content: null, published: false });
await db.orm.public.Post.create({ title: "Release notes", content: "v1.0", published: true });
const posts = await db.orm.public.Post.select("id", "title", "published").all();
console.log(posts);
await db.close();node src/seed.ts[
{ id: 1, title: 'Hello World', published: true },
{ id: 2, title: 'Draft ideas', published: false },
{ id: 3, title: 'Release notes', published: true }
]✔ Applied 1 migration(s) (2 operation(s)) across 1 contract space(s)
App space
├─ Create schema "public"
├─ Create table "Post"
└─ marker e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fde
✔ Advanced ref "db" → e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fdeSeed a few posts so the migration has data to move. Node.js 24 runs TypeScript directly:
import { db } from "./prisma/db.ts";
await db.orm.public.Post.create({ title: "Hello World", content: "First post", published: true });
await db.orm.public.Post.create({ title: "Draft ideas", content: null, published: false });
await db.orm.public.Post.create({ title: "Release notes", content: "v1.0", published: true });
const posts = await db.orm.public.Post.select("id", "title", "published").all();
console.log(posts);
await db.close();node src/seed.ts[
{ id: 1, title: 'Hello World', published: true },
{ id: 2, title: 'Draft ideas', published: false },
{ id: 3, title: 'Release notes', published: true }
]Create a branch for the expand step:
git checkout -b expand-post-statusAdd the Status enum and a status field. Leave published in place; the app keeps working against it until the contract step. Prisma ORM stores an enum as text with a CHECK constraint, and the @@type line says so:
// use prisma-8
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
status Status @default(Draft)
}
enum Status {
@@type("pg/text@1")
Draft = "Draft"
InProgress = "InProgress"
InReview = "InReview"
Published = "Published"
} Emit the contract and plan the migration. Planning is offline; it diffs the emitted contract against the db ref and writes a migration directory:
bunx prisma contract emit
bunx prisma migration plan --name add_statuspnpm prisma contract emit
pnpm prisma migration plan --name add_statusyarn prisma contract emit
yarn prisma migration plan --name add_statusnpx prisma contract emit
npx prisma migration plan --name add_status✔ Planned 2 operation(s)
migrations/app/20260910T1552_add_status
├─ Add column "status" to "Post"
└─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
from: e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fde
to: 0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4
app space: migrations/app/20260910T1552_add_status
ℹ DDL preview
ALTER TABLE "public"."Post" ADD COLUMN "status" text DEFAULT 'Draft' NOT NULL;
ALTER TABLE "public"."Post" ADD CONSTRAINT "Post_status_check_eaa35e2a" CHECK ("status" IN ('Draft', 'InProgress', 'InReview', 'Published'));Your directory name carries your own timestamp. The planner did not ask for a backfill because the column has a default, so every existing row would become Draft. That is wrong for posts that are already published. The next step fixes it.
✔ Planned 2 operation(s)
migrations/app/20260910T1552_add_status
├─ Add column "status" to "Post"
└─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
from: e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fde
to: 0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4
app space: migrations/app/20260910T1552_add_status
ℹ DDL preview
ALTER TABLE "public"."Post" ADD COLUMN "status" text DEFAULT 'Draft' NOT NULL;
ALTER TABLE "public"."Post" ADD CONSTRAINT "Post_status_check_eaa35e2a" CHECK ("status" IN ('Draft', 'InProgress', 'InReview', 'Published'));Your directory name carries your own timestamp. The planner did not ask for a backfill because the column has a default, so every existing row would become Draft. That is wrong for posts that are already published. The next step fixes it.
Open migrations/app/<timestamp>_add_status/migration.ts. It is the file the planner wrote, and you edit it like any other TypeScript. Add the imports that wire up a typed SQL builder against this migration's own end contract, then append a dataTransform operation after the two schema operations:
#!/usr/bin/env -S node
import type { Contract as End } from '../../snapshots/0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4/contract';
import endContractJson from '../../snapshots/0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4/contract.json' with { type: 'json' };
import type { Contract as Start } from '../../snapshots/e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fde/contract';
import startContract from '../../snapshots/e8804d8e27d8737475c7be0c538afd9665907df8b82c605bd5cd50926dd73fde/contract.json' with { type: 'json' };
import { Migration, MigrationCLI, col, lit } 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) as End;
const db = sql<End>({
context: createExecutionContext({
contract: endContract,
stack: createSqlExecutionStack({ target: postgresTarget, adapter: postgresAdapter }),
}),
rawCodecInferer: postgresAdapter.rawCodecInferer,
});
export default class M extends Migration<Start, End> {
override readonly startContractJson = startContract;
override readonly endContractJson = endContractJson;
override get operations() {
return [
this.addColumn({
schema: 'public',
table: 'Post',
column: col('status', 'text', {
notNull: true,
default: lit('Draft'),
codecRef: { codecId: 'pg/text@1' },
}),
}),
this.addCheckConstraint({
schema: 'public',
table: 'Post',
constraint: 'Post_status_check_eaa35e2a',
expression: "\"status\" IN ('Draft', 'InProgress', 'InReview', 'Published')",
}),
this.dataTransform(endContract, 'backfill-post-status', {
check: () =>
db.public.Post
.select('id')
.where((f, fns) => fns.and(fns.eq(f.published, true), fns.ne(f.status, 'Published')))
.limit(1),
run: () =>
db.public.Post
.update({ status: 'Published' })
.where((f, fns) => fns.and(fns.eq(f.published, true), fns.ne(f.status, 'Published'))),
}),
];
}
}
MigrationCLI.run(import.meta.url, M);The snapshot paths in your file carry your own contract hashes; keep the ones the planner wrote. The planner writes the JSON import as endContract. Rename that import to endContractJson, as the version above does, and bind endContract to the deserialized contract, because the builder and dataTransform both need the deserialized form.
Three things to notice:
checkis a query that returns a row while work remains. Prisma compiles it twice: as anEXISTSprecheck that decides whether the step runs, and as aNOT EXISTSpostcheck that proves the step finished.runis the update. Both closures are typed against the migration's own contract snapshot, which is whyf.statustype-checks even though the column does not exist in the database yet.- There is no
published: falsebranch. Those rows take the column default,Draft, whenADD COLUMNruns.
Recompile the migration. Running the file regenerates ops.json, the file db migrate actually executes, and re-signs migration.json:
node migrations/app/20260910T1552_add_status/migration.tsWrote ops.json + migration.json to /Users/you/expand-contract/migrations/app/20260910T1552_add_statusReview what the backfill compiled to. migration show lists the operation, and ops.json holds the exact SQL:
bunx prisma migration show 20260910T1552_add_statuspnpm prisma migration show 20260910T1552_add_statusyarn prisma migration show 20260910T1552_add_statusnpx prisma migration show 20260910T1552_add_status✔ 20260910T1552_add_status
3 operation(s)
├─ Add column "status" to "Post"
├─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
└─ Data transform: backfill-post-status{
"id": "data_migration.backfill-post-status",
"operationClass": "data",
"precheck": [{ "sql": "SELECT EXISTS (SELECT \"id\" AS \"id\" FROM \"public\".\"Post\" WHERE (\"published\" = $1 AND \"status\" != $2) LIMIT 1) AS ok", "params": [true, "Published"] }],
"execute": [{ "sql": "UPDATE \"public\".\"Post\" SET \"status\" = $1 WHERE (\"published\" = $2 AND \"status\" != $3)", "params": ["Published", true, "Published"] }],
"postcheck": [{ "sql": "SELECT NOT EXISTS (SELECT \"id\" AS \"id\" FROM \"public\".\"Post\" WHERE (\"published\" = $1 AND \"status\" != $2) LIMIT 1) AS ok", "params": [true, "Published"] }]
}Commit migration.ts, ops.json, and migration.json together. Recompiling stores a hash of ops.json and migration.json inside migration.json. If either file changes after that, db migrate refuses to run the migration, and npx prisma migration check reports the same problem before you commit.
✔ 20260910T1552_add_status
3 operation(s)
├─ Add column "status" to "Post"
├─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
└─ Data transform: backfill-post-status{
"id": "data_migration.backfill-post-status",
"operationClass": "data",
"precheck": [{ "sql": "SELECT EXISTS (SELECT \"id\" AS \"id\" FROM \"public\".\"Post\" WHERE (\"published\" = $1 AND \"status\" != $2) LIMIT 1) AS ok", "params": [true, "Published"] }],
"execute": [{ "sql": "UPDATE \"public\".\"Post\" SET \"status\" = $1 WHERE (\"published\" = $2 AND \"status\" != $3)", "params": ["Published", true, "Published"] }],
"postcheck": [{ "sql": "SELECT NOT EXISTS (SELECT \"id\" AS \"id\" FROM \"public\".\"Post\" WHERE (\"published\" = $1 AND \"status\" != $2) LIMIT 1) AS ok", "params": [true, "Published"] }]
}Commit migration.ts, ops.json, and migration.json together. Recompiling stores a hash of ops.json and migration.json inside migration.json. If either file changes after that, db migrate refuses to run the migration, and npx prisma migration check reports the same problem before you commit.
Apply it to your development database. On PostgreSQL the whole run is one transaction, so the column, the constraint, and the backfill land together or not at all:
bunx prisma db migrate --advance-ref dbpnpm prisma db migrate --advance-ref dbyarn prisma db migrate --advance-ref dbnpx prisma db migrate --advance-ref db✔ Applied 1 migration(s) (3 operation(s)) across 1 contract space(s)
App space
├─ Add column "status" to "Post"
├─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
├─ Data transform: backfill-post-status
└─ marker 0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4
✔ Advanced ref "db" → 0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4Check the rows. The published posts moved to Published, and the draft kept the default:
import { db } from "./prisma/db.ts";
const posts = await db.orm.public.Post
.select("id", "title", "published", "status")
.orderBy((p) => p.id.asc())
.all();
console.log(posts);
await db.close();node src/posts.ts[
{ id: 1, title: 'Hello World', published: true, status: 'Published' },
{ id: 2, title: 'Draft ideas', published: false, status: 'Draft' },
{ id: 3, title: 'Release notes', published: true, status: 'Published' }
]Both columns exist now, so this is the moment to move application code from published to status. The new field is typed as 'Draft' | 'InProgress' | 'InReview' | 'Published' in the emitted contract:
const published = await db.orm.public.Post
.select("id", "title", "status")
.where((p) => p.status.eq("Published"))
.orderBy((p) => p.id.asc())
.all();[
{ id: 1, title: 'Hello World', status: 'Published' },
{ id: 3, title: 'Release notes', status: 'Published' }
]Merge the branch and deploy it. Wait until nothing reads or writes published before you continue.
✔ Applied 1 migration(s) (3 operation(s)) across 1 contract space(s)
App space
├─ Add column "status" to "Post"
├─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
├─ Data transform: backfill-post-status
└─ marker 0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4
✔ Advanced ref "db" → 0e9486dcd7f4216d673cbe0ec503edb638520dec9b0a8a22f48bb88edd1442c4Check the rows. The published posts moved to Published, and the draft kept the default:
import { db } from "./prisma/db.ts";
const posts = await db.orm.public.Post
.select("id", "title", "published", "status")
.orderBy((p) => p.id.asc())
.all();
console.log(posts);
await db.close();node src/posts.ts[
{ id: 1, title: 'Hello World', published: true, status: 'Published' },
{ id: 2, title: 'Draft ideas', published: false, status: 'Draft' },
{ id: 3, title: 'Release notes', published: true, status: 'Published' }
]Both columns exist now, so this is the moment to move application code from published to status. The new field is typed as 'Draft' | 'InProgress' | 'InReview' | 'Published' in the emitted contract:
const published = await db.orm.public.Post
.select("id", "title", "status")
.where((p) => p.status.eq("Published"))
.orderBy((p) => p.id.asc())
.all();[
{ id: 1, title: 'Hello World', status: 'Published' },
{ id: 3, title: 'Release notes', status: 'Published' }
]Merge the branch and deploy it. Wait until nothing reads or writes published before you continue.
Create a branch for the cleanup:
git checkout -b drop-published-columnRemove the old field:
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
status Status @default(Draft)
}Emit and plan. The planner flags the drop as destructive, which is the review signal for this step:
bunx prisma contract emit
bunx prisma migration plan --name drop_publishedpnpm prisma contract emit
pnpm prisma migration plan --name drop_publishedyarn prisma contract emit
yarn prisma migration plan --name drop_publishednpx prisma contract emit
npx prisma migration plan --name drop_published✔ Planned 1 operation(s)
migrations/app/20260910T1554_drop_published
└─ ⚠ Drop column "published" from "Post"
⚠ This migration contains destructive operations that may cause data loss.
ℹ DDL preview
ALTER TABLE "public"."Post" DROP COLUMN "published";Nothing to edit this time. Apply it:
✔ Planned 1 operation(s)
migrations/app/20260910T1554_drop_published
└─ ⚠ Drop column "published" from "Post"
⚠ This migration contains destructive operations that may cause data loss.
ℹ DDL preview
ALTER TABLE "public"."Post" DROP COLUMN "published";Nothing to edit this time. Apply it:
bunx prisma db migrate --advance-ref dbpnpm prisma db migrate --advance-ref dbyarn prisma db migrate --advance-ref dbnpx prisma db migrate --advance-ref db✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)
App space
├─ ⚠ Drop column "published" from "Post"
└─ marker 79567c619bcb9790bf476efc358730a10317c94fc352e908e19424fc6441350aThe data survived the round trip. Drop published from the select in src/posts.ts and run it again:
[
{ id: 1, title: 'Hello World', status: 'Published' },
{ id: 2, title: 'Draft ideas', status: 'Draft' },
{ id: 3, title: 'Release notes', status: 'Published' }
]migration log reads the database's own ledger of what ran:
✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)
App space
├─ ⚠ Drop column "published" from "Post"
└─ marker 79567c619bcb9790bf476efc358730a10317c94fc352e908e19424fc6441350aThe data survived the round trip. Drop published from the select in src/posts.ts and run it again:
[
{ id: 1, title: 'Hello World', status: 'Published' },
{ id: 2, title: 'Draft ideas', status: 'Draft' },
{ id: 3, title: 'Release notes', status: 'Published' }
]migration log reads the database's own ledger of what ran:
bunx prisma migration logpnpm prisma migration logyarn prisma migration lognpx prisma migration logApplied at Migration Change Ops
2026-09-10 17:51:19 +02:00 20260910T1551_init ∅ → e8804d8 2 ops
2026-09-10 17:53:46 +02:00 20260910T1552_add_status e8804d8 → 0e9486d 3 ops
2026-09-10 17:55:39 +02:00 20260910T1554_drop_published 0e9486d → 79567c6 1 opsApplied at Migration Change Ops
2026-09-10 17:51:19 +02:00 20260910T1551_init ∅ → e8804d8 2 ops
2026-09-10 17:53:46 +02:00 20260910T1552_add_status e8804d8 → 0e9486d 3 ops
2026-09-10 17:55:39 +02:00 20260910T1554_drop_published 0e9486d → 79567c6 1 opsProduction runs the same migrations from the same files; only the connection string changes. db migrate reads the database's marker, finds the path through the migration graph, and applies whatever is pending, backfill included. Your deploy pipeline needs one command:
bunx prisma db migrate --db "$PRODUCTION_DATABASE_URL"pnpm prisma db migrate --db "$PRODUCTION_DATABASE_URL"yarn prisma db migrate --db "$PRODUCTION_DATABASE_URL"npx prisma db migrate --db "$PRODUCTION_DATABASE_URL"It refuses to run a migration whose ops.json or migration.json changed after it was compiled. If your pipeline deploys each environment to a ref, as Applying a migration shows, add --to <ref> to that command. To see the route before it runs, add --show, which previews it without changing anything. Here is that preview against a production database that shipped the init migration and holds live rows, followed by the apply:
ℹ The following 2 migrations will run:
20260910T1552_add_status e8804d8 → 0e9486d
20260910T1554_drop_published 0e9486d → 79567c6✔ Applied 2 migration(s) (4 operation(s)) across 1 contract space(s)
App space
├─ Add column "status" to "Post"
├─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
├─ Data transform: backfill-post-status
├─ ⚠ Drop column "published" from "Post"
└─ marker 79567c619bcb9790bf476efc358730a10317c94fc352e908e19424fc6441350aIn practice the two migrations reach production in separate deploys, one per branch, and the app moves from published to status between them. The commands are the same either way. Production never runs your TypeScript: the runner executes ops.json, which is plain data, so no application code runs with production credentials.
It refuses to run a migration whose ops.json or migration.json changed after it was compiled. If your pipeline deploys each environment to a ref, as Applying a migration shows, add --to <ref> to that command. To see the route before it runs, add --show, which previews it without changing anything. Here is that preview against a production database that shipped the init migration and holds live rows, followed by the apply:
ℹ The following 2 migrations will run:
20260910T1552_add_status e8804d8 → 0e9486d
20260910T1554_drop_published 0e9486d → 79567c6✔ Applied 2 migration(s) (4 operation(s)) across 1 contract space(s)
App space
├─ Add column "status" to "Post"
├─ Add check constraint "Post_status_check_eaa35e2a" on "Post"
├─ Data transform: backfill-post-status
├─ ⚠ Drop column "published" from "Post"
└─ marker 79567c619bcb9790bf476efc358730a10317c94fc352e908e19424fc6441350aIn practice the two migrations reach production in separate deploys, one per branch, and the app moves from published to status between them. The commands are the same either way. Production never runs your TypeScript: the runner executes ops.json, which is plain data, so no application code runs with production credentials.
Run npx prisma@latest init once to install the Prisma ORM skills for your coding agent and keep them matching your installed packages. Prompts that map to this guide:
- "Using the prisma-8 skill, add a
dataTransformto the latest migration that backfillsstatusfrompublished, recompile it, and show me the compiled SQL in ops.json." - "Plan a migration that drops the
publishedcolumn and explain the destructive warning." - "Preview what
db migratewould run against staging and summarize the operations."
- Editing a migration: placeholders,
rawSql, and the recompile loop in depth. - Applying a migration: targets, refs, and what happens when a run fails.
- Rollbacks and recovery: a rollback is one more planned migration, and a failed run is safe to retry.
- The migration graph: why two branches with migrations merge cleanly.