{"record":{"id":"34bb4a71a2e18871","repo":"gastownhall/beads","slug":"db-nextcounterid-prefix-must-not-be-empty","errorCode":null,"errorMessage":"db: NextCounterID: prefix must not be empty","messagePattern":"db: NextCounterID: prefix must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":642,"sourceCode":"\t}\n\ttable := pickIssueTable(opts.UseWispsTable)\n\tvar count int\n\t//nolint:gosec // G201: table is one of two hardcoded constants\n\terr := r.runner.QueryRowContext(ctx, fmt.Sprintf(`\n\t\tSELECT COUNT(*)\n\t\tFROM %s\n\t\tWHERE id LIKE CONCAT(?, '-%%')\n\t\t  AND INSTR(SUBSTRING(id, LENGTH(?) + 2), '.') = 0\n\t`, table), prefix, prefix).Scan(&count)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"db: CountForPrefix %s: %w\", prefix, err)\n\t}\n\treturn count, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) NextCounterID(ctx context.Context, prefix string) (int, error) {\n\tif prefix == \"\" {\n\t\treturn 0, errors.New(\"db: NextCounterID: prefix must not be empty\")\n\t}\n\n\tres, err := r.runner.ExecContext(ctx, \"UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?\", prefix)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"db: NextCounterID: increment %q: %w\", prefix, err)\n\t}\n\trows, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"db: NextCounterID: rows affected %q: %w\", prefix, err)\n\t}\n\n\tif rows == 0 {\n\t\tif err := r.seedCounterFromExisting(ctx, prefix); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"db: NextCounterID: seed %q: %w\", prefix, err)\n\t\t}\n\t\tres, err = r.runner.ExecContext(ctx, \"UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?\", prefix)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"db: NextCounterID: increment after seed %q: %w\", prefix, err)","sourceCodeStart":624,"sourceCodeEnd":660,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L624-L660","documentation":"NextCounterID() atomically increments the per-prefix row in the issue_counter table to mint the next sequential id number. An empty prefix would not identify a counter row, so the repository rejects it before executing the UPDATE.","triggerScenarios":"Calling issueSQLRepositoryImpl.NextCounterID(ctx, \"\") — usually when the configured issue prefix is empty or a caller computed the prefix from a malformed identifier.","commonSituations":"Fresh project where the ID prefix was never initialized in config/metadata; passing an already-stripped or wrongly-parsed ID; blank environment/config value used as prefix.","solutions":["Initialize/pass the correct prefix (e.g. \"bd\") before generating ids","If the counter row may not exist, use the repository's create-or-increment path instead of calling with an empty prefix","Validate the configured prefix at startup"],"exampleFix":"// before\nn, err := repo.NextCounterID(ctx, prefixFromID(rawID))\n// after\nprefix := prefixFromID(rawID)\nif prefix == \"\" {\n    return errors.New(\"cannot derive prefix from id %q\")\n}\nn, err := repo.NextCounterID(ctx, prefix)","handlingStrategy":"validation","validationCode":"if prefix == \"\" {\n    return errors.New(\"cannot mint id: prefix is empty\")\n}","typeGuard":"func canMintID(prefix string) bool { return strings.TrimSpace(prefix) != \"\" }","tryCatchPattern":"n, err := repo.NextCounterID(ctx, prefix)\nif err != nil {\n    return fmt.Errorf(\"next id for %q: %w\", prefix, err)\n}","preventionTips":["Initialize the project prefix (bd init / metadata.json) before any issue creation","Never derive the prefix from a possibly-empty parsed ID without a fallback","Fail fast at config load when the prefix is blank"],"tags":["go","validation","database","id-generation"],"backgroundTag":"empty-required-argument","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}