{"record":{"id":"f28aac0648e7c924","repo":"gastownhall/beads","slug":"search-s-w-f28aac","errorCode":null,"errorMessage":"search %s: %w","messagePattern":"search (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/search.go","lineNumber":420,"sourceCode":"\t\tid := proj.id(item)\n\t\tif _, dup := seen[id]; dup {\n\t\t\tcontinue // GH#3567: skip duplicate rows from dependency subqueries\n\t\t}\n\t\tseen[id] = struct{}{}\n\t\tresults = append(results, item)\n\t}\n\t_ = rows.Close()\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"search %s: rows: %w\", tables.Main, err)\n\t}\n\n\tif goSideSort {\n\t\tresults = goSideSortAndTrim(results, proj.id, filter.SortDesc, eff)\n\t}\n\n\tif proj.hydrate != nil && len(results) > 0 {\n\t\tif err := proj.hydrate(ctx, tx, tables, results, filter); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"search %s: %w\", tables.Main, err)\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// searchTablePatternBT runs Pattern B for wide projections. It reuses the\n// id-only search (idProjection) — byte-for-byte the non-hydrating query\n// SearchIssueIDsInTx runs against this table — to get the ordered, LIMIT-bound\n// id list, then batch-fetches the full projection for those ids and hydrates.\n// Keeping the shrink scan in exactly one place (the id projection) is why this\n// no longer hand-rolls its own SELECT id loop. Narrow projections never reach\n// here: they leave idShrink false and are themselves the id scan.\nfunc searchTablePatternBT[T any](ctx context.Context, tx DBTX, query string, filter types.IssueFilter, tables FilterTables, proj searchProjection[T]) ([]T, error) {\n\tids, err := searchTableInTxT(ctx, tx, query, filter, tables, idProjection)\n\tif err != nil {\n\t\treturn nil, err\n\t}","sourceCodeStart":402,"sourceCodeEnd":438,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/search.go#L402-L438","documentation":"searchTableInTxT wraps a failure from the projection's post-search hydrate callback with the searched table name (tables.Main). The main row scan succeeded, but enriching the result rows (e.g. loading dependencies, labels, or other related data) inside the same transaction failed. This is a wrapper: the wrapped cause (err) carries the real failure.","triggerScenarios":"Calling any search that routes through searchTableInTxT (directly or via getReadyWispsInTx, searchInTx, searchTablePatternBT) where proj.hydrate is non-nil, results are non-empty, and the hydration query fails — e.g. the hydration SQL hits a missing/corrupt auxiliary table, the tx was already broken by an earlier error, or ctx was cancelled mid-hydration.","commonSituations":"Running bd search/list against a database where an auxiliary table (dependencies, labels, etc.) was dropped or is from an older schema version; a DB lock/timeout during hydration; context deadline exceeded on large result sets; a corrupted transaction after a prior failed statement.","solutions":["Inspect the wrapped inner error (%w cause) — it names the actual hydration failure; fix that root cause first","Verify all auxiliary tables the projection hydrates from exist (run schema migration, e.g. bd doctor / bd migrate)","If the cause is context deadline/cancel, increase the timeout or reduce filter.Limit / MaxRows to shrink the result set","Retry the search on a fresh transaction if the tx was poisoned by an earlier failed statement","Check DB connectivity/locks (another process holding a write lock on Dolt/SQLite)"],"exampleFix":"// before: searching with a filter that hydrates deps against a schema missing the table\nissues, err := issueops.SearchIssuesInTx(ctx, tx, \"\", filter)\n// after: ensure schema is current first, and bound the work\nif err := store.EnsureSchema(ctx); err != nil { return err }\nfilter.Limit = 100\nissues, err := issueops.SearchIssuesInTx(ctx, tx, \"\", filter)","handlingStrategy":"try-catch","validationCode":"// before calling search: ensure auxiliary tables exist and tx is live\nvar one int\nif err := tx.QueryRowContext(ctx, \"SELECT 1 FROM dependencies LIMIT 1\").Scan(&one); err != nil && !errors.Is(err, sql.ErrNoRows) {\n    return fmt.Errorf(\"hydration prerequisite missing: %w\", err)\n}\nif ctx.Err() != nil { return ctx.Err() }","typeGuard":"func isHydrateSearchErr(err error) (string, bool) {\n    if err == nil { return \"\", false }\n    msg := err.Error()\n    for _, table := range []string{\"issues\", \"wisps\", \"wisp_dependencies\"} {\n        if strings.HasPrefix(msg, \"search \"+table+\": \") {\n            return table, true\n        }\n    }\n    return \"\", false\n}","tryCatchPattern":"results, err := issueops.SearchIssuesInTx(ctx, tx, q, filter)\nif err != nil {\n    var table string\n    if isHydrateSearchErr(err) && strings.HasPrefix(err.Error(), table = \"search \") {\n        // inner cause via errors.Unwrap / %w chain\n        return fmt.Errorf(\"search hydration failed for %s: %w\", table, err)\n    }\n    return err\n}","preventionTips":["Run schema migrations before searching (bd migrate / bd doctor)","Keep context deadlines proportional to result-set size","Use SkipLabels/SkipCounts/Lite filters when you do not need hydrated fields","Retry transient driver errors with backoff on fresh transactions"],"tags":["database","sql","hydration","search"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}