Database
Migrations
Load file-based migrations, plan changes, and register database backends.
Mental model
golazy.dev/lazymigrate has two sides:
Sourceloads ordered migration files.Backendrecords and executes migration steps for a real store.
Migrator sits between them. It loads all sources, asks the backend which IDs
are already applied, builds a plan, and calls the backend for each up, down,
or redo step.
Migration sources
Application migrations normally live under migrations/<database>/.
lazymigrate.ForDatabase(files, "postgres") reads direct children from that
directory, ignores migrations.toml, rejects Go files, and requires each file
name to contain a sortable timestamp:
migrations/postgres/202606290001_create_posts.sql
migrations/postgres/202606290002_add_post_status.sql
The file extension does not affect the ID. The first file above has ID
202606290001_create_posts.
Package backends can expose embedded migrations as lazymigrate.Source values:
var catalog lazymigrate.Catalog
_ = catalog.Add("postgres", lazymigrate.ForDatabase(app.Migrations, "postgres"))
_ = catalog.Add("postgres", pgjobs.Migrations())
_ = catalog.Add("postgres", pgfiles.Migrations())
The catalog is useful when one database needs application tables plus package tables. Duplicate IDs across all selected sources are rejected.
Backend contract
A migration backend implements:
type Backend interface {
Setup(context.Context) error
List(context.Context) ([]lazymigrate.BackendMigration, error)
Run(context.Context, lazymigrate.Step) error
DumpSchema(context.Context) ([]byte, error)
LoadSchema(context.Context, []byte) error
}
Setup prepares backend metadata, such as a migration table. List returns
the applied migration IDs. Run receives a Step with DirectionUp or
DirectionDown plus the raw migration content. The backend owns the concrete
file format, transactions, locks, metadata writes, and rollback behavior.
DumpSchema and LoadSchema are schema snapshot hooks. A backend can return a
clear unsupported error until it grows real dump/load semantics.
Implement a backend
A backend should:
- Validate that required clients or pools are configured.
- Create its metadata table or collection in
Setup. - Return stable, unique applied IDs from
List. - In
Run, parse the backend-specific migration format. - Run each step under the store's safest locking and transaction model.
- Record the ID after a successful up step and remove it after a successful down step.
- Return errors with enough context to identify the migration ID and phase.
PostgreSQL's backend does exactly that: it parses -- +lazy Up and
-- +lazy Down SQL sections, runs the selected section in a transaction, holds
an advisory migration lock, and stores applied IDs in lazy_migrations.
Register and run
Build a migrator with the backend and the selected sources:
backend := pgmigrate.New(db)
migrator, err := lazymigrate.New(lazymigrate.Config{
Backend: backend,
Sources: catalog.Sources("postgres"),
})
if err != nil {
return err
}
plan, err := migrator.Up(ctx, 0)
if err != nil {
return err
}
_ = plan
List reports applied, pending, and missing migrations. PlanUp, PlanDown,
and PlanRedo return the steps without touching the backend. Up, Down, and
Redo build the plan and apply it.
A migration recorded by the backend but missing from loaded sources blocks execution plans. That prevents down or redo operations from running with an incomplete source history.
App task registration
Generated apps typically keep a small migration config next to the migration files so local tasks and future generators know which backend package and URL environment variable belong to each database:
[postgres]
backend = "golazy.dev/pg/pgmigrate"
url_env = "DATABASE_URL"
That config does not replace Go registration. It is the app-level convention
for tooling; the runtime still constructs a real backend with the database
client and passes it to lazymigrate.New.
Continue with PostgreSQL for the current PostgreSQL backend and package migrations.