gastownhall/beads · error

db: DependencySQLRepository.CountsByIssueIDs (in): %w

Error message

db: DependencySQLRepository.CountsByIssueIDs (in): %w

What it means

Wraps a failure from the incoming 'blocks' count query in CountsByIssueIDs (COUNT(*) grouped by depends_on_id for the DependentCount field). The outgoing count succeeded but the inbound aggregate failed, so the whole counts map is discarded (returns nil, error).

Source

Thrown at internal/storage/domain/db/dependency.go:476

	idPlaceholders, idArgs := buildInPlaceholders(issueIDs)
	table := pickDepTable(opts.UseWispsTable)

	//nolint:gosec // G201: table is one of two hardcoded constants
	outQ := fmt.Sprintf(
		`SELECT issue_id, COUNT(*) FROM %s WHERE issue_id IN (%s) AND type = 'blocks' GROUP BY issue_id`,
		table, idPlaceholders,
	)
	if err := scanCounts(ctx, r.runner, outQ, idArgs, result, func(c *types.DependencyCounts, n int) { c.DependencyCount = n }); err != nil {
		return nil, fmt.Errorf("db: DependencySQLRepository.CountsByIssueIDs (out): %w", err)
	}

	//nolint:gosec // G201: table and depTargetExpr are hardcoded
	inQ := fmt.Sprintf(
		`SELECT %s AS depends_on_id, COUNT(*) FROM %s WHERE %s IN (%s) AND type = 'blocks' GROUP BY %s`,
		depTargetExpr, table, depTargetExpr, idPlaceholders, depTargetExpr,
	)
	if err := scanCounts(ctx, r.runner, inQ, idArgs, result, func(c *types.DependencyCounts, n int) { c.DependentCount = n }); err != nil {
		return nil, fmt.Errorf("db: DependencySQLRepository.CountsByIssueIDs (in): %w", err)
	}

	return result, nil
}

func (r *dependencySQLRepositoryImpl) GetBlockingInfo(ctx context.Context, issueIDs []string, opts domain.DepListOpts) (domain.BlockingInfo, error) {
	info := domain.BlockingInfo{
		BlockedBy: make(map[string][]string),
		Blocks:    make(map[string][]string),
		Parent:    make(map[string]string),
	}
	if len(issueIDs) == 0 {
		return info, nil
	}

	table := pickDepTable(opts.UseWispsTable)
	idPlaceholders, idArgs := buildInPlaceholders(issueIDs)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the whole call; counts are cheap read-only aggregates and both halves must succeed for a consistent map.
  2. Chunk issueIDs to avoid placeholder limits.
  3. Verify UseWispsTable matches the deps table.
  4. Check connection stability/pool settings if only the second query keeps failing.

Example fix

// before
counts, err := deps.CountsByIssueIDs(ctx, ids, opts)
if err != nil { return nil, err } // may be the (in) half failing transiently
// after
counts, err := retry.Do(ctx, 3, backoff, func() (map[string]*types.DependencyCounts, error) {
    return deps.CountsByIssueIDs(ctx, ids, opts)
})
Defensive patterns

Strategy: retry

Validate before calling

if len(issueIDs) == 0 { return map[string]*types.DependencyCounts{}, nil }
if len(issueIDs) > 500 { /* chunk before calling */ }

Try / catch

counts, err := deps.CountsByIssueIDs(ctx, ids, opts)
if err != nil {
    if isTransientDBError(err) {
        counts, err = deps.CountsByIssueIDs(ctx, ids, opts) // retry whole call for consistency
    }
}

Prevention

When it happens

Trigger: CountsByIssueIDs(ctx, issueIDs, opts): the inbound aggregate SELECT fails or scanCounts errors — connection drop between the two queries, placeholder limit, wisps-table mismatch, or type-scan mismatch.

Common situations: Same bulk-count contexts as (out), plus transient mid-batch connection loss where the first query succeeds and the second fails.

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/3222a3b904b9f378. Report an issue: GitHub.