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

  1. 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
  2. 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
  3. If a migration/backfill step wrongly wrote to user tables, capture the diff output and file/fix the migration bug before retrying
  4. Restore affected dirty tables from backup if migration writes corrupted them
  5. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/10bab53ffb008540. Report an issue: GitHub.