gastownhall/beads · error

db: CommentSQLRepository.CountsByIssueIDs: scan: %w

Error message

db: CommentSQLRepository.CountsByIssueIDs: scan: %w

What it means

During CountsByIssueIDs, each row (issue_id, COUNT(*)) is scanned into a string and an int. This error wraps a rows.Scan failure decoding one of those columns. It means the result row's column types could not be converted to (string, int) — usually a NULL column or driver type coercion issue.

Source

Thrown at internal/storage/domain/db/comment.go:59

		args[i] = id
	}
	table := pickCommentTable(opts.UseWispsTable)
	//nolint:gosec // G201: table is one of two hardcoded constants
	q := fmt.Sprintf(
		"SELECT issue_id, COUNT(*) FROM %s WHERE issue_id IN (%s) GROUP BY issue_id",
		table, strings.Join(placeholders, ","),
	)
	rows, err := r.runner.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("db: CommentSQLRepository.CountsByIssueIDs: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var issueID string
		var count int
		if err := rows.Scan(&issueID, &count); err != nil {
			return nil, fmt.Errorf("db: CommentSQLRepository.CountsByIssueIDs: scan: %w", err)
		}
		result[issueID] = count
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("db: CommentSQLRepository.CountsByIssueIDs: rows: %w", err)
	}
	return result, nil
}

func (r *commentSQLRepositoryImpl) ListByIssueIDs(ctx context.Context, issueIDs []string, opts domain.CommentOpts) (map[string][]*types.Comment, error) {
	result := make(map[string][]*types.Comment)
	if len(issueIDs) == 0 {
		return result, nil
	}
	placeholders := make([]string, len(issueIDs))
	args := make([]any, len(issueIDs))
	for i, id := range issueIDs {
		placeholders[i] = "?"

View on GitHub (pinned to 71377f2769)

Solutions

  1. Find and repair rows where comments.issue_id IS NULL (issue_id should be NOT NULL).
  2. Check the wrapped driver error to identify which column failed and its actual type.
  3. Align the driver version with what beads was built against.
  4. Retry if the error surfaced from a transient conversion edge case after a driver upgrade.

Example fix

// before: NULL issue_id breaks Scan into string
var issueID string
rows.Scan(&issueID, &count)

// after: make column NOT NULL in schema, or scan NullString defensively
var issueID sql.NullString
if err := rows.Scan(&issueID, &count); err != nil { return nil, err }
if issueID.Valid { result[issueID.String] = count }
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure no NULL issue_id rows exist before bulk count
var bad int
conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM comments WHERE issue_id IS NULL").Scan(&bad)
if bad > 0 { return fmt.Errorf("%d comment rows with NULL issue_id; repair first", bad) }

Type guard

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

Try / catch

counts, err := repo.CountsByIssueIDs(ctx, ids, opts)
if err != nil && isCountScanError(err) {
    // repair offending rows, then retry once
    repairNullIssueIDs(ctx, conn)
    counts, err = repo.CountsByIssueIDs(ctx, ids, opts)
}

Prevention

When it happens

Trigger: Calling CountsByIssueIDs when the comment table's `issue_id` column contains NULL, or the driver returns COUNT(*) in a type the driver refuses to convert into Go int (driver version change).

Common situations: Schema drift after manual edits or imports that inserted NULL issue_id rows; switching drivers (e.g. between Dolt and MySQL drivers) with different Scan strictness.

Related errors


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