{"record":{"id":"108e15a76b8af000","repo":"gastownhall/beads","slug":"backfilling-wisp-dependencies-split-targets-for-mi","errorCode":null,"errorMessage":"backfilling wisp_dependencies split targets for migration 0053: %w","messagePattern":"backfilling wisp_dependencies split targets for migration 0053: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/schema/migration_repairs.go","lineNumber":650,"sourceCode":"\t\t\t\treturn fmt.Errorf(\"adding wisp_dependencies.%s for migration 0053: %w\", col.name, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tlegacyTarget, err := schemaColumnExists(ctx, db, \"wisp_dependencies\", \"depends_on_id\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checking wisp_dependencies.depends_on_id: %w\", err)\n\t}\n\tif !legacyTarget {\n\t\t// Nothing left to backfill from: either a prior pass already\n\t\t// finished (depends_on_id has since been dropped) or this database\n\t\t// never had the legacy column to begin with.\n\t\treturn nil\n\t}\n\n\tfor _, repair := range wispDependenciesSplitTargetBackfillSQL() {\n\t\tif _, err := db.ExecContext(ctx, repair); err != nil {\n\t\t\treturn fmt.Errorf(\"backfilling wisp_dependencies split targets for migration 0053: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc wispDependenciesSplitTargetColumns() []struct{ name, definition string } {\n\treturn []struct{ name, definition string }{\n\t\t{\"depends_on_issue_id\", \"VARCHAR(255) NULL\"},\n\t\t{\"depends_on_wisp_id\", \"VARCHAR(255) NULL\"},\n\t\t{\"depends_on_external\", \"VARCHAR(255) NULL\"},\n\t}\n}\n\nfunc wispDependenciesSplitTargetBackfillSQL() []string {\n\treturn []string{\n\t\t\"UPDATE wisp_dependencies SET depends_on_external = depends_on_id WHERE depends_on_external IS NULL AND depends_on_id LIKE 'external:%'\",\n\t\t\"UPDATE wisp_dependencies wd JOIN wisps w ON w.id = wd.depends_on_id SET wd.depends_on_wisp_id = wd.depends_on_id WHERE wd.depends_on_wisp_id IS NULL AND wd.depends_on_external IS NULL\",\n\t\t\"UPDATE wisp_dependencies wd JOIN issues i ON i.id = wd.depends_on_id SET wd.depends_on_issue_id = wd.depends_on_id WHERE wd.depends_on_issue_id IS NULL AND wd.depends_on_external IS NULL AND wd.depends_on_wisp_id IS NULL\",","sourceCodeStart":632,"sourceCodeEnd":668,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/migration_repairs.go#L632-L668","documentation":"Wraps a failed backfill UPDATE while copying legacy depends_on_id values into the split-target columns (depends_on_external / depends_on_wisp_id / depends_on_issue_id) as part of migration 0053 repair. This is critical to fix before migration 0005 runs: 0005 drops depends_on_id, after which the source data for an interrupted backfill is gone forever. The statements are idempotent (each updates only rows not yet filled), so a retry is safe.","triggerScenarios":"One of the four UPDATE statements in wispDependenciesSplitTargetBackfillSQL() fails — a JOIN to wisps/issues hits a corrupt/missing row, a constraint violation on the target column, lock contention, or server failure mid-statement.","commonSituations":"Interrupted first attempt left columns present but unpopulated and a retry now fails on data issues; wisps/issues tables missing rows referenced by depends_on_id; concurrent writers holding row locks on wisp_dependencies.","solutions":["Read the wrapped driver error; if it is a lock/timeout issue, retry when the conflicting writer is done — the backfill is scoped and idempotent.","DO NOT run or ignore migration 0005 until this backfill succeeds; depends_on_id is dropped by 0005 and the data would be unrecoverable.","Verify referenced rows exist: for depends_on_id values, check matching rows in wisps and issues tables.","Restore from Dolt history/backup if rows were corrupted, then re-run the repair before proceeding past 0005."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before letting migration 0005 run, verify the backfill completed\nvar unfilled int\ndb.QueryRowContext(ctx, `SELECT COUNT(*) FROM wisp_dependencies\n  WHERE depends_on_id IS NOT NULL AND depends_on_external IS NULL\n  AND depends_on_wisp_id IS NULL AND depends_on_issue_id IS NULL`).Scan(&unfilled)\n// unfilled > 0 with depends_on_id still present means backfill incomplete","typeGuard":"func backfillComplete(rowsFilled, rowsTotal int) bool { return rowsFilled == rowsTotal }","tryCatchPattern":"if err := ensureWispDependenciesSplitTargets(ctx, db); err != nil {\n    // DO NOT proceed to migration 0005; depends_on_id would be dropped\n    return fmt.Errorf(\"refusing to continue toward migration 0005: %w\", err)\n}","preventionTips":["Never skip past a failed backfill — migration 0005 drops depends_on_id and the data becomes unrecoverable","Back up the database (Dolt history/branch) before running repairs","Run backfill when no concurrent writers hold locks on wisp_dependencies","Verify referenced ids exist in wisps/issues before joining"],"tags":["database","migration","backfill","data-loss"],"backgroundTag":"migration-backfill-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}