gastownhall/beads · error

db: ChildCounterSQLRepository.NextChildID: read counter for

Error message

db: ChildCounterSQLRepository.NextChildID: read counter for %s: %w

What it means

Wrapping error from ChildCounterSQLRepository.NextChildID at internal/storage/domain/db/child_counter.go:50. After resolving the counter table (child_counters or wisp_child_counters), the code reads the last used child number with a scalar query. A missing row is handled (lastChild = 0); any other scan/query error is wrapped with this message, meaning the counter row exists but could not be read (or the query itself failed).

Source

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

	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)
	}

	rows, err := r.runner.QueryContext(ctx, fmt.Sprintf(`
		SELECT id FROM %s
		WHERE id LIKE CONCAT(?, '.%%')
		  AND id NOT LIKE CONCAT(?, '.%%.%%')
	`, issueTable), parentID, parentID) //nolint:gosec // G201: issueTable is one of two hardcoded constants
	if err != nil {
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: scan existing children of %s: %w", parentID, err)
	}
	defer rows.Close()
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: scan: %w", err)
		}
		if n, ok := parseChildSuffix(id); ok && n > lastChild {
			lastChild = n

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the chain to see the exact SQL error
  2. Check for row locks / concurrent writers on the counter table and serialize writes
  3. Verify the counter table schema matches the running bd version
  4. Retry the operation after resolving contention or corruption
Defensive patterns

Strategy: retry

Validate before calling

// confirm the parent's counter row is readable before generating children
var last int
err := store.QueryRowContext(ctx,
	"SELECT last_child FROM child_counters WHERE parent = ?", parentID).Scan(&last)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
	return fmt.Errorf("counter row unreadable: %w", err)
}

Try / catch

id, err := repo.NextChildID(ctx, parentID)
if err != nil && strings.Contains(err.Error(), "read counter") {
	if isLockContention(errors.Unwrap(err)) {
		time.Sleep(250 * time.Millisecond) // let the competing writer finish
		id, err = repo.NextChildID(ctx, parentID)
	}
}
return id, err

Prevention

When it happens

Trigger: Calling NextChildID when the SELECT of the counter row for parentID fails with an error other than sql.ErrNoRows — lock contention on the counter row, SQL syntax/type error, dropped connection.

Common situations: Two processes contending for the same parent's counter row under heavy concurrency; counter table corrupted; migration left the counter table in an incompatible state.

Related errors


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