gastownhall/beads · error

get next child ID: update counter: %w

Error message

get next child ID: update counter: %w

What it means

GetNextChildIDTx persists the incremented counter via INSERT ... ON DUPLICATE KEY UPDATE on the counter table; any failure is wrapped as "get next child ID: update counter: %w". If this fails, the child ID was computed but not created and the transaction should roll back. Called from ExecuteCreate.

Source

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

			return "", fmt.Errorf("get next child ID: scan child row: %w", err)
		}
		_, childNum, ok := ParseHierarchicalID(id)
		if ok && childNum > lastChild {
			lastChild = childNum
		}
	}
	if err := rows.Err(); err != nil {
		return "", fmt.Errorf("get next child ID: iterate children: %w", err)
	}

	nextChild := lastChild + 1

	//nolint:gosec // G201: counterTable is one of two hardcoded constants.
	if _, err := tx.ExecContext(ctx, fmt.Sprintf(`
		INSERT INTO %s (parent_id, last_child) VALUES (?, ?)
		ON DUPLICATE KEY UPDATE last_child = ?
	`, counterTable), parentID, nextChild, nextChild); err != nil {
		return "", fmt.Errorf("get next child ID: update counter: %w", err)
	}

	return fmt.Sprintf("%s.%d", parentID, nextChild), nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the create transaction; the ON DUPLICATE KEY UPDATE is idempotent so a retry recomputes the correct next child.
  2. Check wrapped error for 'lock wait timeout' or 'deadlock' and serialize creation under the same parent.
  3. Run schema migrations so the counter table exists.
  4. Keep the parent-create transaction short to reduce counter-row contention.

Example fix

// before
id, err := issueops.GetNextChildIDTx(ctx, tx, parentID)
if err != nil { return err } // dead transaction reused later
// after
id, err := issueops.GetNextChildIDTx(ctx, tx, parentID)
if err != nil {
    tx.Rollback()
    return fmt.Errorf("create child: %w", err) // retry whole tx
}
Defensive patterns

Strategy: retry

Validate before calling

if ctx.Err() != nil { return ctx.Err() }

Try / catch

id, err := issueops.GetNextChildIDTx(ctx, tx, parentID)
if err != nil {
    tx.Rollback()
    return retry(fmt.Errorf("counter update failed: %w", err)) // safe: idempotent recompute
}

Prevention

When it happens

Trigger: Updating the counter when: the counter table is missing, a lock-wait/deadlock occurs with a concurrent creator of the same parent, the transaction is aborted, or the connection drops.

Common situations: Two agents concurrently creating sub-issues under the same parent (lock contention on the counter row); unmigrated schema; long transactions hitting innodb_lock_wait_timeout.

Related errors


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