gastownhall/beads · error

search %s: scan: %w

Error message

search %s: scan: %w

What it means

Raised when rows.Scan (inside scanIssue) fails while iterating search result rows. Scan fails when a column's value cannot be converted into the destination Go type — e.g. NULL in a NOT-NULL-assumed column, a malformed value, or driver/column-count mismatch. The search wraps it as "search <table>: scan: <err>" and closes the rows before returning.

Source

Thrown at internal/storage/domain/db/issue_search.go:269

	orderBy := orderBySQL(filter.SortBy, filter.SortDesc, "")
	window := searchWindowForFilter(filter)

	//nolint:gosec // G201: SQL fragments from fixed table names and parameterized filters.
	querySQL := fmt.Sprintf(`%s%s FROM %s %s %s %s %s`,
		selectKw, issueSelectColumns, plan.FromSQL, sqlbuild.LeaseJoin(tables.Main), whereSQL, orderBy, window.sql)

	rows, err := r.runner.QueryContext(ctx, querySQL, args...)
	if err != nil {
		return domain.SearchPage{}, fmt.Errorf("search %s: %w", tables.Main, err)
	}

	var issues []*types.Issue
	seen := make(map[string]bool)
	for rows.Next() {
		issue, scanErr := scanIssue(rows)
		if scanErr != nil {
			_ = rows.Close()
			return domain.SearchPage{}, fmt.Errorf("search %s: scan: %w", tables.Main, scanErr)
		}
		if seen[issue.ID] {
			continue
		}
		seen[issue.ID] = true
		issues = append(issues, issue)
	}
	_ = rows.Close()
	if err := rows.Err(); err != nil {
		return domain.SearchPage{}, fmt.Errorf("search %s: rows: %w", tables.Main, err)
	}

	sortRowsGoSide(issues, func(i *types.Issue) string { return i.ID }, filter.SortBy, filter.SortDesc)
	items, hasMore, err := finishWindow(issues, window)
	if err != nil {
		return domain.SearchPage{}, err
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped scan error to find which column/value failed conversion.
  2. Run `bd doctor` / schema migrations to bring the DB to the expected schema version.
  3. Inspect the offending issue row and repair or delete corrupt rows.
  4. Restore the database from a known-good backup (Dolt history or .beads backup).

Example fix

// before
// rows contain NULL priority; scanIssue scans into *int with Scan(&priority) fine,
// but a NULL into a plain int fails:
issue, scanErr := scanIssue(rows) // "sql: Scan error on column 'priority'
// after
// migrate/repair the row so values match the schema, or coalesce at read time:
SELECT COALESCE(priority, 0) AS priority, ... FROM issues
Defensive patterns

Strategy: validation

Validate before calling

// validate schema version before searching
if err := bd.CheckSchemaVersion(dbPath); err != nil {
	return fmt.Errorf("run `bd migrate` first: %w", err)
}

Type guard

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

Try / catch

page, err := store.Search(ctx, q, filter)
if err != nil && isScanError(err) {
	// schema drift or corrupt row; surface repair guidance to the user
	return fmt.Errorf("%w (run `bd doctor` / `bd migrate`)", err)
}

Prevention

When it happens

Trigger: A search hits rows whose stored data violates the expected column shape: NULL in a column scanIssue scans into a value type, text in an integer column, or a schema where issueSelectColumns count/order no longer matches the physical table (schema drift between bd versions).

Common situations: Hand-edited or externally modified Dolt/SQLite rows introducing NULLs; upgrading bd across a schema change without running migrations; partial migration leaving old columns in place.

Related errors


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