gastownhall/beads · error

collectConnected (issues): %w

Error message

collectConnected (issues): %w

What it means

This error wraps a failure from depRepo.ListByIssueIDs with Direction=DepDirectionBoth in collectConnectedIssues, which gathers both incoming and outgoing dependencies for the (expanded) id set to find connected issues surviving the delete. Called by both deleteMany (when UpdateTextReferences is set) and previewDelete; a failure propagates and aborts the whole operation.

Source

Thrown at internal/storage/domain/issue_delete.go:346

	if err != nil {
		return preview, err
	}
	preview.ConnectedIssues = connected
	return preview, nil
}

func (u *issueUseCaseImpl) collectConnectedIssues(
	ctx context.Context, allIDs []string, deletedSet map[string]bool,
) (map[string]*types.Issue, map[string]bool, error) {
	out := map[string]*types.Issue{}
	isWisp := map[string]bool{}
	if len(allIDs) == 0 {
		return out, isWisp, nil
	}

	issueRes, err := u.depRepo.ListByIssueIDs(ctx, allIDs, DepListOpts{Direction: DepDirectionBoth})
	if err != nil {
		return nil, nil, fmt.Errorf("collectConnected (issues): %w", err)
	}
	wispRes, err := u.depRepo.ListByIssueIDs(ctx, allIDs, DepListOpts{Direction: DepDirectionBoth, UseWispsTable: true})
	if err != nil && !dberrors.IsTableNotExist(err) {
		return nil, nil, fmt.Errorf("collectConnected (wisps): %w", err)
	}

	neighbors := map[string]bool{}
	accumulate := func(m map[string][]*types.Dependency) {
		for _, deps := range m {
			for _, d := range deps {
				for _, candidate := range [2]string{d.IssueID, d.DependsOnID} {
					if candidate == "" || deletedSet[candidate] {
						continue
					}
					neighbors[candidate] = true
				}
			}
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix the wrapped driver-level error (connectivity, timeout, locks).
  2. Retry the delete; dependency deletion by ID is idempotent, so re-runs are safe.
  3. Chunk the delete when the cascade id set is very large.
  4. Enable text-reference rewriting only when needed to skip this work path.
Defensive patterns

Strategy: retry

Validate before calling

// Check connectivity and bound the cascade before deleting with text rewriting:
if err := store.PingContext(ctx); err != nil {
    return fmt.Errorf("db unreachable: %w", err)
}
if params.UpdateTextReferences && len(ids) > maxBatch {
    return fmt.Errorf("delete batch too large for text rewriting: %d", len(ids))
}

Type guard

var dbErr *dberrors.DBError
if errors.As(err, &dbErr) {
    // classify driver failure before deciding retry vs abort
}

Try / catch

res, err := uc.DeleteIssues(ctx, ids, opts)
if err != nil && strings.Contains(err.Error(), "collectConnected (issues)") {
    // deletes are idempotent by id; safe to retry after transient failure
    return retryWithBackoff(func() error { _, e := uc.DeleteIssues(ctx, ids, opts); return e })
}

Prevention

When it happens

Trigger: DeleteIssues/DeleteWisps with text-reference rewriting enabled, or PreviewDelete, when the bidirectional dependency query on the main table fails: DB outage, timeout on large allIDs sets, scan error.

Common situations: Large cascades making allIDs huge and the bulk query heavy; connection drops mid-delete (after rows were already removed — see partial-apply risk); lock contention on dependencies.

Related errors


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