gastownhall/beads · error

search %s (hydrate): scan: %w

Error message

search %s (hydrate): scan: %w

What it means

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).

Source

Thrown at internal/storage/issueops/search.go:468

	fetchFrom := tables.Main
	if proj.joinLeases {
		fetchFrom += " " + sqlbuild.LeaseJoin(tables.Main)
	}
	//nolint:gosec // G201: column expression and table name are fixed; ids are parameterized.
	fetchSQL := fmt.Sprintf(`SELECT %s FROM %s WHERE id IN (%s)`,
		proj.columns(tables), fetchFrom, strings.Join(placeholders, ","))

	fetchRows, err := tx.QueryContext(ctx, fetchSQL, fetchArgs...)
	if err != nil {
		return nil, fmt.Errorf("search %s (hydrate): %w", tables.Main, err)
	}

	itemMap := make(map[string]T, len(ids))
	for fetchRows.Next() {
		item, scanErr := proj.scan(fetchRows)
		if scanErr != nil {
			_ = fetchRows.Close()
			return nil, fmt.Errorf("search %s (hydrate): scan: %w", tables.Main, scanErr)
		}
		itemMap[proj.id(item)] = item
	}
	_ = fetchRows.Close()
	if err := fetchRows.Err(); err != nil {
		return nil, fmt.Errorf("search %s (hydrate): rows: %w", tables.Main, err)
	}

	// Reorder to preserve the id-scan ORDER BY.
	results := make([]T, 0, len(ids))
	for _, id := range ids {
		if item, ok := itemMap[id]; ok {
			results = append(results, item)
		}
	}

	if proj.hydrate != nil && len(results) > 0 {
		if err := proj.hydrate(ctx, tx, tables, results, filter); err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped scan error for the offending column index/type; compare it to proj.columns(tables) for that projection
  2. Run schema migration to align the DB with the binary's expected schema (bd migrate / bd doctor)
  3. If NULLs are expected in your data, ensure the scan uses sql.NullString / nullable pointers
  4. Verify no manual schema edits diverged from beads' expected layout; restore from a clean export/import (bd export/import)
  5. If writing a custom projection, confirm columns() and scan() read the same columns in the same order

Example fix

// before: scanning a nullable column into string
var closedAt string
_ = rows.Scan(&id, &closedAt)
// after: handle NULL
var closedAt sql.NullString
_ = rows.Scan(&id, &closedAt)
issue.ClosedAt = nullableTime(closedAt)
Defensive patterns

Strategy: validation

Validate before calling

// verify schema version matches the binary before searching
if err := store.ValidateSchema(ctx); err != nil {
    return fmt.Errorf("run bd migrate: %w", err)
}

Type guard

func isScanErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "(hydrate): scan: ")
}

Try / catch

res, err := searchIssues(ctx, tx, q, filter)
if err != nil && isScanErr(err) {
    // scan failures usually mean schema drift: migrate, then retry once
    if migErr := store.Migrate(ctx); migErr != nil { return migErr }
    res, err = searchIssues(ctx, tx, q, filter)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/6c42c1071c75c622. Report an issue: GitHub.