gastownhall/beads · error
failed to scan history: %w
Error message
failed to scan history: %w
What it means
HistoryInTx wraps any error from row.Scan while reading history rows into an Issue struct. The scan fails when a column in the SELECT does not match the destination Go types (NULL into non-nullable fields, string/date conversion failures, wrong column count). The %w wrapping preserves the underlying driver error for errors.Is/As inspection.
Source
Thrown at internal/storage/issueops/history.go:60
var entries []*storage.HistoryEntry
for rows.Next() {
var issue types.Issue
var createdAtStr, updatedAtStr sql.NullString
var closedAt sql.NullTime
var assignee, owner, createdBy, closeReason, molType sql.NullString
var estimatedMinutes sql.NullInt64
var pinned sql.NullInt64
var commitHash, committer string
var commitDate time.Time
if err := rows.Scan(
&issue.ID, &issue.Title, &issue.Description, &issue.Design, &issue.AcceptanceCriteria, &issue.Notes,
&issue.Status, &issue.Priority, &issue.IssueType, &assignee, &owner, &createdBy,
&estimatedMinutes, &createdAtStr, &updatedAtStr, &closedAt, &closeReason,
&pinned, &molType,
&commitHash, &committer, &commitDate,
); err != nil {
return nil, fmt.Errorf("failed to scan history: %w", err)
}
if createdAtStr.Valid {
issue.CreatedAt = ParseTimeString(createdAtStr.String)
}
if updatedAtStr.Valid {
issue.UpdatedAt = ParseTimeString(updatedAtStr.String)
}
if closedAt.Valid {
issue.ClosedAt = &closedAt.Time
}
if assignee.Valid {
issue.Assignee = assignee.String
}
if owner.Valid {
issue.Owner = owner.String
}
if createdBy.Valid {View on GitHub (pinned to 71377f2769)
Solutions
- Check the underlying driver error (errors.Is / %v of the wrapped cause) to identify the offending column or conversion
- Find the history row with the NULL value and backfill it (UPDATE ... SET col = default WHERE col IS NULL)
- Ensure the database schema matches the version of the code (run migrations)
- Add sql.Null* destinations or COALESCE in the SELECT for columns that may legitimately be NULL
Example fix
// before
&createdBy,
// after
var createdBy sql.NullString
... scan into &createdBy, then:
if createdBy.Valid { issue.CreatedBy = createdBy.String } Defensive patterns
Strategy: try-catch
Validate before calling
// check for NULLs in history rows before scanning
rows, _ := db.Query("SELECT id, created_by, issue_type FROM issues WHERE id = ?", id)
for rows.Next() {
var createdBy, issueType sql.NullString
rows.Scan(&id, &createdBy, &issueType)
if !createdBy.Valid || !issueType.Valid { /* backfill or skip row */ }
} Type guard
func scanableHistoryRow(createdBy, issueType sql.NullString) bool {
return createdBy.Valid && issueType.Valid
} Try / catch
issue, err := issueops.HistoryInTx(ctx, tx, issueID)
if err != nil {
var scanErr *fmt.ScanError // or inspect wrapped cause
if errors.Is(err, sql.ErrNoRows) { return nil, storage.ErrNotFound }
log.Printf("history scan failed: %v", err) // cause identifies the column
return nil, fmt.Errorf("history unavailable: %w", err)
} Prevention
- Use sql.Null* types for all nullable columns
- Keep SELECT column lists and Scan destinations in lockstep when editing the query
- Run schema migrations before deploying new code
- Backfill NOT NULL columns with defaults when adding them
When it happens
Trigger: A column expected non-NULL is NULL in a history row (e.g. created_by, issue_type, commit_hash) and Scan into a string/[]byte destination rejects it; or the schema evolved (column added/renamed/removed) so the SELECT column list no longer matches the Scan destination list.
Common situations: Dolt schema migration leaving older rows with NULLs in columns added later; hand-edited or imported rows missing required fields; running a new binary against an older database missing columns.
Related errors
- %s: %w
- failed to scan federation peer: %w
- iter issues: query: %w
- failed to scan dolt_status: %w
- scan dirty config key: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/036320ed8f780edb.
Report an issue: GitHub.