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

  1. Inspect the wrapped cause (lock timeout vs connection loss) and address it
  2. Retry the deletion after concurrent transactions complete; the tx is atomic
  3. Check for long-running transactions holding locks on the dependencies table
  4. 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

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


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