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

  1. Initialize/pass the correct prefix (e.g. "bd") before generating ids
  2. If the counter row may not exist, use the repository's create-or-increment path instead of calling with an empty prefix
  3. 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

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


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