FUNCTIONS / @SPFN/MIGRATE — CODE-BASED DATA MIGRATIONS

Data has
migrations too.

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.

Read the reference Get started $ pnpm add @spfn/migrate
01
The premise

Schema changes are SQL.
Data changes are code.

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.

defineDataMigration
createDataMigrator([...])
apply() at startup
data_migrations ledger
FIG 01 — DEFINE · REGISTER · APPLY · RECORD — THE LEDGER TABLE MUST EXIST BEFORE APPLY()
02
Core technique

The run and the record
are one transaction.

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.

pending migration
up({ db, log })
ledger row written
one commit
FIG 02 — TRANSACTION: TRUE BY DEFAULT · FALSE ⇒ UP MUST BE IDEMPOTENT · ORDER: ALPHABETICAL BY NAME
03
What's inside

A small API
with a full lifecycle.

Move the data.