{"record":{"id":"7570fe99d02738ee","repo":"gastownhall/beads","slug":"pre-repair-for-migration-s-w","errorCode":null,"errorMessage":"pre-repair for migration %s: %w","messagePattern":"pre-repair for migration (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/schema.go","lineNumber":1666,"sourceCode":"\t\t// commit, so the repair would sit uncommitted in the working set while\n\t\t// the cursor row for this version was already committed -- a killed\n\t\t// process between this step and the pass's final commit would leave\n\t\t// history claiming the version applied while the repaired table's\n\t\t// change was never durably recorded, and the version-gated repair\n\t\t// hook cannot re-run to fix it (its version is no longer pending).\n\t\t// Snapshotting first makes repair-hook mutations count as this step's\n\t\t// own newly-dirtied work, so they land in the same atomic commit as\n\t\t// the migration and its cursor row.\n\t\tvar dirtyBeforeStep map[string]dirtyTableState\n\t\tif commitEachStep {\n\t\t\tdirtyBeforeStep, err = dirtyTables(ctx, db, true)\n\t\t\tif err != nil {\n\t\t\t\treturn count, fmt.Errorf(\"snapshotting dirty tables before %s: %w\", mf.name, err)\n\t\t\t}\n\t\t}\n\n\t\tif err := src.preMigrationRepair(ctx, db, mf.version); err != nil {\n\t\t\treturn count, fmt.Errorf(\"pre-repair for migration %s: %w\", mf.name, err)\n\t\t}\n\n\t\tfmt.Fprintf(stderr, \"Applying migration %04d: %s…\\n\", mf.version, humanMigrationName(mf.name))\n\t\tstart := time.Now()\n\t\tif err := execMigrationBody(ctx, db, string(data)); err != nil {\n\t\t\treturn count, fmt.Errorf(\"migration %s: %w\", mf.name, err)\n\t\t}\n\t\tsum := sha256.Sum256(data)\n\t\tcontentHash := hex.EncodeToString(sum[:])\n\t\tif _, err := db.ExecContext(ctx, \"INSERT IGNORE INTO \"+src.cursorTable+\" (version, content_hash) VALUES (?, ?)\", mf.version, contentHash); err != nil {\n\t\t\treturn count, fmt.Errorf(\"recording %s in %s: %w\", mf.name, src.cursorTable, err)\n\t\t}\n\t\tcount++\n\n\t\t// commitEachStep's DOLT_ADD/DOLT_COMMIT is the expensive, fallible\n\t\t// part of this step on the production embedded path. The \"done\" line\n\t\t// (and its timing) must land after that commit succeeds, not before\n\t\t// it: printing \"done\" and then hitting a commit error would show an","sourceCodeStart":1648,"sourceCodeEnd":1684,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L1648-L1684","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"-- before: repair fails when column already exists (drift)\nALTER TABLE dependencies ADD COLUMN id INTEGER;\n-- after: idempotent repair\nSET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS\n             WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='dependencies' AND COLUMN_NAME='id');\nSET @sql := IF(@col = 0, 'ALTER TABLE dependencies ADD COLUMN id INTEGER', 'SELECT 1');\nPREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;","handlingStrategy":"validation","validationCode":"// Verify the repair target is in the expected state before migrating.\nvar colCount int\nerr := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS\n  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dependencies' AND COLUMN_NAME = 'id'`).Scan(&colCount)\nif err != nil {\n    return fmt.Errorf(\"cannot inspect schema before repair: %w\", err)\n}\n// colCount == 0 means repair will add it; colCount == 1 means drift already applied it","typeGuard":"func columnExists(ctx context.Context, db DBConn, table, column string) (bool, error) {\n    var n int\n    err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS\n      WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,\n      table, column).Scan(&n)\n    return n > 0, err\n}","tryCatchPattern":"if err := src.preMigrationRepair(ctx, db, mf.version); err != nil {\n    var mysqlErr *mysql.MySQLError\n    if errors.As(err, &mysqlErr) && (mysqlErr.Number == 1060 || mysqlErr.Number == 1050) { // duplicate column/table\n        log.Printf(\"schema drift detected before migration %04d; reconcile manually\", mf.version)\n    }\n    return count, fmt.Errorf(\"pre-repair for migration %s: %w\", mf.name, err)\n}","preventionTips":["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"],"tags":["database","migration","schema-repair","ddl"],"backgroundTag":"pre-migration-repair-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}