gastownhall/beads · error

search union with counts (issues): %w

Error message

search union with counts (issues): %w

What it means

This error wraps any failure from buildUnionSubquery when constructing the issues-side leg of the issues+wisps UNION ALL search-with-counts query. buildUnionSubquery assembles the per-leg SELECT (filter clauses, window, ORDER BY/LIMIT) and can fail when filter clauses cannot be rendered for the issues filter tables. The wrapper tags the leg so callers can tell which half of the union broke.

Source

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

	if empty || !wispDepsExist {
		out, err := r.runFilterSearchQuery(ctx, query, filter, issuesFilterTables, wispDepsExist)
		if err != nil {
			return domain.SearchCountsPage{}, err
		}
		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...)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause (%w) to see which filter clause failed to build
  2. Simplify or correct the IssueFilter fields (sort, status, labels, etc.) that the cause names
  3. Set filter.SkipWisps or use the non-union path if wisps are irrelevant
  4. If the failure is on an internal alias/clause, check buildIssueFilterClauses for unsupported qualifiers

Example fix

// before
filter := types.IssueFilter{SortBy: "weird_field"}
// after
filter := types.IssueFilter{SortBy: "priority"} // supported sort field
Defensive patterns

Strategy: validation

Validate before calling

if filter.SortBy != "" && !validSortFields[filter.SortBy] {
    return fmt.Errorf("unsupported sort field %q", filter.SortBy)
}

Try / catch

page, err := store.SearchWithCounts(ctx, q, filter)
if err != nil {
    var buildErr *fmt.WrapError // inspect unwrapped cause
    log.Printf("union issues-leg build failed: %v", err)
    filter.SkipWisps = true
    page, err = store.SearchWithCounts(ctx, q, filter)
}

Prevention

When it happens

Trigger: Calling search (with counts) across issues and wisps when the issues leg subquery build fails — typically an unsupported filter combination or an error surfaced from buildIssueFilterClauses inside buildUnionSubquery.

Common situations: Passing an IssueFilter with a field that has no SQL rendering for the issues table set, or an invalid sort/limit combination, during bd search/list queries that span both durable issues and ephemeral wisps.

Related errors


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