{"record":{"id":"04eda517280c00c9","repo":"gastownhall/beads","slug":"search-union-with-counts-wisps-w","errorCode":null,"errorMessage":"search union with counts (wisps): %w","messagePattern":"search union with counts \\(wisps\\): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue_search_counts.go","lineNumber":72,"sourceCode":"\t\t}\n\t\treturn finishSearchCountsPage(out, filter)\n\t}\n\n\treturn r.searchUnionWithCounts(ctx, query, filter, wispDepsExist)\n}\n\nfunc (r *issueSQLRepositoryImpl) searchUnionWithCounts(ctx context.Context, query string, filter types.IssueFilter, wispDepsExist bool) (domain.SearchCountsPage, error) {\n\touterOrderBy := unionOrderBySQL(filter.SortBy, filter.SortDesc)\n\twindow := searchWindowForFilter(filter)\n\tlegWindow := legWindowSQL(outerOrderBy, window)\n\n\tiSub, iArgs, err := r.buildUnionSubquery(query, filter, issuesFilterTables, \"i\", legWindow)\n\tif err != nil {\n\t\treturn domain.SearchCountsPage{}, fmt.Errorf(\"search union with counts (issues): %w\", err)\n\t}\n\twSub, wArgs, err := r.buildUnionSubquery(query, filter, wispsFilterTables, \"w\", legWindow)\n\tif err != nil {\n\t\treturn domain.SearchCountsPage{}, fmt.Errorf(\"search union with counts (wisps): %w\", err)\n\t}\n\n\t// EACH LEG IS PARENTHESIZED, and it is not decoration. A leg that carries\n\t// its own ORDER BY and LIMIT (legWindowSQL) is a syntax error inside a bare\n\t// UNION ALL — the engine reads the clause as belonging to the union — so the\n\t// parentheses are what let the window be pushed down at all.\n\t//nolint:gosec // G201: subqueries built from hardcoded table names and ? placeholders.\n\tunionSQL := fmt.Sprintf(\"SELECT id, src FROM ((%s) UNION ALL (%s)) merged %s %s\",\n\t\tiSub, wSub, outerOrderBy, window.sql)\n\n\targs := make([]any, 0, len(iArgs)+len(wArgs))\n\targs = append(args, iArgs...)\n\targs = append(args, wArgs...)\n\n\trows, err := r.runner.QueryContext(ctx, unionSQL, args...)\n\tif err != nil {\n\t\treturn domain.SearchCountsPage{}, fmt.Errorf(\"search union with counts: %w\", err)\n\t}","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue_search_counts.go#L54-L90","documentation":"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.","triggerScenarios":"searchUnionWithCounts building the wisp leg via buildUnionSubquery(query, filter, wispsFilterTables, \"w\", legWindow) fails — e.g. a filter field not renderable against wisp tables.","commonSituations":"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.","solutions":["Read the wrapped cause to identify the failing clause","Remove or adjust filter fields unsupported for wisps, or set filter.Ephemeral=false / filter.SkipWisps to skip the wisp leg","Verify wispsFilterTables covers every filter field your query uses","Upgrade/rebuild if schema is stale (wisps tables vs code expectations)"],"exampleFix":"// before\nfilter := types.IssueFilter{IncludeEphemeral: true, SomeIssueOnlyField: x}\n// after\nfilter.SkipWisps = true // or drop the issue-only field","handlingStrategy":"validation","validationCode":"if includesWispLeg(filter) && requiresIssueOnlyColumns(filter) {\n    return fmt.Errorf(\"filter field not supported for wisps; set SkipWisps or drop the field\")\n}","typeGuard":null,"tryCatchPattern":"page, err := store.SearchWithCounts(ctx, q, filter)\nif err != nil && strings.Contains(err.Error(), \"(wisps)\") {\n    filter.SkipWisps = true\n    page, err = store.SearchWithCounts(ctx, q, filter)\n}","preventionTips":["Keep wispsFilterTables in sync whenever a filter field is added","Test wisp-leg searches for every filter flag","Prefer SkipWisps when ephemeral issues are irrelevant to the query"],"tags":["sql","search","wisps","query-building"],"backgroundTag":"query-build-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}