migration-preflight
v0.2.1
Published
Test database migrations before you ship them: plant a row, run the migration, verify it survives.
Maintainers
Readme
🚀 migration-preflight
The problem
A migration can look fine and still destroy data: dropping and recreating a table, renaming a column, changing a type. None of that shows up against an empty local database. The same migration can silently wipe real rows the moment it runs against a database that actually has data in it, and by the time you find out, it already ran.
The idea
Plant a row before the migration, run it, and check the row is still there and still correct after. That's the difference between "it didn't crash" and "my data is safe."
flowchart LR
M1[Migration 1] --> S["🌱 seed a row"] --> M2[Migration 2] --> M3[...] --> MN[Migration N]
MN --> Check{Row still there?<br/>Foreign keys still valid?}
Check -->|No| Bug[🔴 caught here, before it ships]
Check -->|Yes| Safe[✅ safe to ship]One test run replays your entire migration history and tells you exactly which step, if any, would have broken something.
Quick start
pnpm add -D migration-preflight @migration-preflight/sqliteimport { createNodeSqliteMigrationDatabase } from "@migration-preflight/sqlite"; // or -postgres
import { MigrationChain } from "migration-preflight";
import { drizzleFileSource } from "migration-preflight/sources"; // or prismaFileSource, sqlFileSource
const migrations = drizzleFileSource(join(import.meta.dirname, "out"));
const chain = new MigrationChain(createNodeSqliteMigrationDatabase(), migrations);
await chain.applyAll();The payoff, catching a migration that silently drops data, comes from seeding a row between two migrations. See Seeding between migrations below.
📦 Packages
This package is the dialect-agnostic core: the replay engine and the ports it runs against. No database driver, no ORM dependency.
| Package | What it's for |
| ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| migration-preflight | Core: replay engine, ports |
| @migration-preflight/sqlite | SQLite driver (node:sqlite) |
| @migration-preflight/postgres | Postgres driver (PGlite) |
migration-preflight isn't linked above: that's this package.
Seeding between migrations
A seed is a row tagged with the migration it goes in right after, a test fixture only, never
imported by production code. applyAll runs it in place after that migration, so you can assert the
row is still correct once every later migration has run:
import type { SqlStatement } from "migration-preflight";
import { renderInsert } from "migration-preflight";
const seedsByTag = new Map<string, readonly SqlStatement[]>([
[
"0000_create_users", // match your own first migration's tag
[{ sql: renderInsert("users", { id: "u1", email: "[email protected]" }), params: [] }],
],
]);
await chain.applyAll((migration) => seedsByTag.get(migration.tag) ?? []);
// Still there after every later migration ran? Then this migration history is safe.
expect(await chain.hasRow("users", "u1")).toBe(true);
expect(await chain.foreignKeyViolations()).toEqual([]);Seeding more rows is another seedsByTag entry, not a rewritten callback. See
How-to § Seed a row between migrations
for the full recipe, including a primary key column other than id.
Integrating with Drizzle, Prisma, plain SQL, and Postgres
migration-preflight itself only knows the MigrationSource, MigrationDatabase, and SqlDialect
ports. Concrete support comes from two other packages, plus three bundled sources:
drizzleFileSource,prismaFileSource,sqlFileSource(all frommigration-preflight/sources) each read a different migrations layout, Drizzle's journal, Prisma'smigrations/folder, or a flat folder of.sqlfiles, into theMigration[]this package replays.@migration-preflight/sqliteand@migration-preflight/postgreseach supply aMigrationDatabaseand, behind a/drizzlesubpath, a thin wrapper over Drizzle's own migrator for a quick "does it apply cleanly" check.
📚 Documentation
- 🚀 Tutorial: seed a row, run a migration, prove it survives.
- 🛠️ How-to guides: task recipes.
- 📖 Reference: API and package layout.
- 💡 Explanation: design rationale.
Contributing
pnpm --filter migration-preflight test # run the test suite
pnpm --filter migration-preflight typecheck # type-check
pnpm --filter migration-preflight lint # eslintNo database, no network, no setup, everything here runs in plain Node.
License
MIT
