Transactions
Understand how go-migration wraps each migration in a transaction and how to opt out.
Transactions
By default, go-migration wraps each migration in a database transaction. If a migration fails, the transaction is rolled back so your database isn't left in a partially-migrated state.
Default Behavior
When m.Up() runs a migration, it:
- Begins a transaction
- Calls the migration's
Upmethod - Records the migration in the tracking table
- Commits the transaction
If the Up method returns an error or panics, the transaction is rolled back. The migration is not recorded, and subsequent migrations in the batch are not executed.
// Each migration runs in its own transaction automatically
if err := m.Up(); err != nil {
// The failed migration was rolled back
// Previously successful migrations in this batch remain applied
log.Fatal(err)
}Each migration gets its own transaction. If migration A succeeds but migration B fails, A remains applied and B is rolled back.
Disabling Transactions
Some database operations cannot run inside a transaction (for example, CREATE INDEX CONCURRENTLY in PostgreSQL). For these cases, implement the DisableTransaction() bool method on your migration struct and return true:
package migrations
import (
"github.com/gopackx/go-migration/pkg/schema"
)
type AddIndexConcurrently struct{}
func (m *AddIndexConcurrently) Up(s *schema.Builder) error {
// This runs outside a transaction
return s.Alter("users", func(bp *schema.Blueprint) {
bp.Index("email")
})
}
func (m *AddIndexConcurrently) Down(s *schema.Builder) error {
return s.Alter("users", func(bp *schema.Blueprint) {
bp.DropIndex("idx_users_email")
})
}
// DisableTransaction opts this migration out of transaction wrapping.
func (m *AddIndexConcurrently) DisableTransaction() bool { return true }When go-migration detects that a migration struct implements DisableTransaction() and it returns true, it skips the transaction wrapper and executes the migration directly.
Migrations that run without transactions cannot be automatically rolled back on failure. If the migration fails partway through, you may need to manually clean up. Use this only when necessary.
The TransactionOption Interface
The opt-out is detected via interface satisfaction:
type TransactionOption interface {
DisableTransaction() bool
}Any migration struct that implements this method and returns true will run outside a transaction. Returning false keeps the default transaction wrapping.
When to Disable Transactions
Common scenarios where you might need to disable transactions:
- Creating indexes concurrently (PostgreSQL)
- Altering enum types (PostgreSQL)
- Operations that require implicit commits (MySQL DDL)
- Long-running data migrations that exceed transaction timeout limits
Manual Transactions
Migration-level transactions are automatic, but sometimes you need all-or-nothing behavior in code that runs outside a migration — for example in a seeder that inserts related rows, or a data backfill. Instead of managing Begin/Commit/Rollback by hand, use the database.WithTransaction helper:
import "github.com/gopackx/go-migration/pkg/database"
err := database.WithTransaction(db, func(tx *sql.Tx) error {
if _, err := tx.Exec("INSERT INTO orders (user_id, total) VALUES ($1, $2)", userID, total); err != nil {
return err // anything non-nil rolls back the whole transaction
}
if _, err := tx.Exec("UPDATE users SET order_count = order_count + 1 WHERE id = $1", userID); err != nil {
return err
}
return nil // returning nil commits
})
if err != nil {
log.Fatal(err)
}It begins a transaction, runs your function, commits when it returns nil, and rolls back when it returns an error. If the commit fails, it attempts a rollback and returns a descriptive error. See the Connection Manager reference for the full signature.
What's Next?
- Running Migrations — how
m.Up()executes migrations - Rollback — undo migrations when something goes wrong