gastownhall/beads · error

failed to read issue counter after increment for prefix %q:

Error message

failed to read issue counter after increment for prefix %q: %w

What it means

After incrementing (or seeding+incrementing) the issue_counter row, NextCounterIDTx re-reads last_id with SELECT ... WHERE prefix = ? and the scan failed. Since a row must exist by this point (increment matched or fallback insert ran), this usually means the row vanished inside the transaction, the tx was rolled back, or a DB-level error occurred.

Source

Thrown at internal/storage/issueops/helpers.go:264

		if err != nil {
			return "", fmt.Errorf("failed to increment issue counter after seeding for prefix %q: %w", prefix, err)
		}
		rowsAffected, err = res.RowsAffected()
		if err != nil {
			return "", fmt.Errorf("failed to check rows affected after seeding for prefix %q: %w", prefix, err)
		}
		if rowsAffected == 0 {
			_, err = tx.ExecContext(ctx, "INSERT INTO issue_counter (prefix, last_id) VALUES (?, 1)", prefix)
			if err != nil {
				return "", fmt.Errorf("failed to insert initial issue counter for prefix %q: %w", prefix, err)
			}
		}
	}

	var nextID int
	err = tx.QueryRowContext(ctx, "SELECT last_id FROM issue_counter WHERE prefix = ?", prefix).Scan(&nextID)
	if err != nil {
		return "", fmt.Errorf("failed to read issue counter after increment for prefix %q: %w", prefix, err)
	}
	return fmt.Sprintf("%s-%d", prefix, nextID), nil
}

// SeedCounterFromExistingIssuesTx scans existing issues to find the highest numeric suffix
// for the given prefix, then seeds the issue_counter table if no row exists yet.
func SeedCounterFromExistingIssuesTx(ctx context.Context, tx DBTX, prefix string) error {
	var existing int
	err := tx.QueryRowContext(ctx, "SELECT last_id FROM issue_counter WHERE prefix = ?", prefix).Scan(&existing)
	if err == nil {
		return nil // already seeded
	}
	if err != sql.ErrNoRows {
		return fmt.Errorf("failed to check existing counter for prefix %q: %w", prefix, err)
	}

	// Find max numeric suffix among existing issues
	rows, err := tx.QueryContext(ctx, `SELECT id FROM issues WHERE id LIKE CONCAT(?, '-%')`, prefix)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped error: if sql.ErrNoRows, verify the counter row still exists for the exact prefix (SELECT * FROM issue_counter WHERE prefix='<P>').
  2. Ensure a single writer path: avoid running multiple bd processes against the same DB concurrently; use bd's locking or serialize ID generation.
  3. Retry the ID generation inside a fresh transaction; counter increment + read is meant to be atomic within one tx.
  4. Confirm the prefix passed to GenerateIssueIDInTable matches the stored prefix exactly (case, trailing dash — ReadConfigPrefix trims trailing '-').

Example fix

// before
id, err := storage.GenerateIssueIDInTable(ctx, tx, "issues", prefix, issue) // fails when counter row missing
// after: ensure counter row is seeded before generating
_, err = tx.ExecContext(ctx, "INSERT IGNORE INTO issue_counter (prefix, last_id) VALUES (?, 0)", prefix)
if err != nil { return err }
id, err = storage.GenerateIssueIDInTable(ctx, tx, "issues", prefix, issue)
Defensive patterns

Strategy: retry

Validate before calling

var n int
if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM issue_counter WHERE prefix = ?", prefix).Scan(&n); err != nil || n == 0 {
    if _, err := tx.ExecContext(ctx, "INSERT IGNORE INTO issue_counter (prefix, last_id) VALUES (?, 0)", prefix); err != nil {
        return err
    }
}

Try / catch

id, err := storage.GenerateIssueIDInTable(ctx, tx, "issues", prefix, issue)
if err != nil && strings.Contains(err.Error(), "failed to read issue counter after increment") {
    // retry once in a fresh transaction; counter ops must be atomic
    tx2, _ := db.BeginTx(ctx, nil)
    id, err = storage.GenerateIssueIDInTable(ctx, tx2, "issues", prefix, issue)
}

Prevention

When it happens

Trigger: GenerateIssueIDInTable in counter mode: the post-increment SELECT last_id FROM issue_counter WHERE prefix = ? returns sql.ErrNoRows or a driver error — e.g. concurrent transaction deleted the row, prefix mismatch after mutation, or connection failure during the query.

Common situations: Concurrent `bd` processes deleting/resetting counter rows; transaction timeout under Dolt under load; prefix containing characters that get normalized differently between the UPDATE and the SELECT; transient DB disconnection.

Related errors


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