Drizzle's SQL migrations change the shape of your tables. @spfn/migrate
handles what's in them — backfills, state transitions, complex calculations — written as
TypeScript, tracked in a ledger table, and applied at server startup.
Adding a column is a schema migration; filling it for every existing row
is not. @spfn/migrate gives that second kind of change the same discipline as
the first: each transformation is a named TypeScript migration —
defineDataMigration with an up({ db, log }) — registered in a
migrator and applied during server bootstrap. A data_migrations table, created
through your normal schema pipeline, records what has already run, so
apply() only ever executes what's pending.
By default each migration's up logic and its row in the
ledger commit together, in a single transaction. That gives the ledger a concrete property:
a migration that failed left no record, and a migration that is recorded ran to completion —
there is no half-applied-but-marked-done state. For huge tables you can set
transaction: false to avoid long-held locks, and the trade is stated plainly:
without the wrapping transaction, your up logic must be idempotent.
apply() runs pending migrations in alphabetical order, which is why names carry
a timestamp prefix (YYYYMMDD_slug) — ordering lives in the name, and parallel
branches don't collide on a sequence number.
Declare a migration — a unique name and an up({ db, log }) body.
Register migrations into one instance that manages their lifecycle.
Runs all pending migrations in alphabetical order, at server startup.
See what's pending, and what's applied, without running anything.
Mark every registered migration as applied without executing it.
The tracking table — created via your schema pipeline, db push or drizzle-kit.
Per-migration transaction toggle — off for huge tables, then up must be idempotent.
Timestamp-prefixed names (YYYYMMDD_slug) order the run and avoid merge conflicts.