golang-migrate/migrate · critical
database is dirty
Error message
database is dirty
What it means
spanner.ErrDatabaseDirty indicates the Spanner migrations table holds a version row with dirty=true: a previous migration was interrupted after partially applying. The driver returns this on Open and stops, requiring a human to inspect the schema and reset the state via migrate.Force. It prevents silently building on an inconsistent schema.
Source
Thrown at database/spanner/spanner.go:39
adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
"google.golang.org/api/iterator"
)
func init() {
db := Spanner{}
database.Register("spanner", &db)
}
// DefaultMigrationsTable is used if no custom table is specified
const DefaultMigrationsTable = "SchemaMigrations"
// Driver errors
var (
ErrNilConfig = errors.New("no config")
ErrNoDatabaseName = errors.New("no database name")
ErrNoSchema = errors.New("no schema")
ErrDatabaseDirty = errors.New("database is dirty")
ErrLockHeld = errors.New("unable to obtain lock")
ErrLockNotHeld = errors.New("unable to release already released lock")
)
// Config used for a Spanner instance
type Config struct {
MigrationsTable string
DatabaseName string
// Whether to parse the migration DDL with spansql before
// running them towards Spanner.
// Parsing outputs clean DDL statements such as reformatted
// and void of comments.
CleanStatements bool
}
// Spanner implements database.Driver for Google Cloud Spanner
type Spanner struct {
db *DBView on GitHub (pinned to 01a9643f14)
Solutions
- Query the migrations table (default 'SchemaMigrations') and inspect the last recorded version and dirty flag.
- Manually apply or revert the partial migration, then call migrate.Force(version) to clear the dirty flag.
- Re-run migrations from a single serialized runner to avoid repeat interruptions.
Example fix
// before (migration keeps failing while dirty)
// after repairing schema state:
m, _ := migrate.New("spanner://projects/p/instances/i/databases/d", "file://migrations")
err := m.Force(7) // mark version 7 as applied, dirty=false Defensive patterns
Strategy: try-catch
Validate before calling
var dirty bool
err := client.Single().Query(ctx, spanner.Statement{SQL: "SELECT dirty FROM SchemaMigrations"}).Do(func(r *spanner.Row) error { return r.Column(0, &dirty) })
// resolve dirty state before running migrations if true Try / catch
if err := m.Up(); err != nil {
if errors.Is(err, spanner.ErrDatabaseDirty) {
// inspect SchemaMigrations, repair partial DDL, then:
if ferr := m.Force(lastGoodVersion); ferr != nil { return ferr }
return m.Up()
}
return err
} Prevention
- Run Spanner migrations from a single serialized job; Spanner DDL can be slow, so allow generous timeouts.
- Avoid killing deploy pods mid-migration (use graceful termination).
- Alert on ErrDatabaseDirty and keep a documented repair runbook (Force + manual DDL).
- Split large DDL batches into smaller migrations to shrink the dirty window.
When it happens
Trigger: A Spanner migration run crashed or timed out mid-statement; SetVersion was invoked with dirty=true and never resolved; concurrent migrate instances raced on the same database.
Common situations: Deployment killed during a long DDL batch (Spanner DDL can be slow), CI job cancelled mid-migration, network failure between applying statements and clearing the dirty flag.
Related errors
- database is dirty
- unable to obtain lock
- unable to release already released lock
- no config
- no database name
AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02).
Data as JSON: /api/errors/99c620692c9c5613.
Report an issue: GitHub.