{"record":{"id":"3f3e59080422f0e9","repo":"gastownhall/beads","slug":"adding-the-wisp-dependencies-id-primary-key-for-th","errorCode":null,"errorMessage":"adding the wisp_dependencies id primary key for the 0058 repair: %w","messagePattern":"adding the wisp_dependencies id primary key for the 0058 repair: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/wisp_dep_forward_repair.go","lineNumber":413,"sourceCode":"\t\treturn err\n\t}\n\tif !hasID {\n\t\tif _, err := db.ExecContext(ctx,\n\t\t\t\"ALTER TABLE wisp_dependencies ADD COLUMN id CHAR(36) NOT NULL DEFAULT (UUID()) PRIMARY KEY FIRST\"); err != nil {\n\t\t\treturn fmt.Errorf(\"adding wisp_dependencies.id for the 0058 repair: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// id survived a crash but its key did not (or the column predates the\n\t// key): add the key alone rather than re-adding the column.\n\thasPK, err := schemaHasPrimaryKey(ctx, db, wispDepTable)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !hasPK {\n\t\tif _, err := db.ExecContext(ctx, \"ALTER TABLE wisp_dependencies ADD PRIMARY KEY (id)\"); err != nil {\n\t\t\treturn fmt.Errorf(\"adding the wisp_dependencies id primary key for the 0058 repair: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// dedupeWispDepNaturalIdentity removes rows that would collide on the three\n// uk_* unique keys added next. Duplicates are reachable because the legacy\n// shape's composite primary key covers (issue_id, depends_on_id) -- so two rows\n// differing only in a column the COALESCE did not select were legal -- and\n// because the keyless window leaves writes unconstrained.\n//\n// The comparison is null-safe (<=>): the target columns are NULL for every\n// target kind a row does not use, and ordinary = would treat two identical\n// wisp-target rows as distinct because their NULL issue targets never compare\n// equal. MIN(id) is an arbitrary but deterministic survivor, which is the\n// property that matters -- it makes a resumed run pick the same row.\nfunc dedupeWispDepNaturalIdentity(ctx context.Context, db DBConn) error {\n\tif _, err := db.ExecContext(ctx, `","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/wisp_dep_forward_repair.go#L395-L431","documentation":"Wraps a failure when adding the primary key on the existing `id` column (`ALTER TABLE wisp_dependencies ADD PRIMARY KEY (id)`) in the crash-recovery path where the column survived but its key did not. The ADD PRIMARY KEY failed, leaving wisp_dependencies without a primary key and aborting the repair.","triggerScenarios":"ensureWispDepSurrogateKey finds id present but no PK, then the ADD PRIMARY KEY DDL errors — NULL ids from a partially backfilled crash, duplicate id values, privilege denial, or lock timeout.","commonSituations":"A prior pass was killed between ADD COLUMN and ADD PRIMARY KEY, leaving NULL or duplicate UUIDs in id; DB user lacking ALTER/INDEX privileges; concurrent writers blocking the ALTER.","solutions":["Check the wrapped cause: duplicates/NULLs in id → dedupe and backfill (UPDATE ... SET id = UUID() WHERE id IS NULL) before re-running the repair.","Remove duplicate rows first (the repair's dedupeWispDepNaturalIdentity handles natural-key collisions; fix id duplicates separately).","Grant ALTER/INDEX to the migration user.","Re-run `bd` so the guarded repair completes the missing PK step."],"exampleFix":"// before: ADD PRIMARY KEY (id) fails — NULL ids from a crashed pass\n// after: backfill then resume\nUPDATE wisp_dependencies SET id = UUID() WHERE id IS NULL OR id = '';\n-- then re-run: bd (resumes guarded repair)\nALTER TABLE wisp_dependencies ADD PRIMARY KEY (id);","handlingStrategy":"validation","validationCode":"// verify id values are unique and non-null before the PK step resumes\nvar dupes, nulls int\ndb.QueryRowContext(ctx, `SELECT COUNT(*) - COUNT(DISTINCT id), SUM(id IS NULL OR id = '') FROM wisp_dependencies`).Scan(&dupes, &nulls)\nif dupes > 0 || nulls > 0 {\n    log.Fatalf(\"id column not PK-ready: %d duplicates, %d nulls — backfill before repair\", dupes, nulls)\n}","typeGuard":null,"tryCatchPattern":"if err := repairWispDependenciesForwardShape(ctx, db); err != nil {\n    if strings.Contains(err.Error(), \"adding the wisp_dependencies id primary key\") {\n        // backfill NULL ids and remove duplicates, then re-run the guarded repair\n        return fmt.Errorf(\"id column not PK-ready after crashed pass: %w\", err)\n    }\n    return err\n}","preventionTips":["Do not kill the process between ADD COLUMN id and ADD PRIMARY KEY steps.","Pre-check id uniqueness/nullability after any crashed repair.","Grant ALTER/INDEX privileges to the migration user.","Run the repair to completion once started; it is guarded and resumable via re-run."],"tags":["database","schema-repair","migration","primary-key","ddl"],"backgroundTag":"schema-migration-ddl-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}