gastownhall/beads · error
pre-repair for migration %s: %w
Error message
pre-repair for migration %s: %w
What it means
This error wraps any failure returned by src.preMigrationRepair(ctx, db, mf.version), the hook that runs targeted schema repairs (e.g. ensureDependenciesIDColumn ALTERs) before applying migration mf.version (internal/storage/schema/schema.go:1666). The library throws it because a required pre-repair failing means the database is not in the shape the migration expects, and proceeding would risk a corrupt half-applied schema.
Source
Thrown at internal/storage/schema/schema.go:1666
// 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++
// commitEachStep's DOLT_ADD/DOLT_COMMIT is the expensive, fallible
// part of this step on the production embedded path. The "done" line
// (and its timing) must land after that commit succeeds, not before
// it: printing "done" and then hitting a commit error would show anView on GitHub (pinned to 71377f2769)
Solutions
- Read the wrapped error to see which repair statement failed and on which table
- Check schema drift: if the repair's change already exists (e.g. column present), drop the manual change or make the repair idempotent (IF NOT EXISTS / check-then-ALTER)
- Grant the migration user CREATE/ALTER/INDEX/DROP privileges
- Kill or wait out long-running queries holding metadata locks before re-running migrations
- Verify the pre-repair target table exists; if the DB predates it, apply the intervening migrations rather than skipping versions
Example fix
-- before: repair fails when column already exists (drift)
ALTER TABLE dependencies ADD COLUMN id INTEGER;
-- after: idempotent repair
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='dependencies' AND COLUMN_NAME='id');
SET @sql := IF(@col = 0, 'ALTER TABLE dependencies ADD COLUMN id INTEGER', 'SELECT 1');
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; Defensive patterns
Strategy: validation
Validate before calling
// Verify the repair target is in the expected state before migrating.
var colCount int
err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dependencies' AND COLUMN_NAME = 'id'`).Scan(&colCount)
if err != nil {
return fmt.Errorf("cannot inspect schema before repair: %w", err)
}
// colCount == 0 means repair will add it; colCount == 1 means drift already applied it Type guard
func columnExists(ctx context.Context, db DBConn, table, column string) (bool, error) {
var n int
err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
table, column).Scan(&n)
return n > 0, err
} Try / catch
if err := src.preMigrationRepair(ctx, db, mf.version); err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && (mysqlErr.Number == 1060 || mysqlErr.Number == 1050) { // duplicate column/table
log.Printf("schema drift detected before migration %04d; reconcile manually", mf.version)
}
return count, fmt.Errorf("pre-repair for migration %s: %w", mf.name, err)
} Prevention
- Make every pre-repair step idempotent (check-then-ALTER or IF NOT EXISTS where supported)
- Never hand-apply schema changes that a pending migration or its repair will make
- Ensure the migration user has ALTER/CREATE/INDEX/DROP on the target schema
- Stop application traffic or lock tables during migration to avoid metadata-lock timeouts
- Run the repair path against a staging copy of production to catch drift before the live window
When it happens
Trigger: Applying a migration whose pre-repair SQL fails: the ALTER/CREATE the repair performs errors due to missing privileges, a conflicting existing column/index, lock timeouts from concurrent traffic, or the table the repair targets does not exist in this deployment's schema state.
Common situations: App user lacks ALTER privilege for the repair DDL; schema drifted (column already added manually or by an old partial run); long-running queries hold metadata locks causing ALTER lock wait timeout; migrating a database created by a much older version missing tables the repair assumes.
Related errors
- dropping idx_wisp_dep_type_target for the 0058 repair: %w
- dropping %s for the 0058 repair: %w
- dropping the wisp_dependencies primary key for the 0058 repa
- dropping wisp_dependencies.depends_on_id for the 0058 repair
- adding wisp_dependencies.id for the 0058 repair: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7570fe99d02738ee.
Report an issue: GitHub.