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
- Inspect the wrapped cause for the concrete SQL error.
- Increase the context timeout — cascade closure over big graphs is the slowest step of a delete.
- Retry in a fresh transaction if it was a transient connection/lock failure.
- 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
- Size the context timeout to graph depth when using --cascade.
- Cascade in explicit batches for very large subgraphs.
- Check connectivity/locks before wide cascading deletes.
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
- affected by delete for %s: %w
- query dependents for batch from %s: %w
- getting issue %s: %w
- db: ChildCounterSQLRepository.NextChildID: probe parent tabl
- db: ChildCounterSQLRepository.NextChildID: read counter for
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/03ee72c9a37258c1.
Report an issue: GitHub.