gastownhall/beads · error
deleting wisp_dependencies rows rejected by the final shape:
Error message
deleting wisp_dependencies rows rejected by the final shape: %w
What it means
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.
Source
Thrown at internal/storage/schema/wisp_dep_forward_repair.go:272
// the constraint been in force when the target went away, so it matches the
// state the table would be in had the window never existed.
//
// A zero-target row names nothing to be blocked on and is deleted outright, as
// 0058 does. Deletes are safe here, before the drop, because removing a row can
// never collide on the legacy key -- unlike the multi-target normalization,
// which must wait (see normalizeWispDepMultiTargetRows).
func deleteWispDepRowsRejectedByFinalShape(ctx context.Context, db DBConn) error {
statements := []string{
// FK orphans.
"DELETE wd FROM wisp_dependencies wd LEFT JOIN wisps w ON w.id = wd.issue_id WHERE w.id IS NULL",
"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",
"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",
// ck_wisp_dep_one_target: zero-target rows.
"DELETE FROM wisp_dependencies WHERE depends_on_issue_id IS NULL AND depends_on_wisp_id IS NULL AND depends_on_external IS NULL",
}
for _, stmt := range statements {
if _, err := db.ExecContext(ctx, stmt); err != nil {
return fmt.Errorf("deleting wisp_dependencies rows rejected by the final shape: %w", err)
}
}
return nil
}
// normalizeWispDepMultiTargetRows reduces a row naming more than one target to
// exactly one, so ck_wisp_dep_one_target can be added at the end.
//
// The precedence (external > wisp > issue) is 0058's and is not a choice: it is
// fixed by the delegate backfill's statement order in
// wispDependenciesSplitTargetBackfillSQL and pinned by
// TestWispDependenciesSplitTargetBackfillPrefersWispOverIssueThroughDoltCLI.
// Matching it keeps (repair -> 0058) equivalent to (0058 alone) on every
// population, which is the invariant that makes the repair auditable. Unlike a
// zero-target row, a multi-target row names real, resolvable targets -- just
// more than one -- so the lower-precedence columns are nulled and the row
// survives rather than being discarded.
//View on GitHub (pinned to 71377f2769)
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.
Example fix
// before: repair run while another bd process holds locks → lock wait timeout
// after: serialize repairs and retry
if err := repairWispDependenciesForwardShape(ctx, db); err != nil {
if isLockTimeout(err) { time.Sleep(5*time.Second); return repairWispDependenciesForwardShape(ctx, db) }
return err
} Defensive patterns
Strategy: retry
Validate before calling
// pre-check rows the cleanup will delete, to size the operation
_, 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`)
if err != nil { log.Printf("cannot pre-check cleanup set: %v", err) } Try / catch
for attempt := 0; attempt < 3; attempt++ {
err := repairWispDependenciesForwardShape(ctx, db)
if err == nil { break }
if strings.Contains(err.Error(), "deleting wisp_dependencies rows rejected by the final shape") && isLockTimeout(err) {
time.Sleep(time.Duration(attempt+1) * 5 * time.Second)
continue
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- pre-repair for migration %s: %w
- checking %s for the pre-0058 repair: %w
- checking %s.id for the pre-0058 repair: %w
- normalizing multi-target wisp_dependencies rows for the 0058
- dropping idx_wisp_dep_type_target for the 0058 repair: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/387d1e81a84c6192.
Report an issue: GitHub.