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

  1. Identify the failing column from the wrapped driver error and inspect the offending row's raw value.
  2. Normalize legacy timestamp formats in `created_at` to what issueops.FormatAuxTime produces.
  3. Replace NULL author/text with empty strings at the schema level (NOT NULL DEFAULT '').
  4. 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

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


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