gastownhall/beads · error

search count %s: %w

Error message

search count %s: %w

What it means

scanCountsRowsInTx wraps a failure of the counts search query (tx.QueryContext) with the main table name. This is the ready-counts / search-with-counts projection query built by the sqlbuild builder; a failure here means the SELECT with count joins never produced a result set. The query string is builder-produced and user input rides ? placeholders, so the cause is a DB-level error, not SQL injection surface.

Source

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

	return out, nil
}

//nolint:gosec // G201: SQL fragments are caller-built from hardcoded shapes
func runSearchQueryInTx(ctx context.Context, tx *sql.Tx, tables FilterTables, whereSQL, orderBySQL, limitSQL string, args []interface{}, includeWispReverseDeps bool, hyd sqlbuild.CountsHydration) ([]*types.IssueWithCounts, error) {
	searchSQL, _ := sqlbuild.SearchCountsSQL(tables, nil, whereSQL, orderBySQL, limitSQL, includeWispReverseDeps, hyd)
	return scanCountsRowsInTx(ctx, tx, tables.Main, searchSQL, args, hyd)
}

// scanCountsRowsInTx runs a prebuilt counts mega-query and hydrates each row
// through ScanReadyWorkRowWithCounts, deduping by issue ID. It is the single
// scan/dedupe loop shared by the predicate-form search path and the by-IDs
// ready-counts path.
//
//nolint:gosec // G201: query is builder-produced; user input rides ? placeholders.
func scanCountsRowsInTx(ctx context.Context, tx *sql.Tx, mainTable, query string, args []interface{}, hyd sqlbuild.CountsHydration) ([]*types.IssueWithCounts, error) {
	rows, err := tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("search count %s: %w", mainTable, err)
	}
	defer func() { _ = rows.Close() }()

	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)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the driver error; for 'no such table', run schema migration (bd migrate / bd doctor)
  2. Validate the query/sort expression (bd query '<expr>' syntax) if the cause is an SQL syntax error
  3. Check for concurrent writers/locks and retry
  4. Raise the context timeout or reduce the filter scope
  5. If the SQL build itself is at fault, check the sqlbuild version matches your storage layer

Example fix

// before: old DB missing table joined by the counts projection
res, err := issueops.ReadyIssuesWithCounts(ctx, filter) // 'no such table: wisp_dependencies'
// after: bring schema current first
if err := store.Migrate(ctx); err != nil { return err }
res, err := issueops.ReadyIssuesWithCounts(ctx, filter)
Defensive patterns

Strategy: validation

Validate before calling

// validate schema and expression before a counts query
if err := store.ValidateSchema(ctx); err != nil {
    return fmt.Errorf("run bd migrate: %w", err)
}
if _, _, err := issueops.BuildIssueFilterClauses(query, filter, tables); err != nil {
    return fmt.Errorf("invalid query/filter: %w", err)
}

Type guard

func isCountsQueryErr(err error) bool {
    if err == nil { return false }
    for _, t := range []string{"issues", "wisps"} {
        if strings.HasPrefix(err.Error(), "search count "+t+": ") {
            return !strings.Contains(err.Error(), ": rows: ")
        }
    }
    return false
}

Try / catch

res, err := issueops.SearchIssuesWithCounts(ctx, q, filter)
if err != nil && isCountsQueryErr(err) {
    if strings.Contains(err.Error(), "no such table") {
        if migErr := store.Migrate(ctx); migErr != nil { return migErr }
        res, err = issueops.SearchIssuesWithCounts(ctx, q, filter)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: runReadyCountsInTx or runSearchQueryInTx invokes scanCountsRowsInTx and the count-projection SELECT fails — missing table (e.g. wisps on old schemas), bad sort/filter expression reaching ORDER BY, DB locked, connection dropped, or ctx cancelled.

Common situations: bd ready / bd list --counts against an older DB missing a joined optional table; an invalid sort key or query expression producing invalid SQL; write-lock contention during a bulk import; remote Dolt outage.

Understand the failure class

Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.

Related errors


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