gastownhall/beads · critical

adding dependencies.id for migration 0053: %w

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.

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)

Solutions

  1. 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
  2. Grant ALTER privilege to the repair user
  3. Serialize repair execution across processes
  4. 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

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


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