gastownhall/beads · critical
pending ignored schema migrations alter pre-existing dirty t
Error message
pending ignored schema migrations alter pre-existing dirty tables: %s
What it means
This is a deliberate, untyped mid-pass guard: pending ignored-source migrations would ALTER tables that were already dirty before MigrateUp started. Unlike the main-source guard (which returns typed *DirtyTablesError before any work), this fires after main migrations applied, so a lenient caller treating it as success would let a reconcile checkpoint a half-applied pass. The message lists the offending tables.
Source
Thrown at internal/storage/schema/schema.go:708
}
backfilled = backfilled || auxRekeyed
touchedIgnoredDirtyTables, err := ignoredSource.pendingMigrationDirtyTables(ctx, db, dirtyBeforeAll)
if err != nil {
return applied, fmt.Errorf("checking dirty tables against pending ignored migrations: %w", err)
}
if len(touchedIgnoredDirtyTables) > 0 {
// Deliberately a plain, untyped error (unlike the main-source guard
// above, which returns *DirtyTablesError): this check fires mid-pass,
// after the main-source migrations have already applied. A lenient
// caller (embeddeddolt's openReadOnlyCommand / openWorkingSetReconcile
// intents) skipping this and returning as if the open succeeded would
// let a reconcile commit checkpoint a half-applied migration pass.
// The ignored source also tracks bd-internal state (dolt_ignore'd
// tables like ignored_schema_migrations), not expected user data, so
// there is no dirty-commit recovery story to support here the way
// there is for the main-source guard (#4566 scope).
return applied, fmt.Errorf("pending ignored schema migrations alter pre-existing dirty tables: %s", strings.Join(touchedIgnoredDirtyTables, ", "))
}
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 {View on GitHub (pinned to 71377f2769)
Solutions
- Commit or revert the listed pre-existing changes in the working set, then re-run MigrateUp
- If the dirty rows are stale migration state from a crashed pass, restore the repo to the last clean Dolt commit and retry
- Do not swallow this error and continue opening - the pass is half-applied and a reconcile commit would checkpoint it
- Avoid hand-editing dolt_ignore'd bd-internal tables (e.g. ignored_schema_migrations)
Example fix
// before: lenient open that checkpoints a half-applied pass
if _, err := schema.MigrateUp(ctx, db); err != nil {
log.Warn(err) // proceed anyway -> corrupt reconcile commit
}
// after: treat the guard as fatal for reconcile opens
if _, err := schema.MigrateUp(ctx, db); err != nil {
return fmt.Errorf("refusing to open for reconcile: %w", err)
} Defensive patterns
Strategy: type-guard
Validate before calling
// Before upgrading, commit any pending changes so the guard cannot fire
_, _ = db.ExecContext(ctx, "CALL dolt_add('.')")
if _, err := db.ExecContext(ctx, "CALL dolt_commit('-m', 'pre-upgrade checkpoint')"); err != nil {
return fmt.Errorf("clean working set required before upgrade: %w", err)
} Type guard
// This error is deliberately untyped: detect it by message
func isIgnoredDirtyTablesErr(err error) bool {
return err != nil && strings.Contains(err.Error(),
"pending ignored schema migrations alter pre-existing dirty tables")
} Try / catch
applied, err := schema.MigrateUp(ctx, db)
if isIgnoredDirtyTablesErr(err) {
// half-applied pass: do NOT proceed/reconcile; commit or revert, then retry
return fmt.Errorf("upgrade blocked; clean working set and re-run: %w", err)
} Prevention
- Commit or stash all working-set changes before running bd upgrades
- Never write directly to bd-internal (dolt_ignore'd) tables
- Always retry MigrateUp after a failed pass instead of proceeding with the open
- For reconcile-style opens, fail closed on any MigrateUp error
When it happens
Trigger: Calling MigrateUp/MigrateUpWithLock while tables such as ignored-migration targets have uncommitted user/local changes AND pending ignored-source migrations touch those same tables.
Common situations: A user or process wrote to bd-internal dolt_ignore'd tables before upgrading; an interrupted earlier pass left those tables dirty; opening with reconcile intent on a working set that was never cleaned.
Related errors
- checking dirty tables against pending migrations: %w
- reading pre-migration dirty table diffs: %w
- checking dirty tables against pending ignored migrations: %w
- pre-existing dirty tables changed during schema migration: %
- failed to migrate credential keys: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/55eb414cada97dbb.
Report an issue: GitHub.