Documentation
¶
Overview ¶
Package migrate applies ordered schema migrations to a database.Database and records them in a bookkeeping table.
migrator := migrate.New(db, func(migrator *migrate.Builder) {
migrator.OutOfOrder(migrate.OutOfOrderError)
})
err := migrator.Add(
migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Schema(schema.Create("users", func(table *schema.Table) {
table.ID()
table.String("email", 255).Unique()
})))
migration.Down(migrate.Schema(schema.Drop("users")))
}),
)
if err != nil {
return err
}
if err := migrator.Up(ctx); err != nil {
return err
}
To revert separately, migrator.Rollback(ctx, migrate.Steps(1)) reverts the most recently applied migration. Check its error before continuing.
Order and ids ¶
Add order is run order, and the list is append-only: add new migrations after existing ones and never reorder or remove them. An id is stable. It is the key in the bookkeeping table, so renaming a migration makes it pending again and it runs a second time, while the old id shows up as Status.Missing. Out-of-order detection applies within each group; see Packaging. Rollback uses newest batch first and reverse Add order within each batch, not wall-clock timestamps. Preserve the relative Add order of applied ids. Unknown ids in a target batch block rollback before known ids in that batch; restore their definitions rather than guessing their original order.
Id matching in Go is byte-exact. MySQL uses VARBINARY(191) NOT NULL with a full single-column primary key for new ledgers and verifies actual metadata on every operation before user steps or history mutations. Unsupported layouts fail with ErrInvalidMigration; unreadable/hidden metadata fails closed without assuming that conversion is needed. Existing ledgers are never automatically altered. See MySQL ledger compatibility below.
Because order comes from Add order, an id needs no number or date prefix. Name a migration for the change it makes, in lowercase words joined by hyphens: "create-users", "add-users-email-index", "backfill-users-email". Ids are unique across every group. A package writes short ids and its MigrationGroup prefixes them with the group name and a slash, so "create-users" in group "acme.users" is stored as "acme.users/create-users"; see Packaging.
Transactions ¶
By default a migration runs in a transaction together with its bookkeeping row, when the dialect rolls back DDL (query.Dialect.TransactionalDDL): PostgreSQL and SQLite. MySQL implicitly commits around DDL, so migrations run there without a transaction. A migration marked MigrationBuilder.WithoutTransaction never starts one. Such a migration must be idempotent: a failure can leave it partly applied and unrecorded, and the next Migrator.Up runs it again.
A step receives the migrator database and a context that carries the retained session and, when applicable, its transaction. Its statements join both. Calling database.Database.WithTransaction inside a transactional migration creates a nested transaction, which fails with database.ErrNestedTransaction when the driver cannot nest. There is no operation-wide transaction: earlier migration commits survive a later failure. Run deployments with an ordinary context. If ctx already carries a transaction, the migrator joins it instead, so that outermost transaction controls final commit and the migration lock's release. Even WithoutTransaction cannot escape a transaction the caller already started.
Dialects ¶
A migration runs on every dialect unless MigrationBuilder.Only restricts it. Use Only for a shipped migration that is meant for one dialect, such as a PostgreSQL-only cache table:
migrate.NewMigration("create-cache", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE UNLOGGED TABLE cache (id INTEGER)")))
migration.Down(migrate.Schema(schema.Drop("cache")))
migration.Only(query.Postgres)
})
Migrator.Add rejects such a migration with ErrInvalidMigration when the database uses another dialect, before any SQL runs. It is never skipped silently. A migration whose logic differs per dialect should stay unrestricted and switch on database.Database.Dialect inside its Func.
MySQL ledger compatibility ¶
Up, Rollback, and Status use target-addressed SHOW FULL COLUMNS and SHOW INDEX under the retained session and lock. Character columns (including _bin and NO PAD collations), fixed BINARY, nullable/generated columns, and composite or prefix primary keys are not canonical ID storage. Additional unique indexes are rejected rather than allowing narrower prefix/expression equality to collide after user steps. SQL inspection failures preserve their cause; unavailable metadata does not trigger a repair attempt. No per-migration event is emitted for work that never started.
A configured MySQL table is frozen to an actual schema/table before steps: unqualified names use the current session database; qualified names retain their components. Subsequent USE statements cannot redirect bookkeeping. ID insert/delete arguments are the original UTF-8 bytes, not character strings subject to connection conversion. Public ID and group values stay unchanged. Non-MySQL preparation and bindings retain their existing behavior.
An operator must explicitly authorize conversion of a legacy ledger. First quiesce all migrators/writers and conflicting DDL, back up the schema and every (HEX(id), batch, ran_at) tuple, and prove the stored bytes match the intended UTF-8 identities within 191 bytes without truncation. Non-UTF-8 storage can require an owner decision: physical bytes and application identity cannot both be preserved by blindly changing a charset. Do not normalize, trim, rename, or discard history. Inspect dependent constraints, triggers, generated values, and column attributes; use strict SQL mode and verify all history tuples and canonical metadata after the approved change. Reconcile earlier failed nontransactional steps before allowing replay: those changes might already be final. The library never runs ALTER for this conversion and the migration role needs no new ALTER privilege. Its lock coordinates cooperating migrators, not arbitrary administrator DDL.
Locking ¶
Migrator.Up, Migrator.Rollback, and Migrator.Status hold the database lock named by Builder.LockName while they work. Each attempt enters database.Database.WithSession and tries the lock once. A refused attempt ends that scope before waiting to retry, releasing newly acquired pool capacity. A successful attempt retains one physical connection for the lock, table preparation, history, planning, all migration transactions and SQL, and bookkeeping. A lost session fails closed: the operation never reconnects or replays its plan through another pool connection. The guarantee depends on the dialect:
- PostgreSQL: session advisory locks span every process and instance that uses the same database. A crashed holder loses the lock when its connection drops. Safe to run on many instances.
- MySQL: GET_LOCK spans every process and instance that uses the same server, and ends with the session. Safe to run on many instances, but see Transactions: MySQL DDL is not transactional.
- SQLite: an in-process lock covers all drivers in one process. For a file-backed database on Linux, macOS, and the BSDs, an flock on a sibling "<dbfile>.<hash>.lock" file also covers other processes on the same host. In-memory databases, other operating systems, and database files on network file systems get no cross-process lock. SQLite is not a horizontal-scaling database.
PgBouncer in transaction pooling mode breaks PostgreSQL session advisory locks: the lock and the unlock can run on different server connections, so the lock is not held, or never released. Run migrations over a direct connection or a session-pooled one.
Steps must pass their supplied context to every facade call, not use a fresh context or the raw database.Database.Driver. Those paths bypass the pinned session and can wait forever for another connection in a one-connection pool. Close streaming cursors before subsequent statements or transaction boundaries. Managed named locks are not reentrant: never acquire the migration's lock separately and then call the migrator. Other root-level held locks also reserve pool connections and can prevent session acquisition when the pool is full.
An existing session or transaction is borrowed, not released between failed attempts; its owner still reserves its connection. Prefer an ordinary context for deployments so contention can release all newly acquired capacity.
Lock timeout ¶
Builder.LockTimeout bounds session/pool acquisition and lock contention. Its timer stops on acquisition, before table preparation, history, or migrations. Caller cancellation still applies throughout the work. Zero uses five minutes and a negative value waits until the context ends. When acquisition times out, the error wraps both ErrLockTimeout and context.DeadlineExceeded. If the caller's own context ends first, the error wraps that context's error instead. Unlock uses a bounded cleanup context independent of caller cancellation. Cleanup errors are joined with the original error; a failed session is never retried by the operation. A callback panic is re-raised after cleanup.
Idempotent migrations ¶
A migration that runs without a transaction, through MigrationBuilder.WithoutTransaction or on MySQL, can stop half way and run again on the next Migrator.Up. Write each step so a second run is safe: prefer schema.CreateIfNotExists and schema.DropIfExists.
migrate.NewMigration("create-audit", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Schema(
schema.CreateIfNotExists("audit", func(table *schema.Table) {
table.ID()
table.String("action", 64)
}),
))
migration.Down(migrate.Schema(schema.DropIfExists("audit")))
migration.WithoutTransaction()
})
Rolling deploys ¶
During a rolling deploy, old and new code run against the same schema. Use expand and contract: first release a migration that only adds (tables, nullable columns, indexes) and keeps old code working; deploy the code that uses the new schema; and in a later release remove what nothing uses any more. Never rename or drop in the same release that stops using the old name.
In production, run migrations once per deploy as a Job, an init container, or a dedicated command that exits when done, before the new instances take traffic. Registering Migrator.Up with Composer.Startup makes every instance compete for the lock, delays startup, and ties migration failures to instance health. The lock keeps that safe, but it is best for development and single-instance setups.
Packaging ¶
Nothing registers itself. A package that needs tables exports a function that returns its definitions, and the application adds them in the order it chooses:
func Migrations() []migrate.Definition {
return migrate.NewGroup("acme.users").Migrations(
migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
migration.Down(migrate.Schema(schema.Drop("users")))
}),
)
}
err := migrator.Add(users.Migrations()...)
A package wraps its definitions in a NewGroup and gives them short ids. MigrationGroup.Migrations prefixes each id with the group name and a slash, so the stored id, Definition.ID, Status.ID, and MigrationEvent.ID are "acme.users/create-users". A short id may not contain a slash, which also rejects ids a package prefixed by hand, and the combined id must fit in 191 bytes; both are ErrInvalidMigration at Migrator.Add. Ungrouped ids may contain slashes. Any valid group name works. Arkoren's own packages name groups "arkoren.dev:<module>:<package>", plus ":<instance>" when a driver can run several instances in one database; that is a repository convention, not a rule of this package.
A group is its own append-only sequence: out-of-order detection compares a migration only with migrations of the same group, so a package release that appends a migration runs it on the next Migrator.Up without an out-of-order warning or ErrOutOfOrder, wherever the application added the package's definitions. Migrations without a group, usually the application's own, form the default group. Add order stays run order across groups: add a package's migrations before application migrations that refer to its tables.
Validation ¶
NewMigration invokes its callbacks directly once, synchronously in order on one MigrationBuilder. Settings return that draft. Nil callbacks panic and application panics propagate; expanded nil or empty slices supply no edits. Callbacks still run when domain validation fails. The published Definition detaches owned step and dialect slices, so retaining a draft cannot change it. Step functions and their closure captures are not copied or run at construction. Exec and Schema do copy their own statement and operation slices immediately; replacing an entry in the caller's slice cannot replace a published step.
The draft retains the first domain error. Required Up is checked after all callbacks; omission is invalid even when no callbacks were supplied. Migrator.Add reports construction errors as ErrInvalidMigration. An unknown nonempty OutOfOrder policy is rejected with ErrInvalidMigration by Up, Rollback, and Status before session acquisition or SQL; empty still selects OutOfOrderWarn. A Func is opaque, so Exec with no statements or a nil statement, or Schema with no operations, is detected when the step runs, before it executes anything, not at Add.
Events ¶
With Builder.Events, Migrator.Up publishes Applied ("database.migration.applied") after each migration, Migrator.Rollback publishes RolledBack ("database.migration.rolled-back"), and either publishes Failed ("database.migration.failed") for the migration that failed before completion. If a migration transaction commits its schema and bookkeeping but cleanup or an after-commit hook then fails, it publishes the success event with nil Err and returns the error without running later migrations. An error returned by a step remains a failure even when it matches database.ErrAfterCommit. An outer session cleanup error does not change events for migrations that already completed. Events inside a caller-owned outer transaction remain provisional until that transaction commits.
migrator := migrate.New(db, func(migrator *migrate.Builder) {
migrator.Events(app.Events())
})
New configures the migrator component through Builder policies; declaration policies remain the separate MigrationBuilder role. Component policies run directly once in order on one draft before defaults are resolved. Nil entries panic; expanded nil/empty slices supply no edits. Publication detaches settings, not the database, logger or event bus. Construction performs no migration, bookkeeping, session or lock I/O.
Index ¶
- Variables
- type Builder
- func (migrator *Builder) Events(bus *event.Bus) *Builder
- func (migrator *Builder) LockName(name string) *Builder
- func (migrator *Builder) LockTimeout(timeout time.Duration) *Builder
- func (migrator *Builder) Logger(logger *slog.Logger) *Builder
- func (migrator *Builder) OutOfOrder(mode OutOfOrder) *Builder
- func (migrator *Builder) Table(table string) *Builder
- type BuilderFunc
- type Definition
- func (definition Definition) DescriptionValue() string
- func (definition Definition) Dialects() []query.Dialect
- func (definition Definition) Err() error
- func (definition Definition) Group() string
- func (definition Definition) ID() string
- func (definition Definition) Reversible() bool
- func (definition Definition) Transactional() bool
- type Func
- type MigrationBuilder
- func (migration *MigrationBuilder) Description(text string) *MigrationBuilder
- func (migration *MigrationBuilder) Down(steps ...Func) *MigrationBuilder
- func (migration *MigrationBuilder) Only(dialects ...query.Dialect) *MigrationBuilder
- func (migration *MigrationBuilder) Up(steps ...Func) *MigrationBuilder
- func (migration *MigrationBuilder) WithoutTransaction() *MigrationBuilder
- type MigrationBuilderFunc
- type MigrationEvent
- type MigrationGroup
- type Migrator
- func (migrator *Migrator) Add(migrations ...Definition) error
- func (migrator *Migrator) Database() *database.Database
- func (migrator *Migrator) Rollback(ctx context.Context, opts ...RollbackOption) error
- func (migrator *Migrator) Status(ctx context.Context) ([]Status, error)
- func (migrator *Migrator) Up(ctx context.Context) error
- type OutOfOrder
- type RollbackOption
- type Status
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidMigration reports an invalid definition or argument. ErrInvalidMigration = errors.New("invalid migration") // ErrDuplicateID reports a migration id that is already added. ErrDuplicateID = errors.New("duplicate migration id") // ErrOutOfOrder reports pending migrations that precede applied ones. ErrOutOfOrder = errors.New("out-of-order migrations") // ErrIrreversible reports a migration without a Down step. ErrIrreversible = errors.New("irreversible migration") // ErrLockTimeout reports that session acquisition or lock contention used // the [Builder.LockTimeout] budget before protected work began. The error // also wraps [context.DeadlineExceeded]. Caller-owned cancellation and // deadlines are returned without this marker; work is never replayed after // successful acquisition, even if its session or cleanup fails. ErrLockTimeout = errors.New("migration lock timeout") // ErrUnknownMigration reports a recorded migration that is not added. ErrUnknownMigration = errors.New("unknown migration") )
var ( // Applied is published after [Migrator.Up] applies and records one // migration. Applied = event.NewTopic[MigrationEvent]("database.migration.applied") // RolledBack is published after [Migrator.Rollback] reverts one // migration and deletes its record. RolledBack = event.NewTopic[MigrationEvent]("database.migration.rolled-back") // Failed is published when applying or reverting a migration fails; the // run stops there. Failed = event.NewTopic[MigrationEvent]("database.migration.failed") )
Functions ¶
This section is empty.
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder configures New before publication. Callbacks run directly once, synchronously in order on one fresh draft. Nil callback entries panic; expanded nil or empty callback slices supply no edits. Retaining the draft cannot change published settings. Methods return the same draft.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.Table("application_migrations")
})
func (*Builder) Events ¶
Events selects the shared bus receiving Applied, RolledBack and Failed after each migration outcome. Nil publishes nothing; listeners cannot change the outcome or take ownership of migration execution.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.Events(nil)
})
func (*Builder) LockName ¶
LockName selects the database lock that serializes migration work. Empty resolves to arkoren_migrations after all callbacks. Construction acquires no lock and does not change session ownership or release timing.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.LockName("application_migrations")
})
func (*Builder) LockTimeout ¶
LockTimeout sets the combined session-acquisition and lock-wait budget. Once the lock is acquired the budget no longer limits preparation or work; the caller's context still does. Zero uses five minutes, and a negative duration disables this budget without disabling caller cancellation.
Example:
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.LockTimeout(time.Minute)
})
func (*Builder) Logger ¶
Logger selects the shared migration log destination. Nil resolves slog.Default after all construction callbacks; no logs are emitted here.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.Logger(nil)
})
func (*Builder) OutOfOrder ¶
func (migrator *Builder) OutOfOrder(mode OutOfOrder) *Builder
OutOfOrder selects how Up handles pending migrations older than completed migrations in the same group. The zero value warns and runs them; allow and error modes retain their existing execution-time behavior. Unknown nonempty labels remain on the draft but Up, Rollback, and Status reject them with ErrInvalidMigration before session acquisition or SQL.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.OutOfOrder(migrate.OutOfOrderError)
})
func (*Builder) Table ¶
Table selects the bookkeeping table. Empty resolves to arkoren_migrations after all callbacks. Construction neither creates nor inspects that table; the existing migration execution path owns bookkeeping I/O. On MySQL, an unqualified name is resolved against the retained session's current database before steps; qualified names preserve their components. The operation then uses that frozen target for metadata and all history SQL. Existing MySQL storage must pass the package's binary-ledger verification; Table does not authorize conversion or alter an existing ledger.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.Table("application_migrations")
})
type Definition ¶
type Definition struct {
// contains filtered or unexported fields
}
Definition describes one immutable migration, published by NewMigration. Its accessors never expose owned containers. Construction errors are retained for Definition.Err and rejected by Migrator.Add before SQL runs.
Example:
definition := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
})
if err := definition.Err(); err != nil {
return err
}
func NewMigration ¶
func NewMigration(id string, configure ...MigrationBuilderFunc) Definition
NewMigration publishes a definition with a stable bookkeeping id of 1-191 printable UTF-8 bytes without whitespace. Callbacks run directly once, synchronously in order on one draft, even if domain validation fails. Nil callbacks panic; expanded nil or empty callback slices make no edits. Callback panics propagate. With no Up, the result has the normal required-Up error. Owned step and dialect slices are detached on setting and publication.
Example:
definition := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
})
if err := definition.Err(); err != nil {
return err
}
func (Definition) DescriptionValue ¶
func (definition Definition) DescriptionValue() string
DescriptionValue returns the description text.
func (Definition) Dialects ¶
func (definition Definition) Dialects() []query.Dialect
Dialects returns a copy of the dialects set by MigrationBuilder.Only, in the order given and without duplicates. Nil means the migration runs on every dialect. Changing the result does not affect the definition.
Example:
definition := migrate.NewMigration("create-cache", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE cache (id INTEGER)")))
migration.Only(query.Postgres)
})
dialects := definition.Dialects() // An independent, one-element slice.
func (Definition) Err ¶
func (definition Definition) Err() error
Err returns the first error found while building the definition, or a missing-Up error.
func (Definition) Group ¶
func (definition Definition) Group() string
Group returns the name of the group set by MigrationGroup.Migrations, or the empty string for the default group.
func (Definition) Reversible ¶
func (definition Definition) Reversible() bool
Reversible reports whether the migration has Down steps.
func (Definition) Transactional ¶
func (definition Definition) Transactional() bool
Transactional reports whether the migration asks for a transaction. The dialect can still run it without one; see the package documentation.
type Func ¶
Func performs one migration step using the supplied database and context. The context routes every facade operation onto the lock-owning physical session and, for transactional migrations, its current transaction. Always forward this context to database calls; a fresh context or direct Driver call bypasses the scope and can deadlock a one-connection pool. The context expires when its enclosing session or transaction callback ends; do not retain it or use it concurrently.
Example:
step := func(ctx context.Context, db *database.Database) error {
_, err := db.Exec(ctx, query.Raw("UPDATE users SET active = TRUE"))
return err
}
func Exec ¶
func Exec(statements ...query.Statementer) Func
Exec returns a step that runs each statement in order with database.Database.Exec. A failure is wrapped as "statement N: ..." with N starting at 1. Because a Func is opaque, calling Exec with no statements or a nil statement is detected when the step runs, before it executes anything, not when the migration is added. The statement slice is copied immediately; statement implementations and arbitrary argument values retain their existing ownership rules.
Example:
definition := migrate.NewMigration("backfill-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("UPDATE users SET active = TRUE")))
})
if err := migrator.Add(definition); err != nil {
return err
}
func Schema ¶
Schema returns a step that runs each schema operation in order. For every operation it first checks schema.Operation.Err, then runs its statements in order with database.Database.Exec, compiled for the database's dialect. A failure is wrapped as "operation N statement M: ..." with both counted from 1; an operation that failed to construct reports "operation N: ..." before anything runs. As with Exec, an empty list is detected when the step runs, before it executes anything, and fails with ErrInvalidMigration.
Statements run one at a time, so on a dialect without transactional DDL (query.Dialect.TransactionalDDL) a failure part way leaves the earlier statements applied. The operation slice is copied immediately without rendering or executing it; later replacement of a caller's slice entry cannot change the step.
Example:
migrate.NewMigration("20261017_create_users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Schema(
schema.Create("users", func(table *schema.Table) {
table.ID()
table.String("email", 255).Unique()
}),
))
migration.Down(migrate.Schema(schema.Drop("users")))
})
type MigrationBuilder ¶
type MigrationBuilder struct {
// contains filtered or unexported fields
}
MigrationBuilder configures a migration during NewMigration. Settings return the same draft and retain the first error. A retained draft cannot change a published Definition; step functions and their captures are shared. Missing Up is checked on the published definition, after all callbacks.
Example:
definition := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
migration.Description("Create the users table")
})
if err := definition.Err(); err != nil {
return err
}
func (*MigrationBuilder) Description ¶
func (migration *MigrationBuilder) Description(text string) *MigrationBuilder
Description replaces the human-readable description, with empty text clearing it. It returns the same draft and does not change any retained error.
Example:
definition := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
migration.Description("Create the users table")
})
fmt.Println(definition.DescriptionValue()) // Create the users table
func (*MigrationBuilder) Down ¶
func (migration *MigrationBuilder) Down(steps ...Func) *MigrationBuilder
Down sets the steps that revert the migration. Calling it twice, with no steps, or with a nil step retains an ErrInvalidMigration error. The input slice is copied without invoking its functions. Omit Down for an irreversible migration. Down returns the same draft, including when it is invalid.
Example:
definition := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
migration.Down(migrate.Schema(schema.Drop("users")))
})
if err := definition.Err(); err != nil {
return err
}
func (*MigrationBuilder) Only ¶
func (migration *MigrationBuilder) Only(dialects ...query.Dialect) *MigrationBuilder
Only restricts the migration to the listed dialects. Migrator.Add rejects the migration with ErrInvalidMigration when the migrator's database uses another dialect: it never skips a migration silently, and fails before any SQL runs.
Use it for a shipped migration that only makes sense on one dialect, such as a PostgreSQL-only cache table:
migrate.NewMigration("create-cache", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE UNLOGGED TABLE cache (id INTEGER)")))
migration.Down(migrate.Schema(schema.Drop("cache")))
migration.Only(query.Postgres)
})
A migration whose logic differs per dialect should not use Only; switch on database.Database.Dialect inside a Func instead.
Calling Only again replaces the earlier list, like MigrationBuilder.Description. Duplicate dialects are removed. Calling it with no dialects is an error. Without Only the migration runs on every dialect. The input list is copied; Only returns the same draft, including when it is invalid.
func (*MigrationBuilder) Up ¶
func (migration *MigrationBuilder) Up(steps ...Func) *MigrationBuilder
Up sets the steps that apply the migration. Calling it twice, with no steps, or with a nil step retains an ErrInvalidMigration error. The input slice is copied; functions are stored for execution, not invoked here. Up returns the same draft, including when it is invalid.
Example:
definition := migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
})
if err := definition.Err(); err != nil {
return err
}
func (*MigrationBuilder) WithoutTransaction ¶
func (migration *MigrationBuilder) WithoutTransaction() *MigrationBuilder
WithoutTransaction configures the draft not to start a per-migration transaction. Its steps still use the lock-owning session, and still join a transaction the caller already supplied. Without an enclosing transaction, failure can leave partial schema changes without a bookkeeping row, so the migration must be safe to run again after deliberate recovery. The setting is idempotent and returns the same draft.
Example:
definition := migrate.NewMigration("create-audit", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE IF NOT EXISTS audit (id INTEGER)")))
migration.WithoutTransaction()
})
type MigrationBuilderFunc ¶
type MigrationBuilderFunc = func(*MigrationBuilder)
MigrationBuilderFunc configures a MigrationBuilder.
type MigrationEvent ¶
type MigrationEvent struct {
// ID is the migration identifier.
ID string
// Group is the migration group; empty for the application's own.
Group string
// Batch is the batch the migration was applied in.
Batch int
// Rollback reports whether the migration was being reverted.
Rollback bool
// Duration is how long the migration ran.
Duration time.Duration
// Err is the error that failed the migration; nil for [Applied] and
// [RolledBack].
Err error
}
MigrationEvent is the payload of Applied, RolledBack, and Failed.
type MigrationGroup ¶
type MigrationGroup struct {
// contains filtered or unexported fields
}
MigrationGroup names an ordered sequence of migrations that a package ships. Out-of-order detection compares migrations only within one group, so a package release that adds a migration does not count as out of order against the application's own migrations. Create one with NewGroup.
func NewGroup ¶
func NewGroup(name string) MigrationGroup
NewGroup returns a MigrationGroup called name. The name has the same rules as a migration id: 1-191 bytes, all printable and none white space. Any valid name works; Arkoren's own packages follow the repository's "arkoren.dev:<module>:<package>" convention, which this package does not enforce.
Example:
func Migrations() []migrate.Definition {
return migrate.NewGroup("acme.users").Migrations(
migrate.NewMigration("create-users", func(migration *migrate.MigrationBuilder) {
migration.Up(migrate.Exec(query.Raw("CREATE TABLE users (id INTEGER)")))
migration.Down(migrate.Schema(schema.Drop("users")))
}),
)
}
func (MigrationGroup) Migrations ¶
func (group MigrationGroup) Migrations(definitions ...Definition) []Definition
Migrations returns copies of definitions that belong to the group, in the same order. Each copy's id becomes the group name, a slash, and the short id the package gave it, so a package writes stable step names ("create-users") and the group decides the full id ("acme.users/create-users") that Definition.ID, Migrator.Status, and MigrationEvent report.
A definition becomes invalid, and Migrator.Add reports it as ErrInvalidMigration, when the group name is invalid, when it already belongs to a group, when its short id contains a slash (which also rejects ids prefixed by hand), or when the combined id is longer than 191 bytes. Ungrouped ids may contain slashes.
type Migrator ¶
type Migrator struct {
// contains filtered or unexported fields
}
Migrator applies migrations to a database. Create one with New.
func New ¶
func New(db *database.Database, configure ...BuilderFunc) *Migrator
New creates a Migrator for db. It performs no I/O.
Construction callbacks run directly once, synchronously in order on one fresh draft. Nil entries panic; expanded nil or empty slices omit edits. Publication detaches owned settings, not live dependencies or captures.
Example:
migrate.New(database, func(migrator *migrate.Builder) {
migrator.LockTimeout(time.Minute)
})
func (*Migrator) Add ¶
func (migrator *Migrator) Add(migrations ...Definition) error
Add registers migrations in order. Add order is run order. It adds all of them or none. It returns ErrInvalidMigration for an invalid definition and ErrDuplicateID for an id that is already added. A definition restricted by MigrationBuilder.Only to dialects that exclude the database's dialect is invalid: Add reports it as ErrInvalidMigration naming the id, the allowed dialects, and the database dialect, before anything is registered or run.
func (*Migrator) Rollback ¶
func (migrator *Migrator) Rollback(ctx context.Context, opts ...RollbackOption) error
Rollback reverts the last batch, or the last n migrations with Steps, in reverse application order under the migration lock. It validates every target before running anything: ErrUnknownMigration for a recorded migration that is not added, ErrIrreversible for one without Down steps. History, planning, steps, and bookkeeping share the lock-owning session; a lost session stops the run without replay. Earlier Down commits remain final unless ctx carries a surrounding transaction. Batches are reverted newest first; within a batch the registered Add order is reversed, independent of timestamp precision or clock changes. Preserve the original relative Add order of applied definitions. Unknown records in a batch take precedence over known targets and fail with ErrUnknownMigration: re-register their definitions rather than guessing their application order. MySQL validates actual binary ledger metadata before any Down callback or deletion, and never converts an existing unsupported ledger automatically.
Example:
err := migrator.Rollback(ctx, migrate.Steps(1))
if err != nil {
return err
}
func (*Migrator) Status ¶
Status lists the registered migrations in Add order, then recorded migrations that are not registered, marked Missing. Like Migrator.Up, it takes the migration lock and creates the bookkeeping table when it does not exist, so it writes to a fresh database. Both preparation and history reads use the lock-owning session. Acquisition, session loss, or cleanup failures return a nil result rather than a snapshot whose protected scope did not complete successfully. On MySQL this also verifies canonical binary ledger storage. An unsupported or unverifiable existing table returns nil and an error, never an ALTER.
Example:
statuses, err := migrator.Status(ctx)
if err != nil {
return err
}
for _, status := range statuses {
fmt.Println(status.ID, status.Ran)
}
func (*Migrator) Up ¶
Up applies pending migrations in Add order while retaining the lock-owning physical session. It stops at the first failure without reconnecting or replaying work; earlier migration commits stay recorded. Each transactional migration commits separately unless ctx already carries an outer transaction. See Builder.OutOfOrder for pending migrations preceding applied ones. MySQL verifies canonical binary ledger storage before history or callbacks; unsupported layouts return ErrInvalidMigration without automatic conversion.
Example:
err := migrator.Up(ctx)
if err != nil {
return err // Inspect the cause; never blindly replay nontransactional work.
}
type OutOfOrder ¶
type OutOfOrder string
OutOfOrder says what Migrator.Up does with a pending migration that was added before a migration of the same group that already ran. Empty construction input selects OutOfOrderWarn. The string values are stable configuration labels, not an ordered set. Other nonempty labels are invalid; Up, Rollback, and Status reject them before acquiring a session.
Example: _ = migrate.New(db, func(migrator *migrate.Builder) {
migrator.OutOfOrder(migrate.OutOfOrderError)
})
const ( // OutOfOrderWarn logs a warning and runs the migration. It is the default. // // Example: _ = migrate.New(db, func(migrator *migrate.Builder) { // migrator.OutOfOrder(migrate.OutOfOrderWarn) // }) OutOfOrderWarn OutOfOrder = "warn" // OutOfOrderAllow runs the migration without a warning. // // Example: _ = migrate.New(db, func(migrator *migrate.Builder) { // migrator.OutOfOrder(migrate.OutOfOrderAllow) // }) OutOfOrderAllow OutOfOrder = "allow" // OutOfOrderError fails with [ErrOutOfOrder] before any migration runs. // // Example: _ = migrate.New(db, func(migrator *migrate.Builder) { // migrator.OutOfOrder(migrate.OutOfOrderError) // }) OutOfOrderError OutOfOrder = "error" )
type RollbackOption ¶
type RollbackOption func(*rollbackConfig)
RollbackOption changes how Migrator.Rollback chooses migrations.
func Steps ¶
func Steps(n int) RollbackOption
Steps reverts the last n applied migrations instead of the last batch. An n below one makes Rollback fail with ErrInvalidMigration.
type Status ¶
type Status struct {
// ID is the migration id.
ID string
// Group is the migration's group, or the empty string for the default
// group and for a Missing migration.
Group string
// Ran reports whether the migration is recorded as applied.
Ran bool
// Batch is the batch that applied the migration, or zero.
Batch int
// RanAt is when the migration was applied, in UTC, or zero.
RanAt time.Time
// OutOfOrder marks a pending migration that precedes an applied one of
// the same group.
OutOfOrder bool
// Missing marks a recorded migration that is not added to the migrator.
Missing bool
}
Status describes one migration for Migrator.Status.