{"record":{"id":"a0280df97a3deee8","repo":"gastownhall/beads","slug":"db-childcountersqlrepository-nextchildid-parenti","errorCode":null,"errorMessage":"db: ChildCounterSQLRepository.NextChildID: parentID must not be empty","messagePattern":"db: ChildCounterSQLRepository\\.NextChildID: parentID must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/child_counter.go","lineNumber":27,"sourceCode":"\t\"strings\"\n\n\t\"github.com/steveyegge/beads/internal/storage/dberrors\"\n\t\"github.com/steveyegge/beads/internal/storage/domain\"\n)\n\nfunc NewChildCounterSQLRepository(runner Runner) domain.ChildCounterSQLRepository {\n\treturn &childCounterSQLRepositoryImpl{runner: runner}\n}\n\ntype childCounterSQLRepositoryImpl struct {\n\trunner Runner\n}\n\nvar _ domain.ChildCounterSQLRepository = (*childCounterSQLRepositoryImpl)(nil)\n\nfunc (r *childCounterSQLRepositoryImpl) NextChildID(ctx context.Context, parentID string, _ domain.ChildCounterOpts) (string, error) {\n\tif parentID == \"\" {\n\t\treturn \"\", errors.New(\"db: ChildCounterSQLRepository.NextChildID: parentID must not be empty\")\n\t}\n\n\tcounterTable, issueTable := \"child_counters\", \"issues\"\n\tparentIsWisp, err := r.parentIsActiveWisp(ctx, parentID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"db: ChildCounterSQLRepository.NextChildID: probe parent table for %s: %w\", parentID, err)\n\t}\n\tif parentIsWisp {\n\t\tcounterTable, issueTable = \"wisp_child_counters\", \"wisps\"\n\t}\n\n\tvar lastChild int\n\terr = r.runner.QueryRowContext(ctx,\n\t\t//nolint:gosec // G201: counterTable is one of two hardcoded constants\n\t\tfmt.Sprintf(\"SELECT last_child FROM %s WHERE parent_id = ?\", counterTable),\n\t\tparentID,\n\t).Scan(&lastChild)\n\tswitch {","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/child_counter.go#L9-L45","documentation":"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.","triggerScenarios":"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.","commonSituations":"Constructing a new issue programmatically without setting ParentID; a partially hydrated issue struct passed down; deserialization gap leaving the parent reference empty.","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."],"exampleFix":"// before\nchildID, err := counter.NextChildID(ctx, \"\", opts)\n// after\nif parent.ID == \"\" {\n    return fmt.Errorf(\"parent issue not loaded\")\n}\nchildID, err := counter.NextChildID(ctx, parent.ID, opts)","handlingStrategy":"validation","validationCode":"if parentID == \"\" {\n    return fmt.Errorf(\"parentID required before NextChildID\")\n}","typeGuard":null,"tryCatchPattern":"childID, err := counter.NextChildID(ctx, parentID, opts)\nif err != nil && strings.Contains(err.Error(), \"parentID must not be empty\") {\n    // resolve/load parent and retry\n}","preventionTips":["Validate parentID at the command/service boundary","Load the parent issue before generating child IDs","Check struct hydration after deserialization"],"tags":["validation","child-counter","go","storage"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}