gastownhall/beads · error

db: ChildCounterSQLRepository.NextChildID: probe parent tabl

Error message

db: ChildCounterSQLRepository.NextChildID: probe parent table for %s: %w

What it means

Wrapping error from ChildCounterSQLRepository.NextChildID at internal/storage/domain/db/child_counter.go:33. Before reading the counter, NextChildID probes which table the parent lives in (issues vs wisps) via r.parentIsActiveWisp; if that probe query fails, the child-ID generation aborts with this error. The %w chain preserves the SQL-level cause; parentID is interpolated into the message.

Source

Thrown at internal/storage/domain/db/child_counter.go:33

func NewChildCounterSQLRepository(runner Runner) domain.ChildCounterSQLRepository {
	return &childCounterSQLRepositoryImpl{runner: runner}
}

type childCounterSQLRepositoryImpl struct {
	runner Runner
}

var _ domain.ChildCounterSQLRepository = (*childCounterSQLRepositoryImpl)(nil)

func (r *childCounterSQLRepositoryImpl) NextChildID(ctx context.Context, parentID string, _ domain.ChildCounterOpts) (string, error) {
	if parentID == "" {
		return "", errors.New("db: ChildCounterSQLRepository.NextChildID: parentID must not be empty")
	}

	counterTable, issueTable := "child_counters", "issues"
	parentIsWisp, err := r.parentIsActiveWisp(ctx, parentID)
	if err != nil {
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: probe parent table for %s: %w", parentID, err)
	}
	if parentIsWisp {
		counterTable, issueTable = "wisp_child_counters", "wisps"
	}

	var lastChild int
	err = r.runner.QueryRowContext(ctx,
		//nolint:gosec // G201: counterTable is one of two hardcoded constants
		fmt.Sprintf("SELECT last_child FROM %s WHERE parent_id = ?", counterTable),
		parentID,
	).Scan(&lastChild)
	switch {
	case err == nil:
	case errors.Is(err, sql.ErrNoRows):
		lastChild = 0
	default:
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: read counter for %s: %w", parentID, err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the chain to see the SQL error from the parent-table probe
  2. Verify the parent ID exists and the issues/wisps tables are readable
  3. Reconnect to the database and retry
  4. If bulk-generating children, batch with retries around transient connection errors

Example fix

// before
parentIsWisp, err := r.parentIsActiveWisp(ctx, parentID)
if err != nil {
	return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: probe parent table for %s: %w", parentID, err)
}
// after
parentIsWisp, err := r.parentIsActiveWisp(ctx, parentID)
if err != nil {
	return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: probe parent table for %s: %w", parentID, err) // caller retries on transient errors
}
Defensive patterns

Strategy: retry

Validate before calling

// validate inputs and storage before generating child IDs
if parentID == "" {
	return errors.New("parentID must not be empty")
}
if err := pingStore(ctx); err != nil {
	return fmt.Errorf("cannot generate child id, storage unreachable: %w", err)
}

Try / catch

id, err := repo.NextChildID(ctx, parentID)
if err != nil && strings.Contains(err.Error(), "probe parent table") {
	if isTransient(errors.Unwrap(err)) { // connection refused, deadline, lock timeout
		backoff := 100 * time.Millisecond
		for i := 0; i < 3; i++ {
			time.Sleep(backoff)
			backoff *= 2
			if id, err = repo.NextChildID(ctx, parentID); err == nil {
				break
			}
		}
	}
}
return id, err

Prevention

When it happens

Trigger: Calling NextChildID(ctx, parentID) with a non-empty parentID whose table probe (parentIsActiveWisp) returns a DB error — SQL failure, connection loss, cancelled context. (An empty parentID produces a different, non-wrapped error on the preceding line.)

Common situations: Parent issue ID referencing a row in a corrupted DB; connection pool exhausted; Dolt server restarted mid-command while generating child IDs in bulk.

Related errors


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