Database

Migrations

Load file-based migrations, plan changes, and register database backends.

By Guillermo Alvarez - Published - Updated

Mental model

golazy.dev/lazymigrate has two sides:

  • Source loads ordered migration files.
  • Backend records 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:

  1. Validate that required clients or pools are configured.
  2. Create its metadata table or collection in Setup.
  3. Return stable, unique applied IDs from List.
  4. In Run, parse the backend-specific migration format.
  5. Run each step under the store's safest locking and transaction model.
  6. Record the ID after a successful up step and remove it after a successful down step.
  7. 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.