Running Migrations
Learn how to execute pending migrations using the m.Up() method and understand batch tracking.
Running Migrations
Once your migrations are registered, use m.Up() to execute all pending migrations.
The m.Up() Method
if err := m.Up(); err != nil {
log.Fatal(err)
}m.Up() runs all migrations that haven't been executed yet, in alphabetical order by name.
What Happens During Execution
Lock Acquisition
go-migration takes a cross-process migration lock (a PostgreSQL advisory lock or a MySQL named lock; SQLite relies on its file lock). If another process is already migrating, this call waits — up to 30 seconds by default — and then sees that process's work as already applied. See Concurrency and Locking.
Migration Table Check
go-migration checks if the migrations tracking table exists. If not, it creates one automatically to record which migrations have run. The table's DDL and the bind placeholders used to query it come from the configured grammar, so the tracking table is created with native syntax on PostgreSQL, MySQL, and SQLite.
Pending Migration Detection
The migrator compares your registered migrations against the tracking table to determine which ones are pending (not yet executed).
Sequential Execution
Each pending migration's Up method is called in order. By default, each migration runs inside its own database transaction.
Batch Recording
All migrations executed in a single m.Up() call are assigned the same batch number. This batch number is used for rollback operations. Each migration's tracking row is written inside that migration's own transaction, so a migration can never end up applied but unrecorded.
The tracker uses the grammar passed with migrator.WithGrammar, and falls back to PostgreSQL syntax when none is given. Always pass WithGrammar for MySQL and SQLite, e.g. migrator.WithGrammar(grammars.NewMySQLGrammar()).
Complete Example
package main
import (
"database/sql"
"log"
_ "github.com/lib/pq"
"github.com/gopackx/go-migration/pkg/migrator"
"github.com/gopackx/go-migration/pkg/schema/grammars"
"your-project/migrations"
)
func main() {
db, err := sql.Open("postgres", "postgres://user:password@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
m := migrator.New(db, migrator.WithGrammar(grammars.NewPostgresGrammar()))
if err := m.Register("20240101000001_create_users_table", &migrations.CreateUsersTable{}); err != nil {
log.Fatal(err)
}
if err := m.Register("20240115000001_create_posts_table", &migrations.CreatePostsTable{}); err != nil {
log.Fatal(err)
}
// Run all pending migrations
if err := m.Up(); err != nil {
log.Fatal(err)
}
log.Println("All migrations completed successfully!")
}Idempotent Behavior
Calling m.Up() multiple times is safe. Migrations that have already run are skipped — only pending migrations are executed. This makes it safe to call m.Up() on every application startup, even from several replicas at once: the migration lock makes the other processes wait, and they compute the pending list only once they hold the lock, so they find nothing left to do.
If a migration fails partway through, the transaction for that specific migration — including its tracking row — is rolled back. Previously completed migrations in the same batch remain applied. On MySQL, DDL statements commit implicitly and cannot be rolled back. See Transactions for details.
Dry Run
Pass migrator.WithDryRun(w) to print the SQL each pending migration would run instead of executing it. The CLI equivalent is the --dry-run flag.
m := migrator.New(db,
migrator.WithGrammar(grammars.NewPostgresGrammar()),
migrator.WithDryRun(os.Stdout),
)Dry-run never writes to the database — not even the tracking table. It still reads the tracking table to find pending migrations, so the table must already exist: run migrate:install (or migrator.NewTrackerWithGrammar(db, "migrations", grammar).EnsureTable()) once first, otherwise dry-run fails with ErrTrackingTable. Inside a dry-run, s.HasTable() and s.HasColumn() report false. Dry-run takes the migration lock like a real run.
Before v1.2.0 a dry-run created the tracking table if it was missing. It is now strictly read-only.
Progress Reporting
Use the WithProgress option to receive real-time progress events as each migration executes. This is useful for logging, progress bars, or monitoring long-running migration batches.
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
"github.com/gopackx/go-migration/pkg/migrator"
"github.com/gopackx/go-migration/pkg/schema/grammars"
"your-project/migrations"
)
func main() {
db, err := sql.Open("postgres", "postgres://user:password@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
m := migrator.New(db,
migrator.WithGrammar(grammars.NewPostgresGrammar()),
migrator.WithProgress(func(e migrator.ProgressEvent) {
// Index is 0-based; add 1 for human-friendly "N of Total" display.
fmt.Printf("[%d/%d] %s %s (%s)\n", e.Index+1, e.Total, e.Direction, e.Name, e.Duration)
}),
)
if err := m.Register("20240101000001_create_users_table", &migrations.CreateUsersTable{}); err != nil {
log.Fatal(err)
}
if err := m.Register("20240115000001_create_posts_table", &migrations.CreatePostsTable{}); err != nil {
log.Fatal(err)
}
if err := m.Up(); err != nil {
log.Fatal(err)
}
}Example Output
[1/2] up 20240101000001_create_users_table (12ms)
[2/2] up 20240115000001_create_posts_table (8ms)The ProgressEvent struct contains:
| Field | Type | Description |
|---|---|---|
Name | string | The migration name |
Index | int | 0-based index of the current migration |
Total | int | Total number of migrations being executed |
Duration | time.Duration | Time taken for this migration |
Direction | string | "up" or "down" |
Progress events fire for Up(), Rollback(), and Reset() operations. The Direction field tells you which way the migration ran.
The progress callback is invoked after each migration completes successfully. If a migration fails, no progress event is emitted for it.
Sentinel Errors
Errors returned during execution wrap exported sentinels you can inspect with errors.Is:
| Error | Meaning |
|---|---|
migrator.ErrMigrationNotFound | A recorded migration (e.g. during rollback or reset) is not registered |
migrator.ErrTransactionFailed | A migration's transaction failed to begin, commit, or roll back |
migrator.ErrTrackingTable | The migration tracking table could not be created or read; the message includes the driver's error, e.g. migration tracking table error: <cause> |
migrator.ErrLockTimeout | The cross-process migration lock was not acquired within the lock timeout; nothing was changed |
if err := m.Up(); err != nil {
if errors.Is(err, migrator.ErrTrackingTable) {
// handle tracking-table failure
}
if errors.Is(err, migrator.ErrLockTimeout) {
// another process is still migrating
}
log.Fatal(err)
}What's Next?
- Batch Tracking — understand how batch numbers work
- Concurrency and Locking — run migrations from several processes safely
- Rollback — undo migrations when needed