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 = nView on GitHub (pinned to 71377f2769)
Solutions
- Unwrap the chain to see the exact SQL error
- Check for row locks / concurrent writers on the counter table and serialize writes
- Verify the counter table schema matches the running bd version
- 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
- Serialize child-ID generation per parent (single writer or row lock)
- Watch for concurrent processes creating children of the same parent
- Verify counter-table schema after migrations
- Back off and retry on lock-wait timeouts
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
- db: ChildCounterSQLRepository.NextChildID: probe parent tabl
- db: ChildCounterSQLRepository.NextChildID: scan existing chi
- failed to increment issue counter for prefix %q: %w
- db: LabelSQLRepository.Insert %s/%s: %w
- get next child ID: read counter: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7491543a558dd1e3.
Report an issue: GitHub.