Batch Tracking
Understand how go-migration uses batch numbers to group and track migration execution.
Batch Tracking
go-migration uses a batch numbering system to group migrations that were executed together. This is central to how rollbacks work.
How Batches Work
Every time you call m.Up(), all pending migrations that run in that call are assigned the same batch number. The batch number increments with each m.Up() call that executes at least one migration.
Example Scenario
Suppose you have four migrations and run them in two separate m.Up() calls:
First run — registers and runs two migrations:
m.Register("20240101000001_create_users_table", &migrations.CreateUsersTable{})
m.Register("20240115000001_create_posts_table", &migrations.CreatePostsTable{})
m.Up() // Both get batch 1Second run — adds two more migrations and runs again:
m.Register("20240101000001_create_users_table", &migrations.CreateUsersTable{})
m.Register("20240115000001_create_posts_table", &migrations.CreatePostsTable{})
m.Register("20240201000001_add_phone_to_users", &migrations.AddPhoneToUsers{})
m.Register("20240215000001_create_comments_table", &migrations.CreateCommentsTable{})
m.Up() // Only the two new ones run, both get batch 2The resulting tracking table looks like:
| Migration | Batch |
|---|---|
20240101000001_create_users_table | 1 |
20240115000001_create_posts_table | 1 |
20240201000001_add_phone_to_users | 2 |
20240215000001_create_comments_table | 2 |
The Migrations Table
go-migration automatically creates a migrations table in your database to track execution state. This table stores:
- Migration name — the unique name you passed to
m.Register() - Batch number — which batch the migration belongs to
- Applied time — when the migration ran (
created_at)
The table's DDL comes from the configured grammar (Grammar.CompileTrackingTable), so it uses each database's native syntax:
| Database | id column | created_at column |
|---|---|---|
| PostgreSQL | SERIAL PRIMARY KEY | TIMESTAMP |
| MySQL | INT AUTO_INCREMENT PRIMARY KEY | TIMESTAMP |
| SQLite | INTEGER PRIMARY KEY AUTOINCREMENT | DATETIME |
All three also have migration VARCHAR(255) NOT NULL UNIQUE and batch INTEGER NOT NULL. Queries against the table use the grammar's bind placeholders ($1 on PostgreSQL, ? on MySQL and SQLite). Without WithGrammar, the tracker falls back to PostgreSQL syntax, so always pass WithGrammar for MySQL and SQLite.
The name defaults to migrations and can be changed with migrator.WithTableName (or migration_table in the config file). It is always quoted, and it may be schema-qualified — WithTableName("app.migrations") keeps the table in schema app, which must already exist. See Tracking Table Name.
Each migration's row is inserted (and, on rollback, deleted) inside the migration's own transaction (or right after it succeeds, for migrations that disable transactions), so a crash cannot leave a migration applied but unrecorded.
Don't manually modify the migrations tracking table. Let go-migration manage it through its API methods.
Why Batches Matter
Batch numbers determine rollback behavior:
m.Rollback(0)rolls back all migrations in the last batch onlym.Rollback(n)rolls back the lastnmigrations regardless of batch
This means you can deploy multiple migrations together and roll them all back as a unit if something goes wrong.
What's Next?
- Rollback — undo migrations by batch or step count
- Migration Status — view the current state of all migrations