gastownhall/beads · error

db: CommentSQLRepository.ListByIssueIDs: scan

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.

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.

Appendix: 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)