gastownhall/beads · error

db: DependencySQLRepository.GetBlockingInfo: inbound: %w

Error message

db: DependencySQLRepository.GetBlockingInfo: inbound: %w

What it means

Wraps a failure from the inbound scanBlockingRows query in GetBlockingInfo (SELECT ... WHERE depends_on_id IN (...) AND type='blocks'), which computes the BlockedBy map. The outbound half already succeeded; this failure discards everything and returns an empty BlockingInfo with the error.

Source

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

	//nolint:gosec // G201: table and depTargetExpr are hardcoded constants
	outQ := fmt.Sprintf(
		"SELECT issue_id, %s AS depends_on_id, type FROM %s WHERE issue_id IN (%s) AND type IN ('blocks', 'parent-child')",
		depTargetExpr, table, idPlaceholders,
	)
	outRows, err := r.scanBlockingRows(ctx, outQ, idArgs)
	if err != nil {
		return domain.BlockingInfo{}, fmt.Errorf("db: DependencySQLRepository.GetBlockingInfo: outbound: %w", err)
	}

	//nolint:gosec // G201: table and depTargetExpr are hardcoded constants
	inQ := fmt.Sprintf(
		"SELECT issue_id, %s AS depends_on_id, type FROM %s WHERE %s IN (%s) AND type = 'blocks'",
		depTargetExpr, table, depTargetExpr, idPlaceholders,
	)
	inRows, err := r.scanBlockingRows(ctx, inQ, idArgs)
	if err != nil {
		return domain.BlockingInfo{}, fmt.Errorf("db: DependencySQLRepository.GetBlockingInfo: inbound: %w", err)
	}

	statusIDs := make(map[string]struct{})
	for _, row := range outRows {
		statusIDs[row.dependsOnID] = struct{}{}
	}
	for _, row := range inRows {
		statusIDs[row.dependsOnID] = struct{}{}
	}
	statusByID, err := r.loadStatusByID(ctx, statusIDs)
	if err != nil {
		return domain.BlockingInfo{}, fmt.Errorf("db: DependencySQLRepository.GetBlockingInfo: status lookup: %w", err)
	}

	for _, row := range outRows {
		if statusByID[row.dependsOnID] == types.StatusClosed {
			continue
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the whole GetBlockingInfo call; both halves are needed for a consistent result.
  2. Chunk issueIDs to stay within placeholder limits.
  3. Verify UseWispsTable points at the correct deps table.
  4. Check connection health/pool timeouts if the second query repeatedly fails after the first.

Example fix

// before
info, err := deps.GetBlockingInfo(ctx, ids, opts)
if err != nil { return err } // possibly transient (inbound) failure
// after
info, err := retry.Do(ctx, 3, backoff, func() (domain.BlockingInfo, error) {
    return deps.GetBlockingInfo(ctx, ids, opts)
})
Defensive patterns

Strategy: retry

Validate before calling

if len(issueIDs) == 0 { return domain.BlockingInfo{}, nil }
if len(issueIDs) > 500 { /* chunk before calling */ }

Try / catch

info, err := deps.GetBlockingInfo(ctx, ids, opts)
if err != nil && strings.Contains(err.Error(), "GetBlockingInfo: inbound") {
    if isTransientDBError(err) {
        info, err = deps.GetBlockingInfo(ctx, ids, opts) // retry whole call
    }
}

Prevention

When it happens

Trigger: GetBlockingInfo(ctx, issueIDs, opts): the inbound SELECT fails or rows fail to scan — connection dropped between the two queries, placeholder limit exceeded, wrong wisps/issues table, corrupted dep rows.

Common situations: Same bulk blocking-info contexts as outbound, plus mid-call connection loss where only the second query 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/0fb71abe8fc84660. Report an issue: GitHub.