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
- Load/resolve the parent issue and pass its real ID to NextChildID.
- Add an upstream check that parentID is non-empty before calling the counter.
- 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
- Validate parentID at the command/service boundary
- Load the parent issue before generating child IDs
- Check struct hydration after deserialization
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
- no store is open for this workspace
- not found
- db: DependencySQLRepository.Insert: dep must not be nil
- db: DependencySQLRepository.Insert: IssueID must not be empt
- db: DependencySQLRepository.Insert: DependsOnID must not be
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a0280df97a3deee8.
Report an issue: GitHub.