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
- Inspect the database state and manually verify/repair the partially applied migration (roll back or complete it)
- 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
- 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
- Run migrations under advisory locks so only one process migrates at a time
- Ensure migrations are transactional or idempotent so crashes cannot leave partial DDL
- Alert on dirty=true in the migrations table after any failed run
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
- database is dirty
- x-migrations-table must be quoted (for instance '"migrate"."
- unable to parse option x-multi-statement: %w
- "%s" MigrationsTable contains too many dot characters
- x-migrations-table must be quoted (for instance '"migrate"."
AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02).
Data as JSON: /api/errors/6d95f5dab14af29f.
Report an issue: GitHub.