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

  1. Check the wrapped cause (%w): lock conflicts → re-run after concurrent writers finish; access denied → fix grants.
  2. Stop other bd processes writing during the migration, then re-run the repair (statements are idempotent).
  3. Run the migration against a writable primary, not a replica.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/9b59ac54b6923e94. Report an issue: GitHub.