gastownhall/beads · error
db: CommentSQLRepository.ListByIssueIDs: scan: %w
Error message
db: CommentSQLRepository.ListByIssueIDs: scan: %w
What it means
ListByIssueIDs scans each row into a types.Comment (ID, IssueID, Author, Text, CreatedAt). This error wraps a rows.Scan failure decoding one comment row — a column value (including timestamps) could not be converted to the target Go types. No partial comment list is returned.
Source
Thrown at internal/storage/domain/db/comment.go:97
}
table := pickCommentTable(opts.UseWispsTable)
//nolint:gosec // G201: table is one of two hardcoded constants
q := fmt.Sprintf(`
SELECT id, issue_id, author, text, created_at
FROM %s
WHERE issue_id IN (%s)
ORDER BY issue_id, created_at ASC, id ASC
`, table, strings.Join(placeholders, ","))
rows, err := r.runner.QueryContext(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("db: CommentSQLRepository.ListByIssueIDs: %w", err)
}
defer rows.Close()
for rows.Next() {
var c types.Comment
if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil {
return nil, fmt.Errorf("db: CommentSQLRepository.ListByIssueIDs: scan: %w", err)
}
cc := c
result[c.IssueID] = append(result[c.IssueID], &cc)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("db: CommentSQLRepository.ListByIssueIDs: rows: %w", err)
}
return result, nil
}
func (r *commentSQLRepositoryImpl) IterByIssueID(ctx context.Context, issueID string, opts domain.CommentOpts) (storage.Iter[types.Comment], error) {
bulk, err := r.ListByIssueIDs(ctx, []string{issueID}, opts)
if err != nil {
return nil, err
}
return storage.NewSliceIter(bulk[issueID]), nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Identify the failing column from the wrapped driver error and inspect the offending row's raw value.
- Normalize legacy timestamp formats in `created_at` to what issueops.FormatAuxTime produces.
- Replace NULL author/text with empty strings at the schema level (NOT NULL DEFAULT '').
- Pin/align the SQL driver version to avoid Scan conversion changes.
Example fix
// before: legacy timestamp format can't be scanned into time.Time rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt) // after: migrate stored timestamps to the canonical format first UPDATE comments SET created_at = DATE_FORMAT(created_at, canonical_fmt) WHERE ...
Defensive patterns
Strategy: try-catch
Validate before calling
// preflight: reject rows whose created_at can't parse canonically
var bad int
conn.QueryRowContext(ctx,
"SELECT COUNT(*) FROM comments WHERE created_at IS NULL OR LENGTH(created_at) = 0").Scan(&bad)
if bad > 0 { return fmt.Errorf("%d malformed comment timestamps", bad) } Type guard
func isCommentScanError(err error) bool {
return err != nil && strings.Contains(err.Error(), "ListByIssueIDs: scan:")
} Try / catch
bulk, err := repo.ListByIssueIDs(ctx, ids, opts)
if err != nil && isCommentScanError(err) {
// identify and normalize the bad row, then retry
return fmt.Errorf("bad comment row: %w", err)
} Prevention
- Keep timestamp format canonical via issueops.FormatAuxTime
- Disallow NULL author/text at schema level
- Run scan-compat tests after driver upgrades
When it happens
Trigger: Calling ListByIssueIDs/IterByIssueID when `created_at` holds a value that doesn't parse into the Comment's time field, `text`/`author` are NULL, or the driver's type mapping changed between versions.
Common situations: Rows written by older schema versions with different timestamp formats; imports inserting NULL author/text; driver upgrades altering time handling (DATETIME vs string parsing).
Related errors
- db: ChildCounterSQLRepository.NextChildID: scan: %w
- db: CommentSQLRepository.CountsByIssueIDs: scan: %w
- db: LabelSQLRepository.List: scan: %w
- db: LabelSQLRepository.ListByIssueIDs: scan: %w
- get issues by IDs: scan: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/bb3bba70615bc2e6.
Report an issue: GitHub.