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

  1. Read the wrapped cause (%w): lock timeout → re-run after the competing process finishes; denied → fix grants.
  2. Ensure no other bd/Dolt process is concurrently writing wisp_dependencies, then re-run the repair.
  3. Increase lock wait timeout (e.g. SET GLOBAL innodb_lock_wait_timeout) for big tables.
  4. 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

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


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