{"record":{"id":"6c42c1071c75c622","repo":"gastownhall/beads","slug":"search-s-hydrate-scan-w","errorCode":null,"errorMessage":"search %s (hydrate): scan: %w","messagePattern":"search (.+?) \\(hydrate\\): scan: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/search.go","lineNumber":468,"sourceCode":"\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))\n\tfor _, id := range ids {\n\t\tif item, ok := itemMap[id]; ok {\n\t\t\tresults = append(results, item)\n\t\t}\n\t}\n\n\tif proj.hydrate != nil && len(results) > 0 {\n\t\tif err := proj.hydrate(ctx, tx, tables, results, filter); err != nil {","sourceCodeStart":450,"sourceCodeEnd":486,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/search.go#L450-L486","documentation":"searchTablePatternBT wraps a row-scan failure of the Pattern B batch-fetch: proj.scan could not decode a row of the SELECT ... WHERE id IN (...) fetch into the projection type. This means the query ran but a column's value could not be scanned (type mismatch, unexpected NULL, column count change).","triggerScenarios":"A row returned by the batch fetch has a column whose Go type does not match the scan target — e.g. schema drift after an upgrade (a NOT NULL column became NULLable and is NULL), a projection/scan pair out of sync in code, or a value stored with an unexpected type in the DB.","commonSituations":"Running a newer bd binary against an older database (or vice versa) so column layout differs; a manually edited/migrated DB with NULLs where the scanner expects values; custom projections in tests whose columns() and scan() disagree.","solutions":["Check the wrapped scan error for the offending column index/type; compare it to proj.columns(tables) for that projection","Run schema migration to align the DB with the binary's expected schema (bd migrate / bd doctor)","If NULLs are expected in your data, ensure the scan uses sql.NullString / nullable pointers","Verify no manual schema edits diverged from beads' expected layout; restore from a clean export/import (bd export/import)","If writing a custom projection, confirm columns() and scan() read the same columns in the same order"],"exampleFix":"// before: scanning a nullable column into string\nvar closedAt string\n_ = rows.Scan(&id, &closedAt)\n// after: handle NULL\nvar closedAt sql.NullString\n_ = rows.Scan(&id, &closedAt)\nissue.ClosedAt = nullableTime(closedAt)","handlingStrategy":"validation","validationCode":"// verify schema version matches the binary before searching\nif err := store.ValidateSchema(ctx); err != nil {\n    return fmt.Errorf(\"run bd migrate: %w\", err)\n}","typeGuard":"func isScanErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"(hydrate): scan: \")\n}","tryCatchPattern":"res, err := searchIssues(ctx, tx, q, filter)\nif err != nil && isScanErr(err) {\n    // scan failures usually mean schema drift: migrate, then retry once\n    if migErr := store.Migrate(ctx); migErr != nil { return migErr }\n    res, err = searchIssues(ctx, tx, q, filter)\n}","preventionTips":["Always migrate the DB after upgrading the bd binary","Never hand-edit schema columns; use bd export/import for reshaping","Keep projections' columns() and scan() in sync when customizing","Use nullable scan types (sql.NullString, pointers) for optional columns"],"tags":["database","sql","scan","schema-mismatch"],"backgroundTag":"sql-scan-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}