{"post":{"slug":"how-to-write-a-migration-script-2026","title":"How to write a migration script in 2026","excerpt":"Migration scripts move data safely between schemas or systems without downtime. This guide covers patterns, tooling, and rollback strategies for 2026.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">M</span>igration scripts are how your database schema evolves without losing data or taking down the app. In 2026, every serious project uses a migration tool — the question is which one and how to structure the migrations. Done right, migrations are boring and invisible; done wrong, they cause production incidents, data loss, or hours of downtime.\n\n## What changed in 2026\n\n- **Drizzle Kit** matured into a first-class migrations tool for TypeScript projects using the Drizzle ORM.\n- **Prisma Migrate** added `--create-only` mode by default in most workflows, making it easier to review generated SQL before applying.\n- **Zero-downtime patterns are standard** — background column additions, dual-write patterns, and blue-green deploys are well-documented and expected.\n- **TypeScript migration scripts** are common — tools like `tsx` make it easy to write migrations in TS alongside your application code.\n\n## Migration tools compared\n\n| Tool | Best for | Language | Approach |\n|---|---|---|---|\n| Prisma Migrate | Prisma ORM users | TypeScript | Schema-first, auto-generates SQL |\n| Drizzle Kit | Drizzle ORM users | TypeScript | Schema-first, generates SQL |\n| node-pg-migrate | Custom Postgres, full SQL control | JS/TS | SQL or JS `up`/`down` functions |\n| Flyway | Java, enterprise, polyglot | SQL | SQL versioned files |\n| Liquibase | Enterprise, changesets | SQL/XML/YAML | Changeset tracking |\n| golang-migrate | Go projects | SQL | Versioned SQL files |\n\n## Basic structure: `up` and `down`\n\n```ts\n// migrations/0012_add_user_role.ts\nimport type { Kysely } from 'kysely';\n\nexport async function up(db: Kysely<unknown>) {\n  await db.schema\n    .alterTable('users')\n    .addColumn('role', 'varchar(50)', (col) => col.notNull().defaultTo('member'))\n    .execute();\n}\n\nexport async function down(db: Kysely<unknown>) {\n  await db.schema\n    .alterTable('users')\n    .dropColumn('role')\n    .execute();\n}\n```\n\nAlways write the `down` function even if you never plan to use it — it forces you to think through the reversal.\n\n## Prisma Migrate workflow\n\n```bash\n# Generate a migration from schema changes\nnpx prisma migrate dev --name add_user_role\n\n# Apply in production (no schema drift, no interactive prompts)\nnpx prisma migrate deploy\n```\n\nPrisma generates a SQL file in `prisma/migrations/`. Review it before applying in production — never blindly run auto-generated DDL on prod.\n\n## Zero-downtime column addition (Postgres)\n\nAdding a non-null column with no default requires a full table rewrite in older Postgres. In Postgres 11+, adding a column with a `DEFAULT` is instant (stored in catalog, not rewritten):\n\n```sql\n-- Fast in Postgres 11+ (no table rewrite)\nALTER TABLE orders ADD COLUMN status varchar(50) NOT NULL DEFAULT 'pending';\n\n-- If you need to backfill existing rows differently, do it in a second migration:\nUPDATE orders SET status = 'legacy' WHERE created_at < '2024-01-01';\n```\n\nFor very large tables (> 10M rows), run the `UPDATE` in batches:\n\n```sql\nDO $$\nDECLARE batch_size INT := 10000;\nDECLARE last_id BIGINT := 0;\nBEGIN\n  LOOP\n    UPDATE orders\n    SET status = 'legacy'\n    WHERE id > last_id AND id <= last_id + batch_size AND created_at < '2024-01-01';\n    EXIT WHEN NOT FOUND;\n    last_id := last_id + batch_size;\n    PERFORM pg_sleep(0.05); -- yield briefly between batches\n  END LOOP;\nEND $$;\n```\n\n## Rename a column safely (3-step deploy)\n\nRenaming a column atomically breaks running application code. The safe pattern:\n\n1. **Add** the new column; update app to write to both old and new.\n2. **Backfill** the new column from the old.\n3. **Drop** the old column once all deploys no longer read it.\n\nEach step is a separate migration and deploy.\n\n## How to start\n\n1. Choose a migration tool that matches your ORM/stack.\n2. Write `up` and `down` for every migration, commit them to version control.\n3. Test against a production-size database clone before running in production.\n4. Apply in a transaction where possible — DDL is transactional in Postgres.\n5. Lock down who can run `migrate deploy` in production (CI/CD only, not laptops).\n\n## Common mistakes\n\n**Editing an applied migration.** Once a migration is applied somewhere (even dev), treat it as immutable. Change it by writing a new migration.\n\n**Running data migrations inline with schema migrations.** Large backfills hold an exclusive lock or run for minutes. Separate them: DDL first, data backfill as a background job or separate migration with batching.\n\n**Not testing rollback.** \"I have a `down` function\" is not the same as \"I have tested it.\" Run the rollback in staging before you need it in production.\n\n**Ignoring lock wait timeouts.** In Postgres, `ALTER TABLE` takes an `ACCESS EXCLUSIVE` lock. On a live table with active queries, set `lock_timeout = '5s'` to fail fast rather than hang.\n\n## What to skip\n\n- **Direct production DDL from a developer laptop** — run migrations from CI/CD only; human-run migrations are a reliability risk.\n- **`DROP TABLE` or `DROP COLUMN` without a retention period** — keep old columns alive for at least one deploy cycle in case you need to roll back.\n- **One giant migration file** — split unrelated changes into separate files; smaller migrations are easier to reason about and roll back.\n\n## FAQ\n\n**Should migrations run automatically on app start?**\nFor small projects: acceptable. For production: no — run migrations as a separate CI/CD step before the new app version boots, so you can catch failures early.\n\n**How do I handle migrations across multiple microservices sharing a DB?**\nEach service should own and apply migrations only for its own tables. Shared tables need a coordination process — usually a designated service or a separate migration repo.\n\n**What is a squash migration?**\nCollapsing many old migrations into one to reduce startup time. Do this only on tables where all envs are in sync; never squash unapplied migrations.\n\n**How do I migrate data between two incompatible schemas?**\nWrite a data migration script with dual-write: write to old and new schema simultaneously, backfill, verify counts, then cut over reads. See [how to seed test data in 2026](/blog/how-to-seed-test-data-2026) for patterns on populating a migrated schema.\n\n## Where to go next\n\n- [How to write a database migration in 2026](/blog/how-to-write-a-database-migration-2026)\n- [How to seed test data in 2026](/blog/how-to-seed-test-data-2026)\n- [How to set up Supabase in 2026](/blog/how-to-set-up-supabase-2026)","hue":313,"metaTitle":"How to Write a Migration Script in 2026","metaDescription":"Write safe database migration scripts in 2026. Covers schema vs data migrations, Prisma Migrate, Drizzle Kit, rollback strategies, and zero-downtime patterns.","keywords":["how to write a migration script 2026","database migration script","prisma migrate 2026","drizzle kit migrations","zero downtime migration","schema migration vs data migration","node-pg-migrate","database rollback strategy","sql migration best practices 2026","how to write a database migration"],"tldr":["**Use a migrations tool, not ad-hoc SQL** — Flyway, Liquibase, Prisma Migrate, Drizzle Kit, and `node-pg-migrate` all track what has run so you never double-apply.","**Separate schema migrations from data migrations** — DDL changes land first; data backfills run separately, often as background jobs on large tables.","**Every migration must be reversible or you need a tested rollback plan** — \"it cannot be reversed\" is an answer, but it must be intentional and documented.","**Test migrations against a production-size snapshot before running in prod** — a 30-second migration on 10k rows can take 20 minutes on 100M rows.","**Skip:** editing the `up` migration after it has been applied anywhere — create a new migration instead; rewriting history causes checksum mismatches and broken environments."],"readTime":"5 min read","wordCount":999},"related":[{"slug":"how-to-write-a-database-migration-2026","title":"How to write a database migration in 2026: safe, reversible, and zero-downtime","excerpt":"Database migrations fail when they lock tables in production or cannot be rolled back. This guide covers the 2026 patterns for safe, zero-downtime schema changes in Postgres.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305},{"slug":"blue-green-deployment-vs-rolling-update-2026","title":"Blue-Green Deployment vs Rolling Update in 2026","excerpt":"Blue-green deployment and rolling updates both aim to ship code without downtime, but they trade off cost, rollback speed, and blast radius very differently. Here is how to choose.","category":"code","date":"July 5, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":200},{"slug":"prisma-vs-drizzle-2026","title":"Prisma vs Drizzle in 2026: which TypeScript ORM should you use?","excerpt":"Drizzle challenged Prisma as the go-to TypeScript ORM with lighter runtime weight and SQL-first design. Here is the 2026 breakdown of which to pick.","category":"code","date":"April 7, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":315}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-work-with-apis-2026","title":"How to work with APIs in 2026: HTTP, auth, errors, and real patterns","excerpt":"Working with APIs is the most common programming task in 2026, yet most tutorials stop at the happy path. This guide covers auth, error handling, rate limits, and building reliable API clients.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":312},"older":{"slug":"how-to-write-a-python-script-2026","title":"How to write a Python script in 2026: structure, tooling, and best practices","excerpt":"Writing a Python script that works once is easy. Writing one that other people can run, debug, and maintain in 2026 requires knowing the right structure, tooling, and modern idioms.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":309}}