gastownhall/beads · error

get next child ID: read counter: %w

Error message

get next child ID: read counter: %w

What it means

GetNextChildIDTx reads the child-ID counter row for a parent from the counter table (SELECT last_child WHERE parent_id = ?) and wraps any Scan error other than ErrNoRows with "get next child ID: read counter: %w". ErrNoRows is treated as 'no children yet' (counter 0), so this error means the counter lookup failed for a real reason. Called from ExecuteCreate when generating hierarchical IDs like bd-12.3.

Source

Thrown at internal/storage/issueops/child_id.go:23

	"database/sql"
	"fmt"
)

func GetNextChildIDTx(ctx context.Context, tx *sql.Tx, parentID string) (string, error) {
	counterTable, issueTable := "child_counters", "issues"
	if IsActiveWispInTx(ctx, tx, parentID) {
		counterTable, issueTable = "wisp_child_counters", "wisps"
	}

	var lastChild int
	//nolint:gosec // G201: counterTable is one of two hardcoded constants.
	err := tx.QueryRowContext(ctx,
		fmt.Sprintf("SELECT last_child FROM %s WHERE parent_id = ?", counterTable),
		parentID).Scan(&lastChild)
	if err == sql.ErrNoRows {
		lastChild = 0
	} else if err != nil {
		return "", fmt.Errorf("get next child ID: read counter: %w", err)
	}

	//nolint:gosec // G201: issueTable is one of two hardcoded constants.
	rows, err := tx.QueryContext(ctx, fmt.Sprintf(`
		SELECT id FROM %s
		WHERE id LIKE CONCAT(?, '.%%')
		  AND id NOT LIKE CONCAT(?, '.%%.%%')
	`, issueTable), parentID, parentID)
	if err != nil {
		return "", fmt.Errorf("get next child ID: query existing children: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return "", fmt.Errorf("get next child ID: scan child row: %w", err)
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Migrate the database so the child counter table exists.
  2. Check the wrapped error text: 'no such table' -> migrate; conversion/scan type error -> inspect the counter row's last_child value.
  3. Retry the create transaction if the failure was a transient connection error.
  4. Verify the parentID is a valid existing issue ID before creating children.
Defensive patterns

Strategy: retry

Validate before calling

parent, err := store.GetIssue(ctx, parentID)
if err != nil || parent == nil { return fmt.Errorf("parent %s does not exist", parentID) }

Try / catch

id, err := issueops.GetNextChildIDTx(ctx, tx, parentID)
if err != nil {
    tx.Rollback()
    return retryable(fmt.Errorf("child id generation failed: %w", err))
}

Prevention

When it happens

Trigger: Calling ExecuteCreate/GetNextChildIDTx when: the counter table is missing or corrupted, the parent_id argument is malformed/NULL causing a driver error, the transaction is dead, or the connection drops during the row read.

Common situations: Creating sub-issues against an unmigrated database (counter table absent); a corrupted counter row with a non-integer last_child that fails to scan into int; connection reset while creating children in bulk.

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/2abb5d1d2e7c1174. Report an issue: GitHub.