{"record":{"id":"631d6bbe791cae84","repo":"gastownhall/beads","slug":"deduplicating-wisp-dependencies-before-the-0058-re","errorCode":null,"errorMessage":"deduplicating wisp_dependencies before the 0058 repair: %w","messagePattern":"deduplicating wisp_dependencies before the 0058 repair: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/wisp_dep_forward_repair.go","lineNumber":445,"sourceCode":"// 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, `\n\t\tDELETE wd FROM wisp_dependencies wd\n\t\tJOIN (\n\t\t\tSELECT MIN(id) AS keep_id, issue_id, depends_on_issue_id, depends_on_wisp_id, depends_on_external\n\t\t\tFROM wisp_dependencies\n\t\t\tGROUP BY issue_id, depends_on_issue_id, depends_on_wisp_id, depends_on_external\n\t\t\tHAVING COUNT(*) > 1\n\t\t) dup\n\t\t  ON wd.issue_id = dup.issue_id\n\t\t AND wd.depends_on_issue_id <=> dup.depends_on_issue_id\n\t\t AND wd.depends_on_wisp_id <=> dup.depends_on_wisp_id\n\t\t AND wd.depends_on_external <=> dup.depends_on_external\n\t\tWHERE wd.id <> dup.keep_id\n\t`); err != nil {\n\t\treturn fmt.Errorf(\"deduplicating wisp_dependencies before the 0058 repair: %w\", err)\n\t}\n\treturn nil\n}\n\n// ensureWispDepFinalKeysAndConstraints completes the final shape. Each object\n// is added only if absent, so this finishes a partially-rebuilt table rather\n// than failing on a duplicate key name -- and re-running it on a converged\n// database does nothing at all.\nfunc ensureWispDepFinalKeysAndConstraints(ctx context.Context, db DBConn) error {\n\tfor _, k := range wispDepFinalKeys {\n\t\tpresent, err := schemaIndexExists(ctx, db, wispDepTable, k.name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif present {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err := db.ExecContext(ctx, \"ALTER TABLE wisp_dependencies \"+k.definition); err != nil {","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/wisp_dep_forward_repair.go#L427-L463","documentation":"This error wraps a SQL failure raised while deduplicating wisp_dependencies rows during the 0058 forward-shape schema repair. Before unique keys (uk_*) can be added, rows that collide on the natural identity (issue_id + null-safe depends_on columns) must be deleted; if the DELETE fails the repair aborts so it never adds keys over duplicate data.","triggerScenarios":"Running the wisp_dependencies forward repair (repairWispDependenciesForwardShape) on a legacy-shaped store where the DELETE wd FROM wisp_dependencies wd JOIN (...) dup statement fails — e.g. missing id column (dedupe requires the surrogate id added by ensureWispDepSurrogateKey first), SQL syntax/permission errors, or a corrupted/locked table.","commonSituations":"Upgrading a pre-0058 beads database whose wisp_dependencies table still has the legacy composite primary key and accumulated duplicate rows; interrupted prior migrations leaving the table half-repaired; database user lacking DELETE/ALTER privileges.","solutions":["Ensure ensureWispDepSurrogateKey ran first — verify the wisp_dependencies.id column and its PRIMARY KEY exist before dedupe","Run the migration/repair as a database user with DELETE and ALTER privileges on wisp_dependencies","Inspect the wrapped MySQL/Dolt error for the root cause (syntax, lock wait, unknown column) and fix that underlying issue","Back up the database, then re-run the repair; it is idempotent and resumes partially-repaired tables"],"exampleFix":"// before (fails: id column missing on legacy table)\ndbd upgrade  // dedupe fails with \"Unknown column 'id'\"\n// after\n// run the full repair path so ensureWispDepSurrogateKey adds id before dedupe,\n// or manually: ALTER TABLE wisp_dependencies ADD COLUMN id CHAR(36) NOT NULL DEFAULT (UUID()) PRIMARY KEY FIRST;","handlingStrategy":"validation","validationCode":"hasID, err := schemaColumnExists(ctx, db, \"wisp_dependencies\", \"id\")\nif err != nil || !hasID {\n    return errors.New(\"run surrogate-key step before dedupe\")\n}","typeGuard":null,"tryCatchPattern":"if err := repairWispDependenciesForwardShape(ctx, db); err != nil {\n    log.Printf(\"0058 repair dedupe failed, database unchanged: %v\", err)\n    return err\n}","preventionTips":["Always run migrations/repairs through the tool's own upgrade path, not ad-hoc SQL","Back up the database before schema repairs","Use a DB account with DELETE and ALTER privileges","Let the idempotent repair resume partially-repaired tables instead of hand-patching"],"tags":["database","migration","schema-repair","mysql"],"backgroundTag":"schema-migration-failure","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}