gastownhall/beads · error
normalizing multi-target wisp_dependencies rows for the 0058
Error message
normalizing multi-target wisp_dependencies rows for the 0058 repair: %w
What it means
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.
Source
Thrown at internal/storage/schema/wisp_dep_forward_repair.go:303
// 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.
//
// This must run after the generated column and composite primary key are gone;
// see the ordering note in the file header for the collision it otherwise
// causes. It also manufactures duplicates by design -- a normalized row can
// land on a sibling's natural identity -- which is why the dedup step follows
// it rather than preceding it.
func normalizeWispDepMultiTargetRows(ctx context.Context, db DBConn) error {
statements := []string{
"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)",
"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",
}
for _, stmt := range statements {
if _, err := db.ExecContext(ctx, stmt); err != nil {
return fmt.Errorf("normalizing multi-target wisp_dependencies rows for the 0058 repair: %w", err)
}
}
return nil
}
// dropWispDepLegacyShape removes the generated column and everything built on
// it, in the order the shipped 0043 analog uses.
//
// The order is load-bearing in two places. idx_wisp_dep_type_target is indexed
// on depends_on_id and must go before the column. Every foreign key must go
// before DROP PRIMARY KEY, because the primary key is the only issue_id-leading
// index on this shape and fk_wisp_dep_issue holds it hostage:
//
// Error 1553 (HY000): can't drop index 'PRIMARY': needed in foreign key
// constraint fk_wisp_dep_issue
//
// Each drop is guarded on the live schema, so a resume after a crash mid-drop
// skips what is already gone rather than failing on a missing object.View on GitHub (pinned to 71377f2769)
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.
Example fix
// before: UPDATE on read-only replica → error
// after: ensure primary and writable
if _, err := db.ExecContext(ctx, "SELECT 1"); err == nil {
return repairWispDependenciesForwardShape(ctx, db) // run against primary
}
Defensive patterns
Strategy: retry
Validate before calling
// pre-check rows needing normalization
_, 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)`)
if err != nil { log.Printf("cannot pre-check multi-target rows: %v", err) } Try / catch
err := repairWispDependenciesForwardShape(ctx, db)
if err != nil && strings.Contains(err.Error(), "normalizing multi-target wisp_dependencies rows") {
time.Sleep(10 * time.Second) // wait out lock holders
err = repairWispDependenciesForwardShape(ctx, db)
}
return err Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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
- deleting wisp_dependencies rows rejected by the final shape:
- 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/9b59ac54b6923e94.
Report an issue: GitHub.