gastownhall/beads · error
delete wisp %s from dependencies: %w
Error message
delete wisp %s from dependencies: %w
What it means
Wraps a failed DELETE FROM dependencies WHERE depends_on_wisp_id = ? while DeleteWispFromDependenciesInTx removes all dependency edges that point at a deleted wisp. Called from deleteIssueRowInTx during issue deletion; a failure here aborts the deletion transaction so no dangling wisp references remain.
Source
Thrown at internal/storage/issueops/dependencies.go:581
SELECT ?
UNION
SELECT d.parent_id
FROM ancestors a
JOIN (%s) d ON d.issue_id = a.node
)
SELECT COUNT(*) FROM ancestors WHERE node = ?
`, strings.Join(unions, " UNION "))
var n int
if err := tx.QueryRowContext(ctx, query, node, candidate).Scan(&n); err != nil {
return false, err
}
return n > 0, nil
}
func DeleteWispFromDependenciesInTx(ctx context.Context, tx *sql.Tx, wispID string) error {
if _, err := tx.ExecContext(ctx,
"DELETE FROM dependencies WHERE depends_on_wisp_id = ?", wispID); err != nil {
return fmt.Errorf("delete wisp %s from dependencies: %w", wispID, err)
}
return nil
}
//nolint:gosec // G201: inClause contains only ? placeholders
func DeleteWispsFromDependenciesInTx(ctx context.Context, tx *sql.Tx, wispIDs []string) error {
if len(wispIDs) == 0 {
return nil
}
inClause, args := buildSQLInClause(wispIDs)
if _, err := tx.ExecContext(ctx,
fmt.Sprintf("DELETE FROM dependencies WHERE depends_on_wisp_id IN (%s)", inClause),
args...); err != nil {
return fmt.Errorf("delete wisps from dependencies: %w", err)
}
return nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause (lock timeout vs connection loss) and address it
- Retry the deletion after concurrent transactions complete; the tx is atomic
- Check for long-running transactions holding locks on the dependencies table
- Batch large deletions to avoid long transactions timing out
Example fix
// before
err := issueops.DeleteWispFromDependenciesInTx(ctx, tx, wispID)
// after: retry transient failures before aborting the whole delete
err := issueops.DeleteWispFromDependenciesInTx(ctx, tx, wispID)
if err != nil && isLockWaitTimeout(err) {
return retryWithBackoff(func() error {
return deleteIssueWithDeps(ctx, issueID)
})
} Defensive patterns
Strategy: retry
Validate before calling
// check the wisp has references and DB is reachable before deleting
refs := countDepsOnWisp(ctx, db, wispID)
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("db unreachable, defer delete of %s", wispID)
} Type guard
func isWispCleanupFailure(err error) bool {
return err != nil && strings.Contains(err.Error(), "delete wisp ") && strings.Contains(err.Error(), "from dependencies")
} Try / catch
err := deleteIssueRowInTx(ctx, tx, issueID)
if err != nil && isLockWaitTimeout(err) {
return retryWithBackoff(func() error { return deleteIssue(ctx, issueID) })
} Prevention
- Keep delete transactions short to avoid lock timeouts on dependencies
- Retry whole transactions — cleanup is atomic with the delete
- Watch for long-running concurrent transactions holding table locks
- Batch large deletions to reduce transaction duration
When it happens
Trigger: Deleting an issue whose wisp is referenced in the dependencies table when the DELETE fails — DB connection loss, lock held by a concurrent reader/writer, or transaction timeout on a large dependencies table.
Common situations: Deleting issues during a sync/import while other connections hold locks; Dolt conflicts between the delete and concurrent dependency writes; network blips mid-transaction.
Related errors
- delete wisp %s: %w
- delete issues: %w
- delete leases: %w
- recompute is_blocked after batch delete: %w
- delete old dependency target: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/8b30e8a5c8c037c8.
Report an issue: GitHub.