Library API¶
Full generated reference lives on pkg.go.dev. This page is the guided version.
Entry points¶
Every function takes a *sql.DB you opened yourself, with whatever driver you
prefer. The library never opens a connection on your behalf.
Up¶
Applies pending migrations forward. Migrations present in the database but
missing from fsys are left alone and reported in Result.ExtrasLeft.
The conservative choice for production: deploying an older branch cannot silently drop schema a newer release added.
Reconcile¶
Makes the database match the files. Refuses with ErrDivergence when that would
require removing something, unless WithRollback() is passed.
Down / DownAll¶
func Down(ctx context.Context, sqlDB *sql.DB, count int, opts ...Option) (*Result, error)
func DownAll(ctx context.Context, sqlDB *sql.DB, opts ...Option) (*Result, error)
Rolls back the last count (or every) applied migration, most recent first,
using the scripts stored in the database. No fs.FS needed — the files are
irrelevant here.
Refused with ErrIrreversible before anything runs if any candidate is marked
irreversible.
Plan¶
What a Reconcile with the same options would do. Takes no lock, writes
nothing, needs no DDL privileges — a missing bookkeeping table just means nothing
has been applied yet.
Analysis.Blocked reports whether that Reconcile would refuse, and
BlockedReason says why. Without those, a plan listing migrations under
ToRollback would read as "this will happen" when the same call would in fact
stop with ErrDivergence.
Status¶
The bookkeeping rows in application order. Read-only. Returns an empty slice for a database that has never been migrated.
Baseline¶
func Baseline(ctx context.Context, sqlDB *sql.DB, fsys fs.FS, throughID string, opts ...Option) (*Result, error)
Records every migration up to and including throughID as applied, without
running any of them — how the library is adopted on a database whose schema
already exists.
// The schema already matches migrations 1..N; adopt without re-running them.
result, err := migrate.Baseline(ctx, db, fsys, "20260115103000_create_users")
if err != nil {
return err
}
// From here on, Up applies only what comes after.
_, err = migrate.Up(ctx, db, fsys)
Returns ErrAlreadyRecorded unless the bookkeeping table is empty, and
ErrBaselineNotFound if the ID is not in fsys. The first guard is what stops
this from doubling as "mark applied without running" — a row it writes is
indistinguishable from a genuine apply.
Forget¶
Removes a migration's bookkeeping row without running its rollback script.
The escape hatch from ErrFailedMigrations: once you have brought the schema to
a known state by hand, this tells the library to stop refusing. It touches
nothing but the ledger — the schema is your business, since only you know what
state you put it in.
Options¶
| Option | Effect |
|---|---|
WithRollback() |
Let Reconcile roll back migrations missing from the source. |
WithInterleaved() |
Allow applying migrations that sort before the latest applied one. |
WithLockTimeout(d) |
Bound the advisory lock wait. 0 waits indefinitely. Default 5s. |
WithLockID(id) |
Change the advisory lock ID, to isolate from another tool. |
WithAllowEmptySource() |
Permit reconciling from a source with no migrations. |
WithTableName(name) |
Bookkeeping table name. Default migrations. |
WithSchema(schema) |
Schema for the bookkeeping table. Default: the search_path. |
WithLogger(l) |
A *slog.Logger. Defaults to slog.Default(). |
result, err := migrate.Reconcile(ctx, db, fsys,
migrate.WithRollback(),
migrate.WithSchema("infra"),
migrate.WithTableName("schema_migrations"),
migrate.WithLockTimeout(2*time.Minute),
migrate.WithLogger(logger),
)
Result types¶
type Result struct {
Applied []string // actually applied, in execution order
RolledBack []string // actually rolled back, in execution order
ExtrasLeft []string // Up only: in the DB, missing from fsys, left alone
Interleaved []string // subset of Applied that went in out of ID order
}
Read the Result even on error
On failure the slices hold the prefix that completed before the failure. A partially applied run reports exactly how far it got, which is what you want in a deployment log.
The pointer is nil only when the call was rejected before anything ran — a
nil *sql.DB, a nil fs.FS, an invalid table name. Guard for it:
type Analysis struct {
BlockedReason string // why; empty unless Blocked
ToApply []string
ToRollback []string
Interleaved []string
Blocked bool // a Reconcile with these options would refuse
}
type AppliedMigration struct {
AppliedAt time.Time
ID string
Error string // the recorded error; empty unless Failed
DownScript string // the rollback script as stored at apply time
Failed bool
}
All three carry JSON tags — the CLI's --json output is these types verbatim.
Sentinel errors¶
Match with errors.Is. Every one of these means nothing was changed unless
noted.
| Error | Meaning |
|---|---|
ErrDivergence |
DB has migrations missing from the files. Pass WithRollback() or fix the source. |
ErrInterleaved |
Files contain IDs sorting before the latest applied. Pass WithInterleaved() if intended. |
ErrFailedMigrations |
A migration is recorded as failed. Terminal — do not retry. |
ErrIrreversible |
A rollback would touch a migration marked irreversible. |
ErrEmptySource |
Source empty, database not. Usually a missing fs.Sub. |
ErrUnrecorded |
The schema changed but the ledger could not be updated. Needs a human. |
ErrLockTimeout |
Another migration is in progress. Safe to retry. |
ErrPoolTooSmall |
SetMaxOpenConns(1). Use 0 or >= 2. |
ErrAlreadyRecorded |
Baseline on a ledger that already holds rows. Adoption happens once. |
ErrBaselineNotFound |
The ID given to Baseline is not in the source. |
switch {
case errors.Is(err, migrate.ErrFailedMigrations):
// Terminal. Stop the rollout and page someone.
return fmt.Errorf("manual intervention required: %w", err)
case errors.Is(err, migrate.ErrLockTimeout):
// Another instance is migrating. Retrying is reasonable.
return retryLater(err)
case err != nil:
return err
}