gastownhall/beads · error

affected by delete for %s: %w

Error message

affected by delete for %s: %w

What it means

DeleteIssueInTx wraps a failure from AffectedByDeletionInTx — the query that computes which issues/wisps depend on the issue being deleted so their is_blocked flags can be recomputed. The wrapped error (%w) is the underlying SQL/query failure, not a 'not found' condition. The %s is the id of the issue whose deletion triggered the dependency scan.

Source

Thrown at internal/storage/issueops/delete.go:34

const deleteBatchSize = 50

// maxRecursiveResults is the safety limit for the total number of issues
// discovered during recursive dependent traversal.
const maxRecursiveResults = 10000

//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func DeleteIssueInTx(ctx context.Context, tx *sql.Tx, id string) error {
	isWisp := IsActiveWispInTx(ctx, tx, id)

	var deletedIssues, deletedWisps []string
	if isWisp {
		deletedWisps = []string{id}
	} else {
		deletedIssues = []string{id}
	}
	affectedIssues, affectedWisps, aerr := AffectedByDeletionInTx(ctx, tx, deletedIssues, deletedWisps)
	if aerr != nil {
		return fmt.Errorf("affected by delete for %s: %w", id, aerr)
	}

	// Edges are journaled before the rows go, while their source snapshots can
	// still be read.
	if err := RecordDependencyRemovalsForIssuesInTx(ctx, tx, []string{id}); err != nil {
		return fmt.Errorf("journal dependency removals for %s: %w", id, err)
	}
	if err := deleteIssueRowInTx(ctx, tx, id, isWisp); err != nil {
		return err
	}

	if err := RecomputeIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
		return fmt.Errorf("recompute is_blocked after delete for %s: %w", id, err)
	}

	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause (%w) — the error is only a wrapper; the root cause is in err.Unwrap()/the chain.
  2. Check that the caller's context was not canceled/expired before the delete; raise the timeout for large dependency graphs.
  3. Verify DB connectivity and transaction health (no prior error on the same tx — a failed statement can poison the tx).
  4. Retry the whole delete operation on a fresh transaction if the failure was transient (connection reset, deadlock).

Example fix

// before: reusing a long-lived context that expired mid-delete
err := storage.DeleteIssue(ctxWith5sTimeout, db, id)
// after: use a fresh, adequately-long context per delete
delCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := storage.DeleteIssue(delCtx, db, id)
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil { return fmt.Errorf("context already expired before delete: %w", err) }
if err := db.PingContext(ctx); err != nil { return fmt.Errorf("db unreachable before delete: %w", err) }

Try / catch

if err := storage.DeleteIssue(ctx, db, id); err != nil {
	if isTransient(err) { // net.Error, driver.ErrBadConn, lock timeout
		// retry once on a fresh tx + fresh context
	}
	return fmt.Errorf("delete %s: %w", id, err)
}

Prevention

When it happens

Trigger: Calling storage.DeleteIssue / DeleteIssueInTx when AffectedByDeletionInTx fails: the underlying tx is broken (rolled back, timeout, context canceled) or the dependency-plane query fails (connection dropped, lock timeout, driver error).

Common situations: Deleting an issue while the DB connection was interrupted mid-transaction; context deadline exceeded because the dependency scan hit a large dependency graph on a slow Dolt server; lock contention with a concurrent writer on dependencies tables.

Related errors


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