golang-migrate/migrate · error

database is dirty

Error message

database is dirty

What it means

ErrDatabaseDirty is a sentinel error returned by the sqlcipher driver when the schema_migrations table indicates a previous migration run did not complete (a record is missing a success/commit marker). The driver refuses to run further migrations to avoid operating on an inconsistently migrated schema. It is returned from WithInstance/Open (during RunMigrations) until the dirty state is resolved.

Source

Thrown at database/sqlcipher/sqlcipher.go:24

	"fmt"
	"io"
	nurl "net/url"
	"strconv"
	"strings"
	"sync/atomic"

	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database"
	_ "github.com/mutecomm/go-sqlcipher/v4"
)

func init() {
	database.Register("sqlcipher", &Sqlite{})
}

var DefaultMigrationsTable = "schema_migrations"
var (
	ErrDatabaseDirty  = fmt.Errorf("database is dirty")
	ErrNilConfig      = fmt.Errorf("no config")
	ErrNoDatabaseName = fmt.Errorf("no database name")
)

type Config struct {
	MigrationsTable string
	DatabaseName    string
	NoTxWrap        bool
}

type Sqlite struct {
	db       *sql.DB
	isLocked atomic.Bool

	config *Config
}

func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {

View on GitHub (pinned to 01a9643f14)

Solutions

  1. Inspect the schema to determine whether the last (partial) migration actually applied.
  2. If it did apply, manually set the migrations table row to clean: UPDATE schema_migrations SET dirty=false WHERE version=<version>;
  3. If it did not apply, manually drop the partially applied changes and the dirty row, then re-run migrate.Up.
  4. Restore the database from a backup taken before the failed migration, then re-run migrations.
  5. Never edit the migrations table while unsure of schema state — verify against the migration SQL first.

Example fix

// inspect then fix
// before
// panic: Dirty database version 5. Fix and force version.
// after (sqlite/sqlcipher shell)
// sqlite3 app.db "SELECT * FROM schema_migrations;"
// -- 5|1  (dirty)
// sqlite3 app.db "UPDATE schema_migrations SET dirty=0 WHERE version=5;"
Defensive patterns

Strategy: type-guard

Validate before calling

// before migrating
rows, err := db.Query("SELECT version, dirty FROM schema_migrations")
if err == nil {
    for rows.Next() {
        var v int; var d bool
        rows.Scan(&v, &d)
        if d { return fmt.Errorf("db dirty at version %d; resolve before migrating", v) }
    }
}

Type guard

func isDirtyErr(err error) bool {
    return errors.Is(err, sqlcipher.ErrDatabaseDirty)
}

Try / catch

if err := m.Up(); err != nil {
    if errors.Is(err, sqlcipher.ErrDatabaseDirty) {
        // halt pipeline; inspect schema and fix schema_migrations manually
        return fmt.Errorf("migration halted: dirty database: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling WithInstance or Open (then migrate.Up/Down) on a sqlcipher database whose migrations table contains a row with dirty=true — i.e., a previous migration was interrupted mid-run (process killed, crash, lost connection) leaving the schema in an unknown state.

Common situations: Migration process killed by Ctrl+C, SIGKILL, OOM, or deploy timeout halfway through an ALTER TABLE; power loss during migration; a partial migration in a container that was restarted; failed transaction rollback leaving a dirty flag.

Related errors


AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02). Data as JSON: /api/errors/b095a4fda816518d. Report an issue: GitHub.