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 the counter, the code reads back `SELECT last_id FROM issue_counter WHERE prefix = ?` to form the new issue ID. This error wraps failure of that read: most notably sql.ErrNoRows when the counter row unexpectedly vanished (missing INSERT path, concurrent delete), or a scan/connection error. Without this value the new ID cannot be produced, so ID generation aborts.

Source

Thrown at internal/storage/dolt/issues.go:897

		}
		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 {
			// Seeding found no existing numeric IDs -- insert the initial row.
			_, 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)
			}
		}
	}

	// Read back the value that was atomically set by the DB engine.
	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
}

// isCounterModeTx checks whether issue_id_mode=counter is configured.
func isCounterModeTx(ctx context.Context, tx *sql.Tx) (bool, error) {
	var idMode string
	err := tx.QueryRowContext(ctx, "SELECT value FROM config WHERE `key` = ?", "issue_id_mode").Scan(&idMode)
	if err != nil && err != sql.ErrNoRows {
		return false, fmt.Errorf("failed to read issue_id_mode config: %w", err)
	}
	return idMode == "counter", nil
}

// generateHashID creates a hash-based ID for a top-level issue.
// Uses base36 encoding (0-9, a-z) for better information density than hex.
func generateHashID(prefix, title, description, creator string, timestamp time.Time, length, nonce int) string {
	return idgen.GenerateHashID(prefix, title, description, creator, timestamp, length, nonce)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped cause: if ErrNoRows, verify another process isn't deleting/resetting issue_counter and that you're on the expected database/branch.
  2. Retry the ID generation; a transient connection drop will usually succeed on retry.
  3. Confirm the UPDATE path actually located the row (re-run with rowsAffected handling fixed) so the SELECT finds it.
  4. Inspect issue_counter contents directly (SELECT * FROM issue_counter WHERE prefix='<p>') to see if the row exists.

Example fix

// before
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)
}
// after
err = tx.QueryRowContext(ctx, "SELECT last_id FROM issue_counter WHERE prefix = ?", prefix).Scan(&nextID)
if err == sql.ErrNoRows {
    return "", fmt.Errorf("issue counter row missing for prefix %q after increment; check concurrent writers/branch", prefix)
} else if err != nil {
    return "", fmt.Errorf("failed to read issue counter after increment for prefix %q: %w", prefix, err)
}
Defensive patterns

Strategy: retry

Validate before calling

var n int
if err := db.QueryRow("SELECT COUNT(*) FROM issue_counter WHERE prefix = ?", prefix).Scan(&n); err != nil || n == 0 {
    return fmt.Errorf("counter row for %q missing; concurrent writers or wrong branch", prefix)
}

Type guard

func isCounterRowMissing(err error) bool {
    return errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "read issue counter after increment")
}

Try / catch

id, err := store.CreateIssue(ctx, issue)
if err != nil && strings.Contains(err.Error(), "read issue counter after increment") {
    if isCounterRowMissing(err) {
        return fmt.Errorf("counter row vanished; check concurrent writers and current branch: %w", err)
    }
    // transient read failure: retry once
    id, err = store.CreateIssue(ctx, issue)
}

Prevention

When it happens

Trigger: Counter-mode ID generation where the increment UPDATE ran (or was skipped via a broken rowsAffected path) but the SELECT returns no row or errors — concurrent transaction deleted/never created the row, wrong database/branch context, or connection failure during query.

Common situations: Concurrent writers where one process rolls back the counter row; operating against a different Dolt branch/database than the one the counter was written to; a connection dropped between UPDATE and SELECT.

Related errors


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