# Editing a migration (/docs/orm/migrations/editing-a-migration)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

A migration is TypeScript you own. Fill in backfills, reorder steps, or drop to raw SQL, then recompile it with one command.

Location: ORM > Migrations > Editing a migration

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 8 you make those changes by editing `migration.ts`, ordinary TypeScript with autocomplete and type checking, and recompiling it. You never hand-edit SQL files or `ops.json`.

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

  

#### bun

```bash
bunx prisma@latest migration plan --name add_display_name
```

#### pnpm

```bash
pnpm dlx prisma@latest migration plan --name add_display_name
```

#### yarn

```bash
yarn dlx prisma@latest migration plan --name add_display_name
```

#### npm

```bash
npx prisma@latest 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 startContractJson from './start-contract.json' with { type: 'json' };
import { Migration, MigrationCLI, col } from '@prisma/orm-postgres/migration';
import postgresAdapter from '@prisma/orm-postgres/adapter/runtime';
import { sql } from '@prisma/orm-postgres/builder/runtime';
import { createExecutionContext, createSqlExecutionStack } from '@prisma/orm-postgres/family-runtime';
import postgresTarget, { PostgresContractSerializer } from '@prisma/orm-postgres/target/runtime';

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

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

export default class M extends Migration<Start, End> {
  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 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, and is never spliced into SQL text. The reviewer reading your PR sees intent in `migration.ts` and the exact statements in `ops.json`, side by side.

## Escape hatch: raw SQL [#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 [#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/orm-mongo/target/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 8 repo's [retail-store example](https://github.com/prisma/prisma/blob/main/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts).

## Starting from a blank migration [#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):

  

#### bun

```bash
bunx prisma@latest migration new --name backfill_scores
```

#### pnpm

```bash
pnpm dlx prisma@latest migration new --name backfill_scores
```

#### yarn

```bash
yarn dlx prisma@latest migration new --name backfill_scores
```

#### npm

```bash
npx prisma@latest migration new --name backfill_scores
```

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

## Editing checklist [#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@latest 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 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 [#prompt-your-coding-agent]

Projects scaffolded with `create-prisma@latest` install [Prisma 8 skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) 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 [#see-also]

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

## Related pages

- [`Applying a migration`](https://www.prisma.io/docs/orm/migrations/applying-a-migration): The db migrate command walks the graph from where your database is to where you want it, with a preview, a checkpoint at every step, and safe retries.
- [`Generating a migration`](https://www.prisma.io/docs/orm/migrations/generating-a-migration): Turn a contract change into a reviewable migration with the migration plan command.
- [`How migrations work`](https://www.prisma.io/docs/orm/migrations/how-migrations-work): Change your contract, plan a migration, review it, apply it. Every step is checked before and after it runs.
- [`Rollbacks and recovery`](https://www.prisma.io/docs/orm/migrations/rollbacks-and-recovery): Rolling back is planning one more migration to a state you've already been in. Recovery is fixing the cause and re-running, safely.
- [`The migration graph`](https://www.prisma.io/docs/orm/migrations/the-migration-graph): You changed your schema, a teammate changed theirs, both merged. The migration graph is what lets every database catch up without renaming files or rebuilding history.