gastownhall/beads · error
get issues by IDs: build wisp set: %w
Error message
get issues by IDs: build wisp set: %w
What it means
Wraps an error from WispIDSetInTx while GetIssuesByIDsInTx builds the set of wisp IDs needed to partition requested IDs into wisps vs permanent issues. The library throws it so callers of the batch-ID fetch know resolution failed before any partitioning or row queries ran.
Source
Thrown at internal/storage/issueops/dependencies.go:1010
// table (issues/wisps). Uses batched IN clauses.
//
// wispSet is an optional pre-built set of active wisp IDs scoped to
// cover ids (see WispIDSetInTx). Pass nil to have the helper build
// a scoped set internally; callers hydrating multiple batches inside
// one tx can build the set once over the union of their IDs and
// reuse it across calls.
//
//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func GetIssuesByIDsInTx(ctx context.Context, tx DBTX, ids []string, wispSet map[string]struct{}) ([]*types.Issue, error) {
if len(ids) == 0 {
return nil, nil
}
if wispSet == nil {
var err error
wispSet, err = WispIDSetInTx(ctx, tx, ids)
if err != nil {
return nil, fmt.Errorf("get issues by IDs: build wisp set: %w", err)
}
}
// Partition IDs by wisp status.
wispIDs, permIDs := partitionByWispSet(ids, wispSet)
var allIssues []*types.Issue
for _, pair := range []struct {
table string
labelTbl string
ids []string
}{
{"issues", "labels", permIDs},
{"wisps", "wisp_labels", wispIDs},
} {
if len(pair.ids) == 0 {
continue
}View on GitHub (pinned to 71377f2769)
Solutions
- Read the wrapped cause — it is the WispIDSetInTx DB error; address that directly
- If the caller already computed a wisp set, pass it in so the extra query is skipped
- Retry the transaction if the cause is transient (lock timeout, connection reset)
- Confirm wisp tables exist and migrations ran after upgrading
Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight: verify the DB is reachable before batch fetch
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("db unreachable: %w", err)
}
// Optionally pre-compute and pass wispSet so WispIDSetInTx is skipped:
wispSet, err := WispIDSetInTx(ctx, tx, ids)
if err != nil { return err }
issues, err := GetIssuesByIDsInTx(ctx, tx, ids, wispSet) Try / catch
issues, err := GetIssuesByIDsInTx(ctx, tx, ids, nil)
if err != nil && strings.Contains(err.Error(), "build wisp set") && isTransientDBError(errors.Unwrap(err)) {
issues, err = GetIssuesByIDsInTx(ctx, tx, ids, nil) // one retry
} Prevention
- Pass a precomputed wispSet when the caller already has one to skip the extra query
- Keep transactions alive and connections healthy (pool health checks)
- Run migrations so wisp tables exist before calling ID-batch fetches
- Cap batch size to avoid long-running transactions
When it happens
Trigger: GetIssuesByIDsInTx called with a nil wispSet (all its callers: GetBlockedIssuesInTx, DeleteInTx, deleteNeighborsInTx, GetDependenciesWithMetadataInTx, GetDependentsWithMetadataInTx, GetDependenciesInTx) and WispIDSetInTx fails querying wisp membership for the given IDs.
Common situations: DB connection failure or query timeout while probing wisp_dependencies/wisp tables, transaction aborted by a prior statement, or schema mismatch after version upgrades.
Related errors
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
- failed to begin transaction: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/2832b6efeedcdab0.
Report an issue: GitHub.