gastownhall/beads · error

search count %s: rows: %w

Error message

search count %s: rows: %w

What it means

scanCountsRowsInTx wraps the error from rows.Err() after iterating the counts result set. The counts query started fine and rows scanned (or were skipped as duplicate IDs), but the driver reported a failure at end-of-iteration — typically a mid-stream connection or server failure only visible once the result set is exhausted.

Source

Thrown at internal/storage/issueops/search_counts.go:180

	var out []*types.IssueWithCounts
	seen := make(map[string]bool)
	for rows.Next() {
		iwc, scanErr := ScanReadyWorkRowWithCounts(rows, hyd)
		if scanErr != nil {
			return nil, scanErr
		}
		if iwc == nil || iwc.Issue == nil {
			continue
		}
		if seen[iwc.Issue.ID] {
			continue
		}
		seen[iwc.Issue.ID] = true
		out = append(out, iwc)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("search count %s: rows: %w", mainTable, err)
	}
	return out, nil
}

// finishSearchIssuesWithCounts is the single terminal hook every
// SearchIssuesWithCountsInTx exit path routes through: it sorts the merged
// result, applies the caller-facing Limit trim, and only then enforces the
// defensive MaxRows cap (be-x42v) on the delivered count — mirroring
// searchInTx's trimToSearchLimit-before-EnforceMaxRowsCap ordering and
// finishReadyWorkWithCounts in ready_work_counts.go.
//
// Trim-before-cap matters for the merged (issues+wisps) case:
// runFilterSearchQueryInTx sizes each leg's SQL LIMIT independently via
// EffectiveSearchLimit(filter.Limit, filter.MaxRows), so the merged
// pre-trim slice can hold up to ~2x that per-leg bound — e.g. Limit=2,
// MaxRows=5, 3 rows in each table merges to 6, which would trip MaxRows
// even though the page actually handed back to the caller (trimmed to
// Limit=2) is well within the cap. Checking the cap against the delivered

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the rows error and retry if transient (reset connection, killed query)
  2. Raise the context timeout for large count scans
  3. Narrow the filter (status, assignee, limit) to shrink the scanned set
  4. Check server-side kill/timeout logs and raise them if they cut the query
  5. Ensure the process is not receiving signals/interrupts mid-scan (e.g. CI cancellation)

Example fix

// before: default deadline too short for a full-repo count scan
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
// after: size the deadline to the corpus
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
issues, err := issueops.SearchIssuesWithCounts(ctx, q, filter)
Defensive patterns

Strategy: retry

Validate before calling

// sanity-check scope before a long count scan
if ctx.Err() != nil { return ctx.Err() }
if filter.Limit == 0 && filter.MaxRows == 0 && corpusIsLarge() {
    filter.MaxRows = 10000 // cap the scan
}

Type guard

func isCountsRowsErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), ": rows: ") &&
        strings.HasPrefix(err.Error(), "search count ")
}

Try / catch

var res []*types.IssueWithCounts
var err error
for attempt := 0; attempt < 3; attempt++ {
    res, err = issueops.SearchIssuesWithCounts(ctx, q, filter)
    if err == nil || !isCountsRowsErr(err) { break }
    time.Sleep(backoff(attempt))
}
if err != nil { return err }

Prevention

When it happens

Trigger: During iteration of the count-projection SELECT in runReadyCountsInTx/runSearchQueryInTx, the connection breaks, the server kills the query, or ctx is cancelled between Next() calls; the deferred error is surfaced via rows.Err() and wrapped with the main table name.

Common situations: Long-running count queries over large corpora hitting a server-side timeout; network blip to a remote Dolt server; SQLite interrupted by a process signal mid-scan.

Related errors


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