gastownhall/beads · error
db: NextCounterID: prefix must not be empty
Error message
db: NextCounterID: prefix must not be empty
What it means
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.
Source
Thrown at internal/storage/domain/db/issue.go:642
}
table := pickIssueTable(opts.UseWispsTable)
var count int
//nolint:gosec // G201: table is one of two hardcoded constants
err := r.runner.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COUNT(*)
FROM %s
WHERE id LIKE CONCAT(?, '-%%')
AND INSTR(SUBSTRING(id, LENGTH(?) + 2), '.') = 0
`, table), prefix, prefix).Scan(&count)
if err != nil {
return 0, fmt.Errorf("db: CountForPrefix %s: %w", prefix, err)
}
return count, nil
}
func (r *issueSQLRepositoryImpl) NextCounterID(ctx context.Context, prefix string) (int, error) {
if prefix == "" {
return 0, errors.New("db: NextCounterID: prefix must not be empty")
}
res, err := r.runner.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
if err != nil {
return 0, fmt.Errorf("db: NextCounterID: increment %q: %w", prefix, err)
}
rows, err := res.RowsAffected()
if err != nil {
return 0, fmt.Errorf("db: NextCounterID: rows affected %q: %w", prefix, err)
}
if rows == 0 {
if err := r.seedCounterFromExisting(ctx, prefix); err != nil {
return 0, fmt.Errorf("db: NextCounterID: seed %q: %w", prefix, err)
}
res, err = r.runner.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
if err != nil {
return 0, fmt.Errorf("db: NextCounterID: increment after seed %q: %w", prefix, err)View on GitHub (pinned to 71377f2769)
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
Example fix
// before
n, err := repo.NextCounterID(ctx, prefixFromID(rawID))
// after
prefix := prefixFromID(rawID)
if prefix == "" {
return errors.New("cannot derive prefix from id %q")
}
n, err := repo.NextCounterID(ctx, prefix) Defensive patterns
Strategy: validation
Validate before calling
if prefix == "" {
return errors.New("cannot mint id: prefix is empty")
} Type guard
func canMintID(prefix string) bool { return strings.TrimSpace(prefix) != "" } Try / catch
n, err := repo.NextCounterID(ctx, prefix)
if err != nil {
return fmt.Errorf("next id for %q: %w", prefix, err)
} Prevention
- 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
When it happens
Trigger: Calling issueSQLRepositoryImpl.NextCounterID(ctx, "") — usually when the configured issue prefix is empty or a caller computed the prefix from a malformed identifier.
Common situations: 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.
Related errors
- db: Exists: id must not be empty
- db: CountForPrefix: prefix must not be empty
- failed to check for ID collision: %w
- failed to generate unique ID after trying lengths %d-%d with
- no store is open for this workspace
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/34bb4a71a2e18871.
Report an issue: GitHub.