gastownhall/beads · error

db: CommentSQLRepository.ListByIssueIDs: rows

Error message

db: CommentSQLRepository.ListByIssueIDs: rows: %w

What it means

ListByIssueIDs calls rows.Err() after the scan loop to detect errors during row streaming; this wrapper surfaces it. The entire per-issue comment grouping is discarded on failure, and IterByIssueID propagates it directly. Callers get no partial data.

Solutions

  1. Check errors.Is(err, context.DeadlineExceeded) and raise the timeout for large fetches.
  2. Retry; the operation is read-only and safe to repeat.
  3. Reduce result size with pagination/chunking of issue IDs.
  4. Verify connection pool MaxLifetime is shorter than the server's idle timeout.

Example fix

// before: streaming a huge comment set inside a short request context
bulk, err := repo.ListByIssueIDs(ctx, ids, opts)

// after: bounded retry on iteration failure
for attempt := 0; attempt < 3; attempt++ {
    bulk, err = repo.ListByIssueIDs(ctx, ids, opts)
    if err == nil || !errors.Is(err, context.DeadlineExceeded) { break }
    time.Sleep(time.Second * time.Duration(attempt+1))
}
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil { return fmt.Errorf("pre-list context check: %w", err) }

Type guard

func isListRowsError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "ListByIssueIDs: rows:")
}

Try / catch

bulk, err := repo.ListByIssueIDs(ctx, ids, opts)
for attempt := 1; attempt <= 3 && isListRowsError(err); attempt++ {
    time.Sleep(time.Duration(attempt) * time.Second)
    bulk, err = repo.ListByIssueIDs(ctx, ids, opts)
}

Prevention

When it happens

Trigger: Calling ListByIssueIDs/IterByIssueID when the connection drops, the context is canceled, or the server aborts the query while comment rows are being streamed.

Common situations: Request-scoped deadlines expiring on large comment fetches; remote Dolt server over an unstable network; server-side query kill due to max_execution_time.

Related errors


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

Appendix: source

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

		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
}

func (r *commentSQLRepositoryImpl) Insert(ctx context.Context, issueID, author, text string, opts domain.CommentOpts) (*types.Comment, error) {
	// Live add: advance past the issue's newest comment so a burst inside one
	// second still reads back in write order (issueops.NextLiveCommentTime).
	// InsertRecord honors a supplied CreatedAt verbatim, which is what keeps
	// imported comments on their original timestamps.
	stamp, err := issueops.NextLiveCommentTime(ctx, r.runner, pickCommentTable(opts.UseWispsTable), issueID, time.Now())

View on GitHub (pinned to 71377f2769)