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

  1. Read the wrapped cause — it is the WispIDSetInTx DB error; address that directly
  2. If the caller already computed a wisp set, pass it in so the extra query is skipped
  3. Retry the transaction if the cause is transient (lock timeout, connection reset)
  4. 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

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


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