gastownhall/beads · error

db: CommentSQLRepository.ListByIssueIDs: rows: %w

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.

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)

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.