gastownhall/beads · critical
adding dependencies.id for migration 0053
Error message
adding dependencies.id for migration 0053: %w
What it means
This wraps failure of `ALTER TABLE dependencies ADD COLUMN id CHAR(36) NULL` after the repair confirmed the id column is absent. This is a genuine DDL failure during migration 0053's repair; the column was about to be added and the statement was rejected or aborted. Because backfill and keying depend on this column, the whole repair stops here.
Solutions
- Read the wrapped error: ER_DUP_FIELDNAME means the column appeared concurrently — re-run the repair, it will skip the ADD and continue to backfill
- Grant ALTER privilege to the repair user
- Serialize repair execution across processes
- Handle lock-wait timeouts by running DDL in a quiet window or enabling online DDL
Example fix
// before: partial-state left after failed ALTER
runRepair(db) // crashes mid-0053
// after: retry safely — repair re-verifies each step
if err := runRepair(db); err != nil {
log.Printf("retrying repair: %v", err)
err = runRepair(db)
} Defensive patterns
Strategy: try-catch
Validate before calling
// confirm ALTER rights and no concurrent repair before starting
var ok int
_ = db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM information_schema.user_privileges WHERE grantee = CURRENT_USER() AND privilege_type = 'ALTER'").Scan(&ok)
if ok == 0 {
return errors.New("cannot run 0053 repair without ALTER privilege")
} Try / catch
err := repairV53RigAndSplitTargets(ctx, db)
if err != nil {
var drv *mysql.MySQLError
if errors.As(err, &drv) {
switch drv.Number {
case 1060: // column added by a concurrent repair — resume
return repairV53RigAndSplitTargets(ctx, db)
case 1205: // lock timeout
return fmt.Errorf("retry 0053 repair in quiet window: %w", err)
}
}
return err
} Prevention
- Acquire an exclusive lock before running migration repairs
- Ensure ALTER privilege on the dependencies table
- On ER_DUP_FIELDNAME, re-run the repair — it skips completed steps
- Back up the database before multi-step DDL repairs
When it happens
Trigger: ensureDependenciesIDColumn detects dependencies.id missing and the ALTER TABLE fails: missing ALTER privilege, concurrent repair adding the same column (ER_DUP_FIELDNAME), DDL/metadata lock timeout, or storage failure during the table rebuild.
Common situations: Two `bd` processes repairing one clone concurrently; repairs run with a data-only DB user; large dependencies tables triggering lock-wait timeouts on non-online-DDL servers.
Related errors
- adding wisps.is_blocked
- creating idx_wisps_is_blocked
- adding wisp_dependencies.
- checking dependencies.id
- checking idx_wisps_is_blocked index
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/fbbd5f2d0b2d4a2a.
Report an issue: GitHub.
Appendix: source
Thrown at internal/storage/schema/migration_repairs.go:435
// while the stale row survives. Restoring id as the PRIMARY KEY is what makes
// REPLACE's own conflict detection do its job.
//
// This is deliberately re-entrant rather than a single "column present ->
// nil" gate: preMigrationRepair's mutations to a synced table like
// dependencies land in the same atomic per-step commit as migration 0053
// (see runMigrations' dirty-table-snapshot ordering), but a process killed
// mid-repair -- after ADD COLUMN, before the backfill or the key finishes --
// still needs the NEXT open's repair call to finish the job rather than
// short-circuit on "column exists". Every step below re-verifies its own
// target state instead of trusting an earlier step ran to completion.
func ensureDependenciesIDColumn(ctx context.Context, db DBConn) error {
hasID, err := schemaColumnExists(ctx, db, "dependencies", "id")
if err != nil {
return fmt.Errorf("checking dependencies.id: %w", err)
}
if !hasID {
if _, err := db.ExecContext(ctx, "ALTER TABLE dependencies ADD COLUMN id CHAR(36) NULL"); err != nil {
return fmt.Errorf("adding dependencies.id for migration 0053: %w", err)
}
}
if err := backfillDependenciesID(ctx, db); err != nil {
return err
}
return ensureDependenciesIDPrimaryKey(ctx, db)
}
// backfillDependenciesID fills in any dependencies.id still NULL with
// depid.New(issue_id, target) -- the same deterministic id every insert path
// and the post-migration rekeyDependencyIDs pass use (dep_id_backfill.go) --
// so rows with real edges get a real, cross-clone-stable id rather than a
// throwaway placeholder, and rekeyDependencyIDs finds nothing left to correct
// afterwards. The `WHERE id IS NULL` scope (rather than every row) is what
// makes re-entry after a partial prior run cheap and idempotent: a row this
// function already backfilled, or one that already had an id, is untouched.
func backfillDependenciesID(ctx context.Context, db DBConn) error {View on GitHub (pinned to 71377f2769)