{"record":{"id":"387d1e81a84c6192","repo":"gastownhall/beads","slug":"deleting-wisp-dependencies-rows-rejected-by-the-fi","errorCode":null,"errorMessage":"deleting wisp_dependencies rows rejected by the final shape: %w","messagePattern":"deleting wisp_dependencies rows rejected by the final shape: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/wisp_dep_forward_repair.go","lineNumber":272,"sourceCode":"// the constraint been in force when the target went away, so it matches the\n// state the table would be in had the window never existed.\n//\n// A zero-target row names nothing to be blocked on and is deleted outright, as\n// 0058 does. Deletes are safe here, before the drop, because removing a row can\n// never collide on the legacy key -- unlike the multi-target normalization,\n// which must wait (see normalizeWispDepMultiTargetRows).\nfunc deleteWispDepRowsRejectedByFinalShape(ctx context.Context, db DBConn) error {\n\tstatements := []string{\n\t\t// FK orphans.\n\t\t\"DELETE wd FROM wisp_dependencies wd LEFT JOIN wisps w ON w.id = wd.issue_id WHERE w.id IS NULL\",\n\t\t\"DELETE wd FROM wisp_dependencies wd LEFT JOIN wisps w ON w.id = wd.depends_on_wisp_id WHERE wd.depends_on_wisp_id IS NOT NULL AND w.id IS NULL\",\n\t\t\"DELETE wd FROM wisp_dependencies wd LEFT JOIN issues i ON i.id = wd.depends_on_issue_id WHERE wd.depends_on_issue_id IS NOT NULL AND i.id IS NULL\",\n\t\t// ck_wisp_dep_one_target: zero-target rows.\n\t\t\"DELETE FROM wisp_dependencies WHERE depends_on_issue_id IS NULL AND depends_on_wisp_id IS NULL AND depends_on_external IS NULL\",\n\t}\n\tfor _, stmt := range statements {\n\t\tif _, err := db.ExecContext(ctx, stmt); err != nil {\n\t\t\treturn fmt.Errorf(\"deleting wisp_dependencies rows rejected by the final shape: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// normalizeWispDepMultiTargetRows reduces a row naming more than one target to\n// exactly one, so ck_wisp_dep_one_target can be added at the end.\n//\n// The precedence (external > wisp > issue) is 0058's and is not a choice: it is\n// fixed by the delegate backfill's statement order in\n// wispDependenciesSplitTargetBackfillSQL and pinned by\n// TestWispDependenciesSplitTargetBackfillPrefersWispOverIssueThroughDoltCLI.\n// 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//","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/wisp_dep_forward_repair.go#L254-L290","documentation":"Wraps a failure when executing cleanup DELETE statements that remove wisp_dependencies rows the final 0058 shape would reject: rows pointing at nonexistent issues, and rows with zero targets. The DML failed against the database, aborting the repair before constraints are re-added. Leaving such rows would violate ck_wisp_dep_one_target / FKs added later.","triggerScenarios":"deleteWispDepRowsRejectedByFinalShape runs one of its DELETE statements (orphan depends_on_issue_id cleanup, zero-target cleanup) and db.ExecContext returns an error — lock timeout, FK-related error, connection drop, or insufficient privilege.","commonSituations":"Large tables hitting innodb lock_wait_timeout during migration; another process holding row locks on wisp_dependencies; read-only replica; Dolt server OOM/crash mid-repair.","solutions":["Read the wrapped cause (%w): lock timeout → re-run after the competing process finishes; denied → fix grants.","Ensure no other bd/Dolt process is concurrently writing wisp_dependencies, then re-run the repair.","Increase lock wait timeout (e.g. SET GLOBAL innodb_lock_wait_timeout) for big tables.","If the server died mid-repair, restart and re-run — steps are guarded and idempotent."],"exampleFix":"// before: repair run while another bd process holds locks → lock wait timeout\n// after: serialize repairs and retry\nif err := repairWispDependenciesForwardShape(ctx, db); err != nil {\n    if isLockTimeout(err) { time.Sleep(5*time.Second); return repairWispDependenciesForwardShape(ctx, db) }\n    return err\n}","handlingStrategy":"retry","validationCode":"// pre-check rows the cleanup will delete, to size the operation\n_, err := db.ExecContext(ctx, `SELECT COUNT(*) FROM wisp_dependencies wd LEFT JOIN issues i ON i.id = wd.depends_on_issue_id WHERE wd.depends_on_issue_id IS NOT NULL AND i.id IS NULL`)\nif err != nil { log.Printf(\"cannot pre-check cleanup set: %v\", err) }","typeGuard":null,"tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n    err := repairWispDependenciesForwardShape(ctx, db)\n    if err == nil { break }\n    if strings.Contains(err.Error(), \"deleting wisp_dependencies rows rejected by the final shape\") && isLockTimeout(err) {\n        time.Sleep(time.Duration(attempt+1) * 5 * time.Second)\n        continue\n    }\n    return err\n}","preventionTips":["Run the repair with exclusive ownership of the database (no concurrent bd writers).","Increase innodb_lock_wait_timeout for large cleanup sets.","Run on the writable primary, never a replica.","Back up wisp_dependencies before the repair so deleted rows are recoverable."],"tags":["database","schema-repair","migration","data-cleanup"],"backgroundTag":"schema-repair-ddl-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}