{"record":{"id":"9b59ac54b6923e94","repo":"gastownhall/beads","slug":"normalizing-multi-target-wisp-dependencies-rows-fo","errorCode":null,"errorMessage":"normalizing multi-target wisp_dependencies rows for the 0058 repair: %w","messagePattern":"normalizing multi-target wisp_dependencies rows for the 0058 repair: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/wisp_dep_forward_repair.go","lineNumber":303,"sourceCode":"// Matching it keeps (repair -> 0058) equivalent to (0058 alone) on every\n// population, which is the invariant that makes the repair auditable. Unlike a\n// zero-target row, a multi-target row names real, resolvable targets -- just\n// more than one -- so the lower-precedence columns are nulled and the row\n// survives rather than being discarded.\n//\n// This must run after the generated column and composite primary key are gone;\n// see the ordering note in the file header for the collision it otherwise\n// causes. It also manufactures duplicates by design -- a normalized row can\n// land on a sibling's natural identity -- which is why the dedup step follows\n// it rather than preceding it.\nfunc normalizeWispDepMultiTargetRows(ctx context.Context, db DBConn) error {\n\tstatements := []string{\n\t\t\"UPDATE wisp_dependencies SET depends_on_wisp_id = NULL, depends_on_issue_id = NULL WHERE depends_on_external IS NOT NULL AND (depends_on_wisp_id IS NOT NULL OR depends_on_issue_id IS NOT NULL)\",\n\t\t\"UPDATE wisp_dependencies SET depends_on_issue_id = NULL WHERE depends_on_external IS NULL AND depends_on_wisp_id IS NOT NULL AND depends_on_issue_id IS NOT NULL\",\n\t}\n\tfor _, stmt := range statements {\n\t\tif _, err := db.ExecContext(ctx, stmt); err != nil {\n\t\t\treturn fmt.Errorf(\"normalizing multi-target wisp_dependencies rows for the 0058 repair: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// dropWispDepLegacyShape removes the generated column and everything built on\n// it, in the order the shipped 0043 analog uses.\n//\n// The order is load-bearing in two places. idx_wisp_dep_type_target is indexed\n// on depends_on_id and must go before the column. Every foreign key must go\n// before DROP PRIMARY KEY, because the primary key is the only issue_id-leading\n// index on this shape and fk_wisp_dep_issue holds it hostage:\n//\n//\tError 1553 (HY000): can't drop index 'PRIMARY': needed in foreign key\n//\tconstraint fk_wisp_dep_issue\n//\n// Each drop is guarded on the live schema, so a resume after a crash mid-drop\n// skips what is already gone rather than failing on a missing object.","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/wisp_dep_forward_repair.go#L285-L321","documentation":"Wraps a failure when running the two UPDATE statements that normalize multi-target wisp_dependencies rows for the 0058 repair: clearing wisp/issue targets on external rows, and clearing the issue target on rows naming both a wisp and an issue. The final shape allows only one target per row, so these updates must succeed before constraints are applied.","triggerScenarios":"normalizeWispDepMultiTargetRows runs either UPDATE (external rows with extra targets; dual wisp+issue targets) and db.ExecContext errors — lock timeout, connection drop, insufficient privilege, or read-only server.","commonSituations":"Concurrent writers holding locks on wisp_dependencies; migration run against a read-only replica; very wide UPDATE on a large table timing out; Dolt transaction conflict during the repair.","solutions":["Check the wrapped cause (%w): lock conflicts → re-run after concurrent writers finish; access denied → fix grants.","Stop other bd processes writing during the migration, then re-run the repair (statements are idempotent).","Run the migration against a writable primary, not a replica.","For very large tables, run during a maintenance window to avoid lock/timeout pressure."],"exampleFix":"// before: UPDATE on read-only replica → error\n// after: ensure primary and writable\nif _, err := db.ExecContext(ctx, \"SELECT 1\"); err == nil {\n    return repairWispDependenciesForwardShape(ctx, db) // run against primary\n}\n","handlingStrategy":"retry","validationCode":"// pre-check rows needing normalization\n_, err := db.ExecContext(ctx, `SELECT COUNT(*) FROM wisp_dependencies WHERE depends_on_external IS NOT NULL AND (depends_on_wisp_id IS NOT NULL OR depends_on_issue_id IS NOT NULL)`)\nif err != nil { log.Printf(\"cannot pre-check multi-target rows: %v\", err) }","typeGuard":null,"tryCatchPattern":"err := repairWispDependenciesForwardShape(ctx, db)\nif err != nil && strings.Contains(err.Error(), \"normalizing multi-target wisp_dependencies rows\") {\n    time.Sleep(10 * time.Second) // wait out lock holders\n    err = repairWispDependenciesForwardShape(ctx, db)\n}\nreturn err","preventionTips":["Stop all bd instances and Dolt writers before upgrading schema.","Run migrations on a writable primary.","Schedule large-table UPDATEs in a maintenance window.","Re-run the repair after a failure — the UPDATEs are idempotent."],"tags":["database","schema-repair","migration","data-normalization"],"backgroundTag":"schema-repair-ddl-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}