golang-migrate/migrate · critical

database is dirty

Error message

database is dirty

What it means

ErrDatabaseDirty is a sentinel error returned by the ql driver when the migrations table shows a previous migration failed mid-run (a 'dirty' state with a non-null version but no successful flag). Migrate refuses to continue on a dirty database to avoid applying migrations on top of an unknown, partially applied state. The same sentinel is redeclared in the cassandra and mysql drivers with identical meaning.

Source

Thrown at database/ql/ql.go:23

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

	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database"
	_ "modernc.org/ql/driver"
)

func init() {
	database.Register("ql", &Ql{})
}

var DefaultMigrationsTable = "schema_migrations"
var (
	ErrDatabaseDirty  = fmt.Errorf("database is dirty")
	ErrNilConfig      = fmt.Errorf("no config")
	ErrNoDatabaseName = fmt.Errorf("no database name")
	ErrAppendPEM      = fmt.Errorf("failed to append PEM")
)

type Config struct {
	MigrationsTable string
	DatabaseName    string
}

type Ql 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 database state and manually verify/repair the partially applied migration (roll back or complete it)
  2. Set the migration version/dirty flag back to a clean known state, e.g. SET version_force_save or run migrator.Force(version) with the correct version
  3. Re-run migrations with the corrected state; add advisory locking (Lock/Unlock) to prevent concurrent migrator runs

Example fix

// before (stuck: dirty=true at version 5)
m.Run()
// after
m, _ := source.New(sourceURL, path)
d, _ := sql.Open(qlDsn)
mig, _ := migrate.NewWithDatabaseInstance(sourceURL, "ql", d)
mig.Force(5) // set version to 5, clears dirty
mig.Migrate(6) // re-apply migration 6 after manual fix
Defensive patterns

Strategy: validation

Validate before calling

// before running migrations
var version int
var dirty bool
row := db.QueryRowContext(ctx, "SELECT version, dirty FROM schema_migrations")
if err := row.Scan(&version, &dirty); err == nil && dirty {
    return fmt.Errorf("database dirty at version %d; repair manually before migrating", version)
}

Type guard

func isDirtyDatabaseErr(err error) bool {
    return errors.Is(err, ql.ErrDatabaseDirty)
}

Try / catch

if err := mig.Migrate(target); err != nil {
    if errors.Is(err, ql.ErrDatabaseDirty) {
        // inspect schema_migrations, fix partial state, then mig.Force(version)
    }
    return err
}

Prevention

When it happens

Trigger: Running migration steps when the schema_migrations table has a version recorded but dirty=true, typically after a previous migration process crashed or was killed mid-migration.

Common situations: Container or CI job killed during a long migration; network loss between client and database during DDL; running two migrator instances where one failed; manually editing the migrations table.

Related errors


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