{"record":{"id":"fbbd5f2d0b2d4a2a","repo":"gastownhall/beads","slug":"adding-dependencies-id-for-migration-0053-w","errorCode":null,"errorMessage":"adding dependencies.id for migration 0053: %w","messagePattern":"adding dependencies\\.id for migration 0053: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/schema/migration_repairs.go","lineNumber":435,"sourceCode":"// while the stale row survives. Restoring id as the PRIMARY KEY is what makes\n// REPLACE's own conflict detection do its job.\n//\n// This is deliberately re-entrant rather than a single \"column present ->\n// nil\" gate: preMigrationRepair's mutations to a synced table like\n// dependencies land in the same atomic per-step commit as migration 0053\n// (see runMigrations' dirty-table-snapshot ordering), but a process killed\n// mid-repair -- after ADD COLUMN, before the backfill or the key finishes --\n// still needs the NEXT open's repair call to finish the job rather than\n// short-circuit on \"column exists\". Every step below re-verifies its own\n// target state instead of trusting an earlier step ran to completion.\nfunc ensureDependenciesIDColumn(ctx context.Context, db DBConn) error {\n\thasID, err := schemaColumnExists(ctx, db, \"dependencies\", \"id\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checking dependencies.id: %w\", err)\n\t}\n\tif !hasID {\n\t\tif _, err := db.ExecContext(ctx, \"ALTER TABLE dependencies ADD COLUMN id CHAR(36) NULL\"); err != nil {\n\t\t\treturn fmt.Errorf(\"adding dependencies.id for migration 0053: %w\", err)\n\t\t}\n\t}\n\n\tif err := backfillDependenciesID(ctx, db); err != nil {\n\t\treturn err\n\t}\n\treturn ensureDependenciesIDPrimaryKey(ctx, db)\n}\n\n// backfillDependenciesID fills in any dependencies.id still NULL with\n// depid.New(issue_id, target) -- the same deterministic id every insert path\n// and the post-migration rekeyDependencyIDs pass use (dep_id_backfill.go) --\n// so rows with real edges get a real, cross-clone-stable id rather than a\n// throwaway placeholder, and rekeyDependencyIDs finds nothing left to correct\n// afterwards. The `WHERE id IS NULL` scope (rather than every row) is what\n// makes re-entry after a partial prior run cheap and idempotent: a row this\n// function already backfilled, or one that already had an id, is untouched.\nfunc backfillDependenciesID(ctx context.Context, db DBConn) error {","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/migration_repairs.go#L417-L453","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: partial-state left after failed ALTER\nrunRepair(db) // crashes mid-0053\n// after: retry safely — repair re-verifies each step\nif err := runRepair(db); err != nil {\n    log.Printf(\"retrying repair: %v\", err)\n    err = runRepair(db)\n}","handlingStrategy":"try-catch","validationCode":"// confirm ALTER rights and no concurrent repair before starting\nvar ok int\n_ = db.QueryRowContext(ctx,\n    \"SELECT COUNT(*) FROM information_schema.user_privileges WHERE grantee = CURRENT_USER() AND privilege_type = 'ALTER'\").Scan(&ok)\nif ok == 0 {\n    return errors.New(\"cannot run 0053 repair without ALTER privilege\")\n}","typeGuard":null,"tryCatchPattern":"err := repairV53RigAndSplitTargets(ctx, db)\nif err != nil {\n    var drv *mysql.MySQLError\n    if errors.As(err, &drv) {\n        switch drv.Number {\n        case 1060: // column added by a concurrent repair — resume\n            return repairV53RigAndSplitTargets(ctx, db)\n        case 1205: // lock timeout\n            return fmt.Errorf(\"retry 0053 repair in quiet window: %w\", err)\n        }\n    }\n    return err\n}","preventionTips":["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"],"tags":["database","ddl","mysql","alter-table","schema-migration"],"backgroundTag":"alter-table-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}