{"record":{"id":"a8b88bdd128815c6","repo":"gastownhall/beads","slug":"search-s-hydrate-w-a8b88b","errorCode":null,"errorMessage":"search %s (hydrate): %w","messagePattern":"search (.+?) \\(hydrate\\): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/search.go","lineNumber":460,"sourceCode":"\n\t// Batch-fetch full rows from the known table (no wispSet partition needed).\n\tplaceholders := make([]string, len(ids))\n\tfetchArgs := make([]interface{}, len(ids))\n\tfor i, id := range ids {\n\t\tplaceholders[i] = \"?\"\n\t\tfetchArgs[i] = id\n\t}\n\tfetchFrom := tables.Main\n\tif proj.joinLeases {\n\t\tfetchFrom += \" \" + sqlbuild.LeaseJoin(tables.Main)\n\t}\n\t//nolint:gosec // G201: column expression and table name are fixed; ids are parameterized.\n\tfetchSQL := fmt.Sprintf(`SELECT %s FROM %s WHERE id IN (%s)`,\n\t\tproj.columns(tables), fetchFrom, strings.Join(placeholders, \",\"))\n\n\tfetchRows, err := tx.QueryContext(ctx, fetchSQL, fetchArgs...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"search %s (hydrate): %w\", tables.Main, err)\n\t}\n\n\titemMap := make(map[string]T, len(ids))\n\tfor fetchRows.Next() {\n\t\titem, scanErr := proj.scan(fetchRows)\n\t\tif scanErr != nil {\n\t\t\t_ = fetchRows.Close()\n\t\t\treturn nil, fmt.Errorf(\"search %s (hydrate): scan: %w\", tables.Main, scanErr)\n\t\t}\n\t\titemMap[proj.id(item)] = item\n\t}\n\t_ = fetchRows.Close()\n\tif err := fetchRows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"search %s (hydrate): rows: %w\", tables.Main, err)\n\t}\n\n\t// Reorder to preserve the id-scan ORDER BY.\n\tresults := make([]T, 0, len(ids))","sourceCodeStart":442,"sourceCodeEnd":478,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/search.go#L442-L478","documentation":"searchTablePatternBT wraps a failure of the Pattern B batch-fetch query (SELECT <columns> FROM <table> WHERE id IN (...)) executed after the id-only scan. The cheap id scan succeeded but re-fetching full rows for those ids failed. The table name is interpolated; the ids are bound as parameters, so the SQL itself is fixed — the cause is a driver/DB-level failure of that SELECT.","triggerScenarios":"searchTablePatternBT runs (wide projection + filter.Limit > 0 + !NoIDShrink), the id scan returns ids, and tx.QueryContext on the fetch SQL errors — e.g. table missing/dropped between the two queries, connection dropped, ctx cancelled, or too many IN placeholders exhausting a driver limit.","commonSituations":"Very large filter.Limit producing an IN list exceeding driver parameter limits (SQLite default 999/32766); connection reset mid-search; database file locked by another process; schema changed (table dropped) between the id scan and fetch.","solutions":["Read the wrapped driver error; if it's a parameter-count or query-size limit, lower filter.Limit or set filter.NoIDShrink to force the single-query path","Confirm the main issues/wisps table still exists and schema is current (bd migrate / bd doctor)","Check DB connectivity and that no other process holds an exclusive lock","If ctx deadline was exceeded, raise the timeout or reduce the limit","Retry the operation on a new transaction/connection"],"exampleFix":"// before: huge limit -> enormous IN clause -> driver parameter limit\nfilter.Limit = 100000\nids, err := searchIssues(ctx, tx, q, filter)\n// after: keep Pattern B within driver limits or opt out of id-shrink\nfilter.Limit = 500 // or:\nfilter.NoIDShrink = true // single wide query, no IN fetch\nids, err := searchIssues(ctx, tx, q, filter)","handlingStrategy":"validation","validationCode":"// keep the IN list within driver parameter limits before a Pattern B search\nconst maxParams = 900 // conservative for SQLite\nif filter.Limit > maxParams && !filter.NoIDShrink {\n    filter.NoIDShrink = true // single wide query instead of IN (...) fetch\n}","typeGuard":"func isBatchFetchErr(err error) bool {\n    if err == nil { return false }\n    s := err.Error()\n    return strings.Contains(s, \"(hydrate)\") && !strings.Contains(s, \"scan:\") && !strings.Contains(s, \"rows:\")\n}","tryCatchPattern":"res, err := searchIssues(ctx, tx, q, filter)\nif err != nil && isBatchFetchErr(err) {\n    // fall back to the non-shrink single-query path\n    filter.NoIDShrink = true\n    res, err = searchIssues(ctx, tx, q, filter)\n}","preventionTips":["Keep filter.Limit under the driver's parameter limit when id-shrink is active","Set NoIDShrink for very large limits","Keep the connection alive across both queries of the pattern (same tx, healthy conn)","Run bd doctor to confirm table/schema health before bulk searches"],"tags":["database","sql","query","id-shrink"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}