gastownhall/beads · error

expand cascade: %w

Error message

expand cascade: %w

What it means

ResolveDeletionSetInTx wraps a failure from FindAllDependentsInTx — the transitive-closure query that expands the requested ids into all dependents when cascade=true. Without this expansion, a cascading delete would orphan dependent issues, so the operation refuses to proceed.

Source

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

	// read or a citation rewrite to, and what nothing else may recompute.
	All []string
}

// ResolveDeletionSetInTx decides WHICH rows a delete removes: the named ids,
// plus — under cascade — the transitive closure of everything that depends on
// them, in BOTH planes.
//
// THE CASCADE IS ROOTED AT EVERY NAMED ID, WISPS INCLUDED. Rooting it at the
// durable half is what made `bd wisp gc` (which hardcodes cascade) silently
// under-delete. It does not read the caller's slice destructively either: the
// non-cascade set is a copy, because DeleteRequest promises IDs is never
// sorted in place.
func ResolveDeletionSetInTx(ctx context.Context, tx DBTX, ids []string, cascade bool) (DeletionSet, error) {
	all := append([]string(nil), ids...)
	if cascade {
		closure, err := FindAllDependentsInTx(ctx, tx, ids)
		if err != nil {
			return DeletionSet{}, fmt.Errorf("expand cascade: %w", err)
		}
		all = workapi.SortedDeleteIDs(closure)
	}
	if len(all) == 0 {
		return DeletionSet{}, nil
	}
	wispIDs, regularIDs, err := PartitionWispIDsInTx(ctx, tx, all)
	if err != nil {
		return DeletionSet{}, fmt.Errorf("partition delete ids: %w", err)
	}
	return DeletionSet{WispIDs: wispIDs, RegularIDs: regularIDs, All: all}, nil
}

func DeleteIssuesInTx(ctx context.Context, tx *sql.Tx, ids []string, cascade bool, force bool, dryRun bool) (*types.DeleteIssuesResult, error) {
	if len(ids) == 0 {
		return &types.DeleteIssuesResult{}, nil
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause for the concrete SQL error.
  2. Increase the context timeout — cascade closure over big graphs is the slowest step of a delete.
  3. Retry in a fresh transaction if it was a transient connection/lock failure.
  4. Run without --cascade and delete in explicit batches to shrink the closure size.

Example fix

// before
bd delete bd-1 bd-2 --cascade // times out on a huge graph
// after: larger timeout
bd delete bd-1 bd-2 --cascade // with BEADS_DB_TIMEOUT=120s or a fresh context in code
delCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
Defensive patterns

Strategy: validation

Validate before calling

// pre-check graph depth cheaply before a cascade
direct, err := externalDependentsOf(ctx, db, ids...)
if err != nil { return err }
if len(direct) > 1000 { /* use an extended timeout or delete in batches */ }

Try / catch

res, err := storage.DeleteIssues(ctx, db, ids, true /*cascade*/, false, false)
if err != nil {
	if errors.Is(err, context.DeadlineExceeded) {
		// retry with larger timeout or without cascade, in batches
	}
	return err
}

Prevention

When it happens

Trigger: bd delete --cascade (or DeleteIssuesInTx/DeleteInTx with cascade=true) when the recursive dependents query fails: context timeout on a deep/large dependency graph, connection loss, or SQL error on the dependency planes.

Common situations: Cascading a delete across a large multi-layer dependency graph that exceeds the statement timeout; lock contention while another writer mutates dependencies; stale schema missing dependency tables.

Related errors


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