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 *DB

View on GitHub (pinned to 01a9643f14)

Solutions

  1. Query the migrations table (default 'SchemaMigrations') and inspect the last recorded version and dirty flag.
  2. Manually apply or revert the partial migration, then call migrate.Force(version) to clear the dirty flag.
  3. 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

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


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