{"record":{"id":"b76f7a67d5a313df","repo":"gastownhall/beads","slug":"search-count-s-w-b76f7a","errorCode":null,"errorMessage":"search count %s: %w","messagePattern":"search count (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/search_counts.go","lineNumber":159,"sourceCode":"\treturn out, nil\n}\n\n//nolint:gosec // G201: SQL fragments are caller-built from hardcoded shapes\nfunc runSearchQueryInTx(ctx context.Context, tx *sql.Tx, tables FilterTables, whereSQL, orderBySQL, limitSQL string, args []interface{}, includeWispReverseDeps bool, hyd sqlbuild.CountsHydration) ([]*types.IssueWithCounts, error) {\n\tsearchSQL, _ := sqlbuild.SearchCountsSQL(tables, nil, whereSQL, orderBySQL, limitSQL, includeWispReverseDeps, hyd)\n\treturn scanCountsRowsInTx(ctx, tx, tables.Main, searchSQL, args, hyd)\n}\n\n// scanCountsRowsInTx runs a prebuilt counts mega-query and hydrates each row\n// through ScanReadyWorkRowWithCounts, deduping by issue ID. It is the single\n// scan/dedupe loop shared by the predicate-form search path and the by-IDs\n// ready-counts path.\n//\n//nolint:gosec // G201: query is builder-produced; user input rides ? placeholders.\nfunc scanCountsRowsInTx(ctx context.Context, tx *sql.Tx, mainTable, query string, args []interface{}, hyd sqlbuild.CountsHydration) ([]*types.IssueWithCounts, error) {\n\trows, err := tx.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"search count %s: %w\", mainTable, err)\n\t}\n\tdefer func() { _ = rows.Close() }()\n\n\tvar out []*types.IssueWithCounts\n\tseen := make(map[string]bool)\n\tfor rows.Next() {\n\t\tiwc, scanErr := ScanReadyWorkRowWithCounts(rows, hyd)\n\t\tif scanErr != nil {\n\t\t\treturn nil, scanErr\n\t\t}\n\t\tif iwc == nil || iwc.Issue == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif seen[iwc.Issue.ID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[iwc.Issue.ID] = true\n\t\tout = append(out, iwc)","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/search_counts.go#L141-L177","documentation":"scanCountsRowsInTx wraps a failure of the counts search query (tx.QueryContext) with the main table name. This is the ready-counts / search-with-counts projection query built by the sqlbuild builder; a failure here means the SELECT with count joins never produced a result set. The query string is builder-produced and user input rides ? placeholders, so the cause is a DB-level error, not SQL injection surface.","triggerScenarios":"runReadyCountsInTx or runSearchQueryInTx invokes scanCountsRowsInTx and the count-projection SELECT fails — missing table (e.g. wisps on old schemas), bad sort/filter expression reaching ORDER BY, DB locked, connection dropped, or ctx cancelled.","commonSituations":"bd ready / bd list --counts against an older DB missing a joined optional table; an invalid sort key or query expression producing invalid SQL; write-lock contention during a bulk import; remote Dolt outage.","solutions":["Unwrap the driver error; for 'no such table', run schema migration (bd migrate / bd doctor)","Validate the query/sort expression (bd query '<expr>' syntax) if the cause is an SQL syntax error","Check for concurrent writers/locks and retry","Raise the context timeout or reduce the filter scope","If the SQL build itself is at fault, check the sqlbuild version matches your storage layer"],"exampleFix":"// before: old DB missing table joined by the counts projection\nres, err := issueops.ReadyIssuesWithCounts(ctx, filter) // 'no such table: wisp_dependencies'\n// after: bring schema current first\nif err := store.Migrate(ctx); err != nil { return err }\nres, err := issueops.ReadyIssuesWithCounts(ctx, filter)","handlingStrategy":"validation","validationCode":"// validate schema and expression before a counts query\nif err := store.ValidateSchema(ctx); err != nil {\n    return fmt.Errorf(\"run bd migrate: %w\", err)\n}\nif _, _, err := issueops.BuildIssueFilterClauses(query, filter, tables); err != nil {\n    return fmt.Errorf(\"invalid query/filter: %w\", err)\n}","typeGuard":"func isCountsQueryErr(err error) bool {\n    if err == nil { return false }\n    for _, t := range []string{\"issues\", \"wisps\"} {\n        if strings.HasPrefix(err.Error(), \"search count \"+t+\": \") {\n            return !strings.Contains(err.Error(), \": rows: \")\n        }\n    }\n    return false\n}","tryCatchPattern":"res, err := issueops.SearchIssuesWithCounts(ctx, q, filter)\nif err != nil && isCountsQueryErr(err) {\n    if strings.Contains(err.Error(), \"no such table\") {\n        if migErr := store.Migrate(ctx); migErr != nil { return migErr }\n        res, err = issueops.SearchIssuesWithCounts(ctx, q, filter)\n    }\n    if err != nil { return err }\n}","preventionTips":["Migrate after every binary upgrade so joined tables exist","Validate bd query expressions and sort keys before running","Avoid count searches during exclusive-lock bulk operations","Keep sqlbuild and storage layers version-aligned"],"tags":["database","sql","counts","query"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}