gastownhall/beads · critical
pre-existing dirty tables changed during schema migration: %
Error message
pre-existing dirty tables changed during schema migration: %s
What it means
MigrateUp verifies that no pre-existing dirty table changed during the migration pass by comparing post-pass table signatures to pre-pass snapshots. When one or more dirty tables differ, this plain error names them. It means the migration pass touched user data it was supposed to leave alone — a safety invariant violation — so the pass aborts before staging and committing the schema.
Source
Thrown at internal/storage/schema/schema.go:727
}
appliedIgnored, ignoredColumnAdded, err := ignoredSource.migrate(ctx, db, 0)
if err != nil {
return applied, fmt.Errorf("ignored migrations: %w", err)
}
if err := unstageIgnoredTables(ctx, db); err != nil {
return applied, fmt.Errorf("unstaging ignored migration tables: %w", err)
}
if applied == 0 && !backfilled && appliedIgnored == 0 && !mainColumnAdded && !ignoredColumnAdded {
return applied, nil
}
changedDirtyTables, err := changedDirtyTableSignatures(ctx, db, dirtyBeforeSignatures)
if err != nil {
return applied, fmt.Errorf("checking pre-existing dirty table diffs: %w", err)
}
if len(changedDirtyTables) > 0 {
return applied, fmt.Errorf("pre-existing dirty tables changed during schema migration: %s", strings.Join(changedDirtyTables, ", "))
}
staged, err := stageSchemaTables(ctx, db, dirtyBefore)
if err != nil {
return applied, fmt.Errorf("staging migrations: %w", err)
}
if !staged {
return applied, nil
}
if err := DrainCall(ctx, db, "CALL DOLT_COMMIT('-m', 'schema: apply migrations')"); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "nothing to commit") {
return applied, fmt.Errorf("committing migrations: %w", err)
}
}
return applied, nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Read the table names in the error and inspect what changed in them (dolt diff / dolt status) to determine whether the change was expected user work or migration fallout
- If the changes are legitimate user work, stage/commit or otherwise settle the dirty tables first, then re-run MigrateUp on a clean working set
- If a migration/backfill step wrongly wrote to user tables, capture the diff output and file/fix the migration bug before retrying
- Restore affected dirty tables from backup if migration writes corrupted them
- Re-run MigrateUp with no dirty tables present so the invariant check passes
Example fix
// before: upgrading with uncommitted user edits in dirty tables _, err := schema.MigrateUp(ctx, db) // error: dirty tables changed // after: commit or stash user work so no dirty tables exist pre-migration // (dolt add/commit or dolt reset first) _, err := schema.MigrateUp(ctx, db)
Defensive patterns
Strategy: validation
Validate before calling
// Before migrating, ensure the working set has no dirty (uncommitted) tables
rows, err := db.QueryContext(ctx, "SELECT table_name FROM dolt_status WHERE staged=false AND status='modified'")
if err != nil {
return err
}
defer rows.Close()
var dirty []string
for rows.Next() {
var t string
if err := rows.Scan(&t); err != nil {
return err
}
dirty = append(dirty, t)
}
if len(dirty) > 0 {
return fmt.Errorf("commit or reset dirty tables before migrating: %s", strings.Join(dirty, ", "))
} Type guard
// Narrow the typed dirty-tables error that the main-source guard returns
func asDirtyTables(err error) (tables []string, ok bool) {
var dte *schema.DirtyTablesError
if errors.As(err, &dte) {
return dte.Tables, true
}
return nil, false
} Try / catch
applied, err := schema.MigrateUp(ctx, db)
if err != nil {
if strings.Contains(err.Error(), "pre-existing dirty tables changed during schema migration:") {
// Invariant violation: user data changed. Capture the diff and
// DO NOT commit or reconcile; restore/fix before retry.
return fmt.Errorf("migration mutated dirty tables; restore from backup: %w", err)
}
return err
} Prevention
- Never run upgrades while users have uncommitted DML in the database — commit or stash first
- Diff the named tables immediately to determine whether changes were user work or migration fallout
- Keep pre-migration backups so dirty tables can be restored if a backfill/rekey writes to them
- Report migrations that touch user tables as bugs — this error signals an invariant violation, not routine behavior
When it happens
Trigger: Calling MigrateUp/MigrateUpWithLock on a database where dirty (unstaged, user-modified) tables exist and some migration/backfill/rekey step (e.g. rekeyDependencyIDs, rekeyAuxRowIDsAllPasses, ensureBackfilledCustomStatusesCustomTypes) modified rows in one of those tables, so changedDirtyTableSignatures returns a non-empty list.
Common situations: Users working directly in the database (uncommitted DML in dirty tables) while an upgrade runs, a bug in a backfill/rekey pass that writes to user tables, or a partially recovered database where dirty tables were left in an unexpected state by a previous crash — the diff then flags them and blocks the commit.
Related errors
- pending ignored schema migrations alter pre-existing dirty t
- failed to migrate credential keys: %w
- failed to update encrypted password for peer %s: %w
- failed to initialize schema: %w
- failed to rebuild pool after migration: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/10bab53ffb008540.
Report an issue: GitHub.