go-migrationGo Migration
Migrations
Documentation

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

go
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

main.go
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.

go
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.

main.go
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:

FieldTypeDescription
NamestringThe migration name
Indexint0-based index of the current migration
TotalintTotal number of migrations being executed
Durationtime.DurationTime taken for this migration
Directionstring"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:

ErrorMeaning
migrator.ErrMigrationNotFoundA recorded migration (e.g. during rollback or reset) is not registered
migrator.ErrTransactionFailedA migration's transaction failed to begin, commit, or roll back
migrator.ErrTrackingTableThe migration tracking table could not be created or read; the message includes the driver's error, e.g. migration tracking table error: <cause>
migrator.ErrLockTimeoutThe cross-process migration lock was not acquired within the lock timeout; nothing was changed
go
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?