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 an

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped error to see which repair statement failed and on which table
  2. 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)
  3. Grant the migration user CREATE/ALTER/INDEX/DROP privileges
  4. Kill or wait out long-running queries holding metadata locks before re-running migrations
  5. 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

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


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