go-migrationGo Migration
Migrations
Documentation

Reset, Refresh & Fresh

Roll back all migrations, re-run them, or start from a clean slate with Reset, Refresh, and Fresh.

Reset, Refresh & Fresh

go-migration provides three methods for bulk migration operations beyond simple rollback. All three hold the cross-process migration lock for their whole duration.

m.Reset()

Rolls back all executed migrations in reverse order by calling each migration's Down method.

go
// Roll back every migration
if err := m.Reset(); err != nil {
    log.Fatal(err)
}

After Reset(), your database has no application tables — only the migrations tracking table remains empty.

Like Up() and Rollback(), Reset() runs the before-migrate hooks for every migration it rolls back (with direction "down"), followed by the after-migrate hooks, and emits progress events. A before-hook that returns an error stops the reset before that migration's Down runs.

Before v1.2.0, Reset() skipped the before-migrate hooks. If you rely on a before-hook to block "down" operations (for example in production), it now applies to Reset() and Refresh() too.

Reset() is destructive. All data in migrated tables will be lost. Use with caution in production.

m.Refresh()

Rolls back all migrations and then re-runs them. This is equivalent to calling m.Reset() followed by m.Up().

go
// Roll back all, then re-run all
if err := m.Refresh(); err != nil {
    log.Fatal(err)
}

Refresh() is useful when you want to rebuild your database schema from scratch while keeping the migration history clean. It executes every Down method in reverse, then every Up method in order. One migration lock is held across both phases, so no other process can run migrations in between.

When to Use Refresh

  • Rebuilding a development database after changing migration logic
  • Resetting test databases between test suites
  • Verifying that all Down methods work correctly

m.Fresh()

Drops all tables in the database and then runs all migrations from scratch, in batch 1. Unlike Refresh(), it does not call Down methods — it simply drops everything. The migration lock is acquired before anything is dropped and held until the migrations have run.

go
// Drop all tables, then run all migrations
if err := m.Fresh(); err != nil {
    log.Fatal(err)
}

Fresh() requires a grammar to be configured so it can list and drop the tables. Construct the migrator with WithGrammar, e.g. migrator.New(db, migrator.WithGrammar(&grammars.PostgresGrammar{})). Without a grammar, Fresh() returns an error.

Fresh() drops every table in the default schema, including the migrations tracking table. This is the most destructive operation available. Never use it on a production database.

How Fresh Works on Each Database

Since v1.2.0, Fresh() works on every supported database (it was previously PostgreSQL-only):

DatabaseWhat Fresh() does
PostgreSQLDROP SCHEMA public CASCADE followed by CREATE SCHEMA public. Everything else in the public schema (views, sequences, functions, types) is dropped too, and privileges granted on the schema are reset.
MySQLLists the base tables of the current database and drops each one with SET FOREIGN_KEY_CHECKS = 0 on the same connection.
SQLiteLists the tables in sqlite_master and drops each one with PRAGMA foreign_keys = OFF on the same connection.

The table list comes from the grammar's CompileListTables() query and the statements from CompileDropAllTables(tables); they run one by one on a single dedicated connection. The connection used for the MySQL/SQLite drops is closed afterwards rather than returned to the pool, so the disabled foreign key checks never leak into later queries.

Only the default schema (PostgreSQL public, the current MySQL database, the SQLite main database) is cleared. If the tracking table is schema-qualified (WithTableName("app.migrations")), Fresh() also drops that table explicitly, so every migration runs again in batch 1; other tables in that schema are left alone.

Dry-Run Fresh

With migrator.WithDryRun(w) (or migrate:fresh --dry-run), Fresh() runs the read-only list query, prints the DROP statements it would execute under a -- Fresh: drop all tables header, and then prints the SQL of every registered migration as pending in batch 1 — what a real Fresh() would run. Nothing is dropped or written. As with any dry-run, the tracking table must already exist (run migrate:install first).

Fresh vs Refresh

Refresh()Fresh()
Calls Down methodsYesNo
Drops all tablesNo (only what Down reverses)Yes (everything)
Requires working Down methodsYesNo
Use caseRebuild with rollback verificationClean slate, ignore Down logic

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()

    // WithGrammar is required for Fresh().
    m := migrator.New(db, migrator.WithGrammar(&grammars.PostgresGrammar{}))

    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)
    }

    // Option 1: Roll back everything
    if err := m.Reset(); err != nil {
        log.Fatal(err)
    }

    // Option 2: Roll back everything, then re-run
    if err := m.Refresh(); err != nil {
        log.Fatal(err)
    }

    // Option 3: Drop all tables, then run from scratch
    if err := m.Fresh(); err != nil {
        log.Fatal(err)
    }
}

What's Next?