| name | migrations |
| description | Use when evolving a liteorm schema — AutoMigrate for additive changes, GenerateMigration/Diff for reviewable SQL, the migrate runner (Load/New/Up/Down), or WritePair. |
migrations
liteorm separates the two cases. Additive changes auto-apply; destructive ones become reviewable SQL you run through the migrate runner. The runner (liteorm.org/migrate) executes SQL but never generates DDL; the orm package generates DDL but only auto-applies additions.
Two tracks
| Change | Tool |
|---|
| Add a table / column / index | orm.AutoMigrate[T] — applies immediately, never drops or alters types |
| Drop / retype / anything destructive | orm.GenerateMigration[T] — returns reviewable up/down SQL, executes nothing |
AutoMigrate (additive)
_ = orm.AutoMigrate[User](ctx, sess)
_ = orm.AutoMigrate[Order](ctx, sess, orm.WithForeignKeys())
_ = orm.AutoMigrateAll(ctx, sess, User{}, Order{}, Item{})
Creates a missing table (plus its unique indexes and m2m junctions) or ADD COLUMNs for fields the model gained. Also syncs indexes additively: an index/unique tag added to an existing model's field is created on the next migrate (a removed tag is reviewable, never an auto DROP INDEX). Iterates the model, never the DB, so it can never drop.
Foreign keys are opt-in (off by default — relations ship as plain columns): orm.WithForeignKeys() emits a FOREIGN KEY for every belongs-to on a newly created table, or orm:"constraint:fk" opts in one relation. Migrate the referenced table first; adding a constraint to an existing table is reviewable-only.
Diff / GenerateMigration (reviewable)
ch, _ := orm.Diff[User](ctx, sess)
up, down, _ := orm.GenerateMigration[User](ctx, sess)
ch.Changed reports columns whose type the model changed. Detection canonicalizes both sides before comparing, so cross-dialect spellings (BIGSERIAL/bigint, VARCHAR(255)/varchar, TINYINT(1)/tinyint) don't false-positive; an un-canonicalizable type reports no change (conservative — a missed change is a safe no-op, a false one churns migrations). Type changes are never auto-applied. Index introspection is the optional orm.IntrospectIndexes(ctx, sess, table).
You review the SQL, then either run it yourself or feed it into the runner via WritePair.
The migrate runner (Load / New / Up / Down)
State lives in a single-row (version, dirty) ledger table (default schema_migrations), created dialect-aware. A failed step leaves the ledger dirty; the next run refuses until you Force.
import (
"embed"
"liteorm.org/migrate"
)
var migFS embed.FS
migs, _ := migrate.Load(migFS)
m := migrate.New(sess)
n, err := m.Up(ctx, migs)
| Method | Does |
|---|
m.Up(ctx, migs) | Apply every pending migration (returns count). |
m.UpTo(ctx, migs, target) | Apply up to and including target. |
m.Down(ctx, migs) | Roll back the most recent step (errors if its down is empty). |
m.DownTo(ctx, migs, target) | Roll back everything above target. |
m.Status(ctx, migs) | []Status{Version, Name, Applied}. |
m.Version(ctx) | (version, dirty, err). |
m.Force(ctx, version) | Set version + clear dirty (recovery). |
migrate.Load auto-detects three on-disk formats so adopters keep history:
- golang-migrate split:
NNN_name.up.sql / NNN_name.down.sql
- goose / sql-migrate annotated single file:
-- +goose Up / -- +goose Down
- plain numbered single file:
NNN_name.sql (up-only)
WritePair: bridge generated SQL into the runner
migrate.WritePair writes a golang-migrate-style pair that Load reads back — so a generated diff drops straight into the runner an adopter already uses.
up, down, _ := orm.GenerateMigration[User](ctx, sess)
upPath, downPath, err := migrate.WritePair("migrations", 2, "add user fields", up, down)
Version is zero-padded to six digits; the name is slugified. An empty up is an error; an empty down is written as an irreversibility comment so Down reports it as irreversible rather than silently succeeding.
Pitfalls
- AutoMigrate never alters or drops — a type change or column removal needs
GenerateMigration + the runner.
- A dirty ledger blocks all
Up/Down: fix the DB by hand, then Force to the correct version.
- A migration with no down section is irreversible;
Down errors instead of skipping it.
Deeper