go-migrationGo Migration
Hooks
Documentation

Hooks

Execute custom logic before and after migrations using BeforeMigrate and AfterMigrate hooks in go-migration.

Hooks

go-migration lets you register hooks that run custom logic before and after migration execution. Hooks are useful for logging, sending notifications, clearing caches, or validating preconditions.

Two hooks are available:

  • m.BeforeMigrate() — runs before each migration is applied
  • m.AfterMigrate() — runs after each migration is applied

Hooks fire per migration, not once for the whole run. If a single m.Up() applies three migrations, each hook is invoked three times — once for each migration. The same applies to Rollback(), Reset(), Refresh(), and Fresh(), where hooks fire for each migration that runs.

BeforeMigrate

Register a function that runs before each migration executes. The callback receives the migration name and the direction ("up" or "down") and returns an error.

main.go
m.BeforeMigrate(func(name, direction string) error {
    log.Printf("Starting %s %s...", direction, name)
    return nil
})

If a BeforeMigrate hook returns an error, that migration is aborted and no further migrations run.

main.go
m.BeforeMigrate(func(name, direction string) error {
    if !isMaintenanceWindow() {
        return fmt.Errorf("migrations are only allowed during maintenance windows")
    }
    return nil
})

A BeforeMigrate hook error stops the migration process at the current migration. Migrations already applied in this run remain applied. Use this to enforce preconditions before each schema change.

AfterMigrate

Register a function that runs after each migration completes. The callback receives the migration name, the direction ("up" or "down"), and the duration it took, and returns an error.

main.go
m.AfterMigrate(func(name, direction string, duration time.Duration) error {
    log.Printf("Finished %s %s in %s", direction, name, duration)
    return nil
})

If an AfterMigrate hook returns an error, the error is ignored and execution continues. The schema change remains applied.

main.go
m.AfterMigrate(func(name, direction string, duration time.Duration) error {
    if err := notifySlack(fmt.Sprintf("%s %s applied", direction, name)); err != nil {
        return fmt.Errorf("failed to send Slack notification: %w", err)
    }
    return nil
})

AfterMigrate hook errors are ignored without rolling back. This is by design — the migration has already been applied and committed, so the hook failure is treated as a non-critical side effect.

Error Behavior Summary

HookSignatureOn ErrorMigrations
BeforeMigratefunc(name, direction string) errorAborts current migrationEarlier ones stay applied; current and later not executed
AfterMigratefunc(name, direction string, duration time.Duration) errorIgnoredAlready applied, not rolled back

Complete Example

Here's a full example using both hooks for logging and notifications:

main.go
package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/lib/pq"

    "github.com/gopackx/go-migration/pkg/migrator"
)

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)

    // Runs before each migration: log it and validate preconditions
    m.BeforeMigrate(func(name, direction string) error {
        log.Printf("[%s] %s starting at %s", direction, name, time.Now().Format(time.RFC3339))

        // Example: check a precondition before allowing this migration
        if err := db.Ping(); err != nil {
            return fmt.Errorf("database is not reachable: %w", err)
        }

        return nil
    })

    // Runs after each migration: log completion and send a notification
    m.AfterMigrate(func(name, direction string, duration time.Duration) error {
        log.Printf("[%s] %s completed in %s", direction, name, duration)

        // Example: send a notification (failure won't roll back the migration)
        if err := sendNotification(fmt.Sprintf("%s %s applied", direction, name)); err != nil {
            return fmt.Errorf("notification failed: %w", err)
        }

        return nil
    })

    // Register migrations...
    // m.Register("20240101000001_create_users_table", &CreateUsersTable{})

    if err := m.Up(); err != nil {
        log.Fatal(err)
    }
}

func sendNotification(message string) error {
    // Your notification logic (Slack, email, webhook, etc.)
    log.Println(message)
    return nil
}

Use Cases

  • Logging — record when each migration starts and finishes (with its name, direction, and duration) for audit trails
  • Notifications — send Slack or email alerts as migrations run in production
  • Precondition checks — verify the database is reachable or that a maintenance window is active before applying each change
  • Cache invalidation — clear application caches after schema changes

What's Next?