gastownhall/beads · error

db: ChildCounterSQLRepository.NextChildID: parentID must not

Error message

db: ChildCounterSQLRepository.NextChildID: parentID must not be empty

What it means

NextChildID returns this error when the parentID argument is an empty string. The child-counter repository needs a non-empty parent issue ID to generate the next child identifier. It is a defensive repo-layer validation guard before any SQL is executed.

Source

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

	"strings"

	"github.com/steveyegge/beads/internal/storage/dberrors"
	"github.com/steveyegge/beads/internal/storage/domain"
)

func NewChildCounterSQLRepository(runner Runner) domain.ChildCounterSQLRepository {
	return &childCounterSQLRepositoryImpl{runner: runner}
}

type childCounterSQLRepositoryImpl struct {
	runner Runner
}

var _ domain.ChildCounterSQLRepository = (*childCounterSQLRepositoryImpl)(nil)

func (r *childCounterSQLRepositoryImpl) NextChildID(ctx context.Context, parentID string, _ domain.ChildCounterOpts) (string, error) {
	if parentID == "" {
		return "", errors.New("db: ChildCounterSQLRepository.NextChildID: parentID must not be empty")
	}

	counterTable, issueTable := "child_counters", "issues"
	parentIsWisp, err := r.parentIsActiveWisp(ctx, parentID)
	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 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Load/resolve the parent issue and pass its real ID to NextChildID.
  2. Add an upstream check that parentID is non-empty before calling the counter.
  3. Trace where the empty ID originates (missing field in input JSON, unset struct field) and fix at the source.

Example fix

// before
childID, err := counter.NextChildID(ctx, "", opts)
// after
if parent.ID == "" {
    return fmt.Errorf("parent issue not loaded")
}
childID, err := counter.NextChildID(ctx, parent.ID, opts)
Defensive patterns

Strategy: validation

Validate before calling

if parentID == "" {
    return fmt.Errorf("parentID required before NextChildID")
}

Try / catch

childID, err := counter.NextChildID(ctx, parentID, opts)
if err != nil && strings.Contains(err.Error(), "parentID must not be empty") {
    // resolve/load parent and retry
}

Prevention

When it happens

Trigger: Calling ChildCounterSQLRepository.NextChildID with parentID == "", typically from an upstream call site where the parent issue was not loaded or its ID field was never populated.

Common situations: Constructing a new issue programmatically without setting ParentID; a partially hydrated issue struct passed down; deserialization gap leaving the parent reference empty.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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