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

  1. Check the underlying driver error (errors.Is / %v of the wrapped cause) to identify the offending column or conversion
  2. Find the history row with the NULL value and backfill it (UPDATE ... SET col = default WHERE col IS NULL)
  3. Ensure the database schema matches the version of the code (run migrations)
  4. 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

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


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