gastownhall/beads · error

search union with counts (hydrate wisps): %w

Error message

search union with counts (hydrate wisps): %w

What it means

Same as the issues hydration wrapper but for wisp IDs. Unlike issues, a missing optional wisp table is tolerated (missingOptionalWispTable); only real failures other than 'table missing' are wrapped and returned. Indicates the wisp-side counts mega-query failed for a non-missing-table reason.

Source

Thrown at internal/storage/domain/db/issue_search_counts.go:107

		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts: %w", err)
	}
	page, err := scanIDSrcPage(rows)
	if err != nil {
		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts: %w", err)
	}
	page.sortGoSide(filter.SortBy, filter.SortDesc)
	hasMore, err := page.finishWindow(window)
	if err != nil {
		return domain.SearchCountsPage{}, err
	}

	issuesByID, err := r.fetchCountsByIDs(ctx, page.issueIDs, issuesFilterTables, wispDepsExist, hydrationFor(filter))
	if err != nil {
		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts (hydrate issues): %w", err)
	}
	wispsByID, err := r.fetchCountsByIDs(ctx, page.wispIDs, wispsFilterTables, true, hydrationFor(filter))
	if err != nil && !missingOptionalWispTable(err) {
		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts (hydrate wisps): %w", err)
	}

	out := reassembleBySrc(page.ordered, issuesByID, wispsByID)
	return domain.SearchCountsPage{Items: out, HasMore: hasMore}, nil
}

// hydrationFor reads the hydration opt-outs off a search filter, matching the
// store-backed path's helper of the same name (internal/storage/issueops).
// Both bodies must read the same fields off the same filter or a caller that
// set one would get different columns from the two backends.
func hydrationFor(filter types.IssueFilter) sqlbuild.CountsHydration {
	return sqlbuild.CountsHydration{SkipLabels: filter.SkipLabels, SkipCounts: filter.SkipCounts, Lite: filter.Lite}
}

// readyHydrationFor is the WORK-filter twin, matching
// issueops.readyHydrationFor. See it for why a ready filter carries Lite and
// neither of the other two.
func readyHydrationFor(filter types.WorkFilter) sqlbuild.CountsHydration {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the inner wrapped error; if it IS a missing-table error this path would have succeeded, so look for a different cause
  2. Run migrations/doctor to reconcile wisp schema
  3. Use filter.Ephemeral=true to query wisps alone, or SkipWisps to avoid the leg
  4. Retry transient network/cancellation failures
Defensive patterns

Strategy: fallback

Validate before calling

if !wispTablesExist(ctx, db) {
    filter.SkipWisps = true
}

Try / catch

page, err := store.SearchWithCounts(ctx, q, filter)
if err != nil && strings.Contains(err.Error(), "hydrate wisps") {
    filter.SkipWisps = true // tolerate wisp hydration failure
    page, err = store.SearchWithCounts(ctx, q, filter)
}

Prevention

When it happens

Trigger: fetchCountsByIDs on page.wispIDs fails with an error that is not the tolerated missing-wisp-table sentinel — e.g. engine error, cancellation, or malformed counts query for wisp tables.

Common situations: wisp_dependencies exists but another wisp-related table is missing/incompatible, hydration of a large wisp page times out, or connection drop mid-hydration.

Related errors


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