gastownhall/beads · error
snapshotting dirty tables before %s: %w
Error message
snapshotting dirty tables before %s: %w
What it means
This error wraps a failure from dirtyTables(ctx, db, true) which snapshots the current state of dirty (uncommitted-adjacent) tables before each migration step, when commitEachStep is enabled (internal/storage/schema/schema.go:1661). The library throws it because per-step commits require knowing which tables were dirty BEFORE the pre-migration repair runs, so re-dirtied work can be folded into the migration's atomic commit; without a valid snapshot the step cannot proceed safely.
Source
Thrown at internal/storage/schema/schema.go:1661
// Snapshot the working set BEFORE the pre-migration repair runs, not
// just before the migration's own SQL. preMigrationRepair (below) can
// itself mutate synced tables (e.g. #4690's ensureDependenciesIDColumn
// ALTERs `dependencies`); snapshotting after it ran would misclassify
// that mutation as pre-existing dirt to exclude from this step's
// commit, so the repair would sit uncommitted in the working set while
// the cursor row for this version was already committed -- a killed
// process between this step and the pass's final commit would leave
// history claiming the version applied while the repaired table's
// change was never durably recorded, and the version-gated repair
// hook cannot re-run to fix it (its version is no longer pending).
// Snapshotting first makes repair-hook mutations count as this step's
// own newly-dirtied work, so they land in the same atomic commit as
// the migration and its cursor row.
var dirtyBeforeStep map[string]dirtyTableState
if commitEachStep {
dirtyBeforeStep, err = dirtyTables(ctx, db, true)
if err != nil {
return count, fmt.Errorf("snapshotting dirty tables before %s: %w", mf.name, err)
}
}
if err := src.preMigrationRepair(ctx, db, mf.version); err != nil {
return count, fmt.Errorf("pre-repair for migration %s: %w", mf.name, err)
}
fmt.Fprintf(stderr, "Applying migration %04d: %s…\n", mf.version, humanMigrationName(mf.name))
start := time.Now()
if err := execMigrationBody(ctx, db, string(data)); err != nil {
return count, fmt.Errorf("migration %s: %w", mf.name, err)
}
sum := sha256.Sum256(data)
contentHash := hex.EncodeToString(sum[:])
if _, err := db.ExecContext(ctx, "INSERT IGNORE INTO "+src.cursorTable+" (version, content_hash) VALUES (?, ?)", mf.version, contentHash); err != nil {
return count, fmt.Errorf("recording %s in %s: %w", mf.name, src.cursorTable, err)
}
count++View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped error for the exact SQL/connection failure
- Reconnect and re-run migrations — dirty-state snapshotting is safe to retry before a step
- Verify the DB user can read the dirty-tracking/bookkeeping tables used by dirtyTables
- Increase connection idle/hold timeouts for long migration runs and ensure keepalives
- Run against the primary, not a read-only replica, since snapshots may need current local state
Example fix
// before: single pooled connection dies mid-run
rows, err := db.QueryContext(ctx, snapshotSQL)
// after: validate/refresh the connection before snapshotting
if err := db.PingContext(ctx); err != nil {
return count, fmt.Errorf("db connection lost before snapshot: %w", err)
}
dirtyBeforeStep, err = dirtyTables(ctx, db, true) Defensive patterns
Strategy: retry
Validate before calling
// Ensure the connection is alive and dirty-tracking tables are readable before migrating.
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("connection not ready: %w", err)
}
probe, err := dirtyTables(ctx, db, true)
if err != nil {
return fmt.Errorf("dirty-table snapshot unavailable before migrate: %w", err)
}
_ = probe Type guard
func snapshotAvailable(ctx context.Context, db DBConn) bool {
_, err := dirtyTables(ctx, db, true)
return err == nil
} Try / catch
var dirtyBeforeStep map[string]dirtyTableState
for attempt := 0; attempt < 3; attempt++ {
dirtyBeforeStep, err = dirtyTables(ctx, db, true)
if err == nil {
break
}
if isConnectionError(err) { // check wrapped driver error codes
time.Sleep(backoff(attempt))
continue
}
return count, fmt.Errorf("snapshotting dirty tables: %w", err)
} Prevention
- Take a fresh snapshot of dirty state immediately before starting migrations
- Run migrations on a stable primary connection with adequate idle timeouts
- Schedule migrations in a maintenance window to avoid connection churn
- Grant the migration user read access to all bookkeeping tables used by dirtyTables
- Retry the whole run from scratch after a connection loss — per-step snapshots are pre-commit and safe to redo
When it happens
Trigger: Running migrations with commitEachStep=true when the dirtyTables query against the live database fails — the underlying snapshot query errors due to connection loss, permissions on bookkeeping/system tables, or a corrupted dirty-state tracking table.
Common situations: Connection dropped mid-migration (network blip, server restart, wait_timeout); DB user lacks rights to read the dirty-state bookkeeping tables; migrating against a replica or managed DB that restricts the snapshot queries; long-running migration window exceeding connection timeouts.
Related errors
- probing %s existence: %w
- no database connection available (%s)
- dolt server connection failed: %w
- failed to reach the workspace identity: %v
- failed to migrate credential keys: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/bcf0a093676449c0.
Report an issue: GitHub.