gastownhall/beads · error

db: ChildCounterSQLRepository.NextChildID: scan existing chi

Error message

db: ChildCounterSQLRepository.NextChildID: scan existing children of %s: %w

What it means

Wrapping error from ChildCounterSQLRepository.NextChildID at internal/storage/domain/db/child_counter.go:59. After reading the counter, the code queries all existing direct children of the parent (LIKE '<parent>.%' and not deeper) to reconcile the counter with reality. A failure executing that query (QueryContext error) is wrapped with this message. Row-scan failures later in the loop produce a separate 'scan:' variant.

Source

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

		//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
		}
	}
	if err := rows.Err(); err != nil {
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: rows: %w", err)
	}

	next := lastChild + 1
	//nolint:gosec // G201: counterTable is one of two hardcoded constants
	if _, err := r.runner.ExecContext(ctx, fmt.Sprintf(`

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap to see the underlying SQL/driver error
  2. Check query timeouts if the parent has many children (index on id prefix)
  3. Verify connection stability between the counter read and children scan
  4. Retry once the database is reachable
Defensive patterns

Strategy: retry

Validate before calling

// ensure the children listing query runs before generating a new child ID
rows, err := store.QueryContext(ctx,
	"SELECT id FROM issues WHERE id LIKE CONCAT(?, '.%') AND id NOT LIKE CONCAT(?, '.%.%')",
	parentID, parentID)
if err != nil {
	return fmt.Errorf("children of %s unlistable: %w", parentID, err)
}
rows.Close()

Try / catch

id, err := repo.NextChildID(ctx, parentID)
if err != nil && strings.Contains(err.Error(), "scan existing children") {
	if errors.Is(errors.Unwrap(err), context.DeadlineExceeded) {
		longCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()
		id, err = repo.NextChildID(longCtx, parentID)
	}
}
return id, err

Prevention

When it happens

Trigger: Calling NextChildID when the QueryContext listing direct children of parentID fails — connection dropped between the counter read and this query, SQL execution error, or the issue/wisp table is unreadable.

Common situations: Very large parent (many children) causing a slow query that hits a timeout; DB connection interrupted mid-operation; permissions issue on the issues/wisps table.

Related errors


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