gastownhall/beads · error

search union with counts (wisps): %w

Error message

search union with counts (wisps): %w

What it means

Same wrapper as the issues leg but for the wisps (ephemeral) side of the UNION ALL. The wisps leg uses wispsFilterTables and alias 'w'; any failure rendering that subquery is tagged 'wisps' so you know the ephemeral half failed, not the durable half. The leg carries its own parenthesized ORDER BY/LIMIT window, which is required syntax for pushing the window down into a UNION ALL.

Source

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

		}
		return finishSearchCountsPage(out, filter)
	}

	return r.searchUnionWithCounts(ctx, query, filter, wispDepsExist)
}

func (r *issueSQLRepositoryImpl) searchUnionWithCounts(ctx context.Context, query string, filter types.IssueFilter, wispDepsExist bool) (domain.SearchCountsPage, error) {
	outerOrderBy := unionOrderBySQL(filter.SortBy, filter.SortDesc)
	window := searchWindowForFilter(filter)
	legWindow := legWindowSQL(outerOrderBy, window)

	iSub, iArgs, err := r.buildUnionSubquery(query, filter, issuesFilterTables, "i", legWindow)
	if err != nil {
		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts (issues): %w", err)
	}
	wSub, wArgs, err := r.buildUnionSubquery(query, filter, wispsFilterTables, "w", legWindow)
	if err != nil {
		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts (wisps): %w", err)
	}

	// EACH LEG IS PARENTHESIZED, and it is not decoration. A leg that carries
	// its own ORDER BY and LIMIT (legWindowSQL) is a syntax error inside a bare
	// UNION ALL — the engine reads the clause as belonging to the union — so the
	// parentheses are what let the window be pushed down at all.
	//nolint:gosec // G201: subqueries built from hardcoded table names and ? placeholders.
	unionSQL := fmt.Sprintf("SELECT id, src FROM ((%s) UNION ALL (%s)) merged %s %s",
		iSub, wSub, outerOrderBy, window.sql)

	args := make([]any, 0, len(iArgs)+len(wArgs))
	args = append(args, iArgs...)
	args = append(args, wArgs...)

	rows, err := r.runner.QueryContext(ctx, unionSQL, args...)
	if err != nil {
		return domain.SearchCountsPage{}, fmt.Errorf("search union with counts: %w", err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped cause to identify the failing clause
  2. Remove or adjust filter fields unsupported for wisps, or set filter.Ephemeral=false / filter.SkipWisps to skip the wisp leg
  3. Verify wispsFilterTables covers every filter field your query uses
  4. Upgrade/rebuild if schema is stale (wisps tables vs code expectations)

Example fix

// before
filter := types.IssueFilter{IncludeEphemeral: true, SomeIssueOnlyField: x}
// after
filter.SkipWisps = true // or drop the issue-only field
Defensive patterns

Strategy: validation

Validate before calling

if includesWispLeg(filter) && requiresIssueOnlyColumns(filter) {
    return fmt.Errorf("filter field not supported for wisps; set SkipWisps or drop the field")
}

Try / catch

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

Prevention

When it happens

Trigger: searchUnionWithCounts building the wisp leg via buildUnionSubquery(query, filter, wispsFilterTables, "w", legWindow) fails — e.g. a filter field not renderable against wisp tables.

Common situations: Searching with a filter that assumes durable-issue-only columns (or vice versa) while wisps exist and are included; schema drift between issues and wisps filter table definitions.

Related errors


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