Plan and write relational schema changes that deploy without downtime or data loss in any back-end stack — expand/contract sequencing, lock-safe DDL per engine, backfills over large tables, online index builds, migration tooling (EF Core, Prisma, Drizzle, Flyway, Liquibase, Alembic), and what rollback actually means once data has moved.
When agents use itUse this whenever the user writes or changes a migration, adds or drops a column, renames anything, adds a NOT NULL or a constraint to an existing table, adds an index to a large table, backfills or rewrites data, sees a migration lock up production or time out, plans the deploy order of schema and code, or asks whether a schema change is safe to ship. This skill owns migration safety; the stack skills own the ORM idioms.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill database-migrations -a github-copilot
# or with the org installer (adds .github/skills/database-migrations):
npx -y github:AGCO-Global/org-skills add skill database-migrations
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the backend-shared-skills plugin, which bundles all Backend / Shared skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-shared-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill database-migrations -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill database-migrations -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill database-migrations -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill database-migrations -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Try it — example prompts
Prompts this skill is tested against, and what a good answer includes.
I need to rename users.name to full_name. Write the migration.
refuses a single in-place renameexpand/contract sequence: add nullable column, dual-write, backfill, switch reads, stop writing old, drop latereach phase is a separate deployold and new code run simultaneously during a rolling deploythe drop happens in a later release
Adding an index to our 80-million-row orders table locked up production last time. What should we do?
CREATE INDEX CONCURRENTLY on PostgreSQL / ONLINE = ON on SQL ServerCONCURRENTLY cannot run inside a transaction, so the migration tool must not wrap itcheck for an INVALID index afterwards and drop/rebuild if foundset lock_timeout so the DDL fails fast instead of queueing every query
Our migration adds a NOT NULL column with a default to a large table. Is that safe?
a constant default is metadata-only in current PostgreSQL; a volatile or computed default rewrites the tableNOT NULL with no default requires add-nullable, backfill, then constrainadd the constraint NOT VALID then VALIDATE CONSTRAINT separatelytest against production-like volume
We need to backfill a new column for 200 million rows. How?
batched by primary key range with a commit per batchresumable: record progress so a restart does not start overidempotent, e.g. WHERE full_name IS NULLthrottled, watching replication lagrun outside the migration, not inside its transaction
Should our service run EF Core migrations automatically at startup?
no — multiple instances race and a failed migration takes the app downapply as one explicit pipeline step before the new version startsdotnet ef migrations bundle for the deployment artifactEnsureCreated is not migrations
Write a down-migration so we can roll back this column drop.
a down-migration cannot restore dropped dataplan a forward fix insteada tested restore path and stated recovery point objective for destructive stepsdelay the drop until the old code is gone everywhere
During a rolling deploy, old code and new code run against the same database at the same time. That single fact drives every rule here: a schema change is safe only if the version of the application that is still running can survive it. The database is also the one part of the system with no undo — code rolls back, deleted columns do not. Sequence every change so that each step is compatible with the code on both sides of it, and treat a migration that takes a lock on a large table as a production incident waiting for enough rows.
1. Decide first
Question
Default
Change when
Schema and code in one deploy
No — expand, deploy, migrate, contract across separate deploys
Only for a brand-new table nothing reads yet
Renaming a column or table
Never in place: add new, dual-write, backfill, switch reads, drop later
A table created in the same release and never deployed
Adding a column
Nullable, or with a default the engine applies without a rewrite
Adding NOT NULL with no default requires the expand/contract dance
Adding an index to a big table
CREATE INDEX CONCURRENTLY (Postgres) / ONLINE = ON (SQL Server, edition permitting)
Small table, or a maintenance window you actually have
Backfill strategy
Batched, throttled, resumable, outside the migration transaction
A single UPDATE only when the table is genuinely small
Destructive step (drop column/table)
A later release, after the old code is gone everywhere
Never in the same deploy as the code that stopped using it
Migration runner
The stack's tool, applied as an explicit step before the app starts
Automatic migrate-on-startup is unsafe with multiple instances
Rollback plan
Forward fix, plus a tested restore path for data loss
A down-migration only where it is genuinely lossless
2. Expand / contract
The only sequence that survives a rolling deploy. Renaming users.name to full_name:
Expand — add full_name, nullable. Deploy. Old code ignores it.
Dual-write — deploy code that writes both columns and reads name.
Backfill — copy name into full_name in batches for existing rows.
Switch reads — deploy code that reads full_name and still writes both.
Stop writing the old column — deploy.
Contract — drop name, in a later release, once every instance runs the new code.
Steps 2–5 are separate deploys. Collapsing them is what turns a rename into an outage: between the migration and the last instance restarting, the old code is selecting a column that no longer exists.
The same shape covers type changes, splitting a column and moving data between tables. Per-change recipes: references/change-recipes.md.
3. Lock-safe DDL
What a statement locks, and for how long, decides whether it is safe:
Postgres — ADD COLUMN with a non-volatile default is metadata-only in current versions; ALTER TYPE, adding NOT NULL and most constraint additions rewrite or scan the table. Build indexes with CREATE INDEX CONCURRENTLY (outside a transaction, and check for an INVALID index afterwards). Add constraints as NOT VALID, then VALIDATE CONSTRAINT separately.
SQL Server — ONLINE = ON for index operations where the edition supports it; ALTER COLUMN is generally a size-of-data operation; WITH (ONLINE = ON, RESUMABLE = ON) helps on long index builds.
MySQL/InnoDB — most ALTER TABLE supports ALGORITHM=INPLACE, LOCK=NONE, but not all; state the algorithm explicitly so it fails fast rather than silently copying the table.
Always set a lock timeout (and a statement timeout) in the migration session. Without one, a DDL statement blocked behind a long-running query queues every subsequent query on that table — the classic "one migration took the whole site down". With one, it fails and you retry.
Engine-by-engine detail and the safe form of each operation: references/lock-safe-ddl.md.
4. Backfills
Batch by primary key range, commit per batch, and make it resumable — record progress so a restart does not start over.
Throttle: sleep between batches, and watch replication lag rather than only CPU. A backfill that outruns replication breaks read replicas.
Run it outside the migration. A migration that holds a transaction open for an hour blocks DDL, bloats WAL and cannot be interrupted safely.
Make it idempotent (WHERE full_name IS NULL), so re-running is free.
For very large tables, a backfill is a job with monitoring, not a script someone runs in a terminal.
5. Tooling
Tool
Apply step
Watch out for
EF Core
dotnet ef migrations bundle → run the bundle in the pipeline
EnsureCreated is not migrations; auto-migrate on startup races across instances
Prisma
prisma migrate deploy in the pipeline
migrate dev is for development only; it can reset the database
Drizzle
drizzle-kit generate then apply the SQL
Generated SQL is a starting point — read it before shipping
Flyway / Liquibase
Versioned scripts applied by the pipeline
Never edit an applied migration; add a new one
Alembic
alembic upgrade head
Autogenerate misses data changes and some constraint edits
Whatever the tool: migrations are applied as an explicit deployment step, by one runner, before the new code starts — not by every application instance on boot.
6. Rollback
Rolling back code is easy; rolling back a schema is mostly a fiction. Once a column is dropped or data is transformed, the previous state exists only in a backup.
Plan forward fixes. Keep each step small enough that rolling forward is quick.
Write a down-migration only where it is genuinely lossless (dropping a column you just added).
Before any destructive step, confirm the restore path: a tested backup, and a stated recovery point objective.
The contract step is the dangerous one, and it is also the one under least time pressure — leave it until you are sure.
Deliverable format
## Change — what is changing and why
## Sequence — numbered steps, each marked as migration or deploy, in order
## DDL — the statement for each step, with lock and timeout settings
## Backfill — batching, throttling, resumability, and how progress is observed
## Compatibility — what the currently-running code does at each step
## Rollback — forward fix per step; restore path for anything destructive
## Verification — what to check after each step, including on a replica
Checklist
Old and new code can both run against the schema at every step.
Nothing is renamed or dropped in the same deploy as the code change.
Every destructive step is in a later release than the code that stopped using it.
Index creation on large tables is concurrent/online.
Constraints are added unvalidated, then validated separately, where the engine allows it.
A lock timeout and statement timeout are set in the migration session.
Backfills are batched, throttled, resumable and idempotent, and run outside the migration.
Replication lag is watched during the backfill.
Migrations are applied by one explicit step, not by application startup.
The rollback story is written down, and the restore path for destructive steps is tested.
The migration was run against a realistic data volume, not an empty dev database.
Anti-patterns
Anti-pattern
Why it hurts
Fix
Rename column + change code in one deploy
Old instances query a column that no longer exists
Expand/contract across separate deploys
ADD COLUMN NOT NULL with no default on a big table
Table rewrite under an exclusive lock
Add nullable, backfill, then set NOT NULL (validate separately)
CREATE INDEX without CONCURRENTLY on a hot table
Blocks writes for the duration of the build
CONCURRENTLY / ONLINE = ON, then verify the index is valid
Backfill as one UPDATE inside the migration
Long transaction, lock escalation, no way to stop it safely
Batched, resumable job outside the migration
Migrate on application startup
Several instances race; a failed migration takes the app down with it
One explicit pipeline step before the new version starts
No lock timeout
One long query turns a fast DDL into a site-wide stall
lock_timeout/SET LOCK_TIMEOUT in the migration session
Editing an already-applied migration
Environments diverge silently; checksums break
Add a new migration
Trusting a down-migration for a destructive change
The data is already gone
Forward fix, plus a tested restore path
Testing only on an empty dev database
Everything is fast on zero rows
Rehearse on production-like volume
Go deeper
references/change-recipes.md — step-by-step sequences for rename, type change, NOT NULL, split/merge column, moving data between tables, and adding a foreign key.
references/lock-safe-ddl.md — per-engine lock behaviour, the safe form of each operation, timeout settings and how to verify an online index build.
Sibling skills: messaging-patterns when a change must be published as an event; backend-code-review for reviewing someone else's migration; service-operability for the monitoring a long backfill needs.
---
name: database-migrations
description: >
Plan and write relational schema changes that deploy without downtime or data loss in any back-end stack — expand/contract sequencing, lock-safe DDL per engine, backfills over large tables, online index builds, migration tooling (EF Core, Prisma, Drizzle, Flyway, Liquibase, Alembic), and what rollback actually means once data has moved. Use this whenever the user writes or changes a migration, adds or drops a column, renames anything, adds a NOT NULL or a constraint to an existing table, adds an index to a large table, backfills or rewrites data, sees a migration lock up production or time out, plans the deploy order of schema and code, or asks whether a schema change is safe to ship. This skill owns migration safety; the stack skills own the ORM idioms.
metadata:
technology: Backend (general)
type: development
---
# Database Migrations
> **Targets:** PostgreSQL, SQL Server, MySQL · EF Core 10, Prisma 7, Drizzle, Flyway, Liquibase, Alembic · **Verified:** 2026-09 against https://www.postgresql.org/docs/current/sql-altertable.html and https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/
During a rolling deploy, old code and new code run against the same database at the same time. That single fact drives every rule here: a schema change is safe only if the version of the application that is still running can survive it. The database is also the one part of the system with no undo — code rolls back, deleted columns do not. Sequence every change so that each step is compatible with the code on both sides of it, and treat a migration that takes a lock on a large table as a production incident waiting for enough rows.
## 1. Decide first
| Question | Default | Change when |
|---|---|---|
| Schema and code in one deploy | No — expand, deploy, migrate, contract across separate deploys | Only for a brand-new table nothing reads yet |
| Renaming a column or table | Never in place: add new, dual-write, backfill, switch reads, drop later | A table created in the same release and never deployed |
| Adding a column | Nullable, or with a default the engine applies without a rewrite | Adding `NOT NULL` with no default requires the expand/contract dance |
| Adding an index to a big table | `CREATE INDEX CONCURRENTLY` (Postgres) / `ONLINE = ON` (SQL Server, edition permitting) | Small table, or a maintenance window you actually have |
| Backfill strategy | Batched, throttled, resumable, outside the migration transaction | A single `UPDATE` only when the table is genuinely small |
| Destructive step (drop column/table) | A later release, after the old code is gone everywhere | Never in the same deploy as the code that stopped using it |
| Migration runner | The stack's tool, applied as an explicit step before the app starts | Automatic migrate-on-startup is unsafe with multiple instances |
| Rollback plan | Forward fix, plus a tested restore path for data loss | A down-migration only where it is genuinely lossless |
## 2. Expand / contract
The only sequence that survives a rolling deploy. Renaming `users.name` to `full_name`:
1. **Expand** — add `full_name`, nullable. Deploy. Old code ignores it.
2. **Dual-write** — deploy code that writes both columns and reads `name`.
3. **Backfill** — copy `name` into `full_name` in batches for existing rows.
4. **Switch reads** — deploy code that reads `full_name` and still writes both.
5. **Stop writing the old column** — deploy.
6. **Contract** — drop `name`, in a later release, once every instance runs the new code.
Steps 2–5 are separate deploys. Collapsing them is what turns a rename into an outage: between the migration and the last instance restarting, the old code is selecting a column that no longer exists.
The same shape covers type changes, splitting a column and moving data between tables. Per-change recipes: `references/change-recipes.md`.
## 3. Lock-safe DDL
What a statement locks, and for how long, decides whether it is safe:
- **Postgres** — `ADD COLUMN` with a non-volatile default is metadata-only in current versions; `ALTER TYPE`, adding `NOT NULL` and most constraint additions rewrite or scan the table. Build indexes with `CREATE INDEX CONCURRENTLY` (outside a transaction, and check for an `INVALID` index afterwards). Add constraints as `NOT VALID`, then `VALIDATE CONSTRAINT` separately.
- **SQL Server** — `ONLINE = ON` for index operations where the edition supports it; `ALTER COLUMN` is generally a size-of-data operation; `WITH (ONLINE = ON, RESUMABLE = ON)` helps on long index builds.
- **MySQL/InnoDB** — most `ALTER TABLE` supports `ALGORITHM=INPLACE, LOCK=NONE`, but not all; state the algorithm explicitly so it fails fast rather than silently copying the table.
Always set a **lock timeout** (and a statement timeout) in the migration session. Without one, a DDL statement blocked behind a long-running query queues every subsequent query on that table — the classic "one migration took the whole site down". With one, it fails and you retry.
Engine-by-engine detail and the safe form of each operation: `references/lock-safe-ddl.md`.
## 4. Backfills
- Batch by primary key range, commit per batch, and make it **resumable** — record progress so a restart does not start over.
- Throttle: sleep between batches, and watch replication lag rather than only CPU. A backfill that outruns replication breaks read replicas.
- Run it **outside** the migration. A migration that holds a transaction open for an hour blocks DDL, bloats WAL and cannot be interrupted safely.
- Make it idempotent (`WHERE full_name IS NULL`), so re-running is free.
- For very large tables, a backfill is a job with monitoring, not a script someone runs in a terminal.
## 5. Tooling
| Tool | Apply step | Watch out for |
|---|---|---|
| EF Core | `dotnet ef migrations bundle` → run the bundle in the pipeline | `EnsureCreated` is not migrations; auto-migrate on startup races across instances |
| Prisma | `prisma migrate deploy` in the pipeline | `migrate dev` is for development only; it can reset the database |
| Drizzle | `drizzle-kit generate` then apply the SQL | Generated SQL is a starting point — read it before shipping |
| Flyway / Liquibase | Versioned scripts applied by the pipeline | Never edit an applied migration; add a new one |
| Alembic | `alembic upgrade head` | Autogenerate misses data changes and some constraint edits |
Whatever the tool: migrations are applied as an explicit deployment step, by one runner, before the new code starts — not by every application instance on boot.
## 6. Rollback
Rolling back code is easy; rolling back a schema is mostly a fiction. Once a column is dropped or data is transformed, the previous state exists only in a backup.
- Plan **forward fixes**. Keep each step small enough that rolling *forward* is quick.
- Write a down-migration only where it is genuinely lossless (dropping a column you just added).
- Before any destructive step, confirm the restore path: a tested backup, and a stated recovery point objective.
- The contract step is the dangerous one, and it is also the one under least time pressure — leave it until you are sure.
## Deliverable format
```
## Change — what is changing and why
## Sequence — numbered steps, each marked as migration or deploy, in order
## DDL — the statement for each step, with lock and timeout settings
## Backfill — batching, throttling, resumability, and how progress is observed
## Compatibility — what the currently-running code does at each step
## Rollback — forward fix per step; restore path for anything destructive
## Verification — what to check after each step, including on a replica
```
## Checklist
- [ ] Old and new code can both run against the schema at every step.
- [ ] Nothing is renamed or dropped in the same deploy as the code change.
- [ ] Every destructive step is in a later release than the code that stopped using it.
- [ ] Index creation on large tables is concurrent/online.
- [ ] Constraints are added unvalidated, then validated separately, where the engine allows it.
- [ ] A lock timeout and statement timeout are set in the migration session.
- [ ] Backfills are batched, throttled, resumable and idempotent, and run outside the migration.
- [ ] Replication lag is watched during the backfill.
- [ ] Migrations are applied by one explicit step, not by application startup.
- [ ] The rollback story is written down, and the restore path for destructive steps is tested.
- [ ] The migration was run against a realistic data volume, not an empty dev database.
## Anti-patterns
| Anti-pattern | Why it hurts | Fix |
|---|---|---|
| Rename column + change code in one deploy | Old instances query a column that no longer exists | Expand/contract across separate deploys |
| `ADD COLUMN NOT NULL` with no default on a big table | Table rewrite under an exclusive lock | Add nullable, backfill, then set `NOT NULL` (validate separately) |
| `CREATE INDEX` without `CONCURRENTLY` on a hot table | Blocks writes for the duration of the build | `CONCURRENTLY` / `ONLINE = ON`, then verify the index is valid |
| Backfill as one `UPDATE` inside the migration | Long transaction, lock escalation, no way to stop it safely | Batched, resumable job outside the migration |
| Migrate on application startup | Several instances race; a failed migration takes the app down with it | One explicit pipeline step before the new version starts |
| No lock timeout | One long query turns a fast DDL into a site-wide stall | `lock_timeout`/`SET LOCK_TIMEOUT` in the migration session |
| Editing an already-applied migration | Environments diverge silently; checksums break | Add a new migration |
| Trusting a down-migration for a destructive change | The data is already gone | Forward fix, plus a tested restore path |
| Testing only on an empty dev database | Everything is fast on zero rows | Rehearse on production-like volume |
## Go deeper
- `references/change-recipes.md` — step-by-step sequences for rename, type change, NOT NULL, split/merge column, moving data between tables, and adding a foreign key.
- `references/lock-safe-ddl.md` — per-engine lock behaviour, the safe form of each operation, timeout settings and how to verify an online index build.
- Sibling skills: `messaging-patterns` when a change must be published as an event; `backend-code-review` for reviewing someone else's migration; `service-operability` for the monitoring a long backfill needs.