gastownhall/beads · error

events since cursor (%v, %q) issue %q: %w

Error message

events since cursor (%v, %q) issue %q: %w

What it means

EventsSinceInTx queries events newer than a (createdAt, id) cursor, optionally filtered by issue ID, inside an existing transaction. This error wraps a low-level SQL failure from tx.QueryContext, so it means the events SELECT itself failed — not that no events were found. The cursor and issue ID are echoed into the message to make the failing query reproducible.

Source

Thrown at internal/storage/issueops/events.go:107

// Scope is the durable `events` table only — wisp_events are deliberately not
// unioned in, unlike GetAllEventsSince, so the feed stays durable-only.
func EventsSinceInTx(ctx context.Context, tx DBTX, cursorCreatedAt time.Time, cursorID, issueID string, limit int) ([]*types.Event, error) {
	if limit <= 0 {
		limit = defaultEventsSinceLimit
	}
	if limit > maxEventsSinceLimit {
		limit = maxEventsSinceLimit
	}

	query := EventsSinceQuery(issueID, limit)
	args := []any{cursorCreatedAt, cursorCreatedAt, cursorID}
	if issueID != "" {
		args = append(args, issueID)
	}

	rows, err := tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("events since cursor (%v, %q) issue %q: %w", cursorCreatedAt, cursorID, issueID, err)
	}
	defer rows.Close()

	return scanEvents(rows)
}

// EventsSinceQuery returns the exact SQL EventsSinceInTx executes for the given
// issueID scope and already-clamped limit, with ? placeholders bound in order by
// the caller: created_at (sargable lower bound), created_at (strict), id
// (same-second tie-break), and issue_id when issueID != "". It is exported so
// the backend sargability guard EXPLAINs this production string rather than a
// hand-copied literal — a change to the SARGABLE predicate here then breaks the
// guard.
//
//nolint:gosec // G201: limit is an int the caller clamps; every runtime value is a bound parameter.
func EventsSinceQuery(issueID string, limit int) string {
	query := `
		SELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check database connectivity and retry the operation; if the tx is dead, start a new transaction.
  2. Verify the events table schema matches the version of the beads storage layer (run schema migration).
  3. Inspect the wrapped driver error (%w) in the message chain for the root cause.
  4. Validate cursor arguments (non-empty cursorID when cursorCreatedAt is set) before calling.

Example fix

// before
events, err := EventsSinceInTx(ctx, tx, cursorTime, cursorID, issueID) // err swallowed
// after
if err != nil {
	return fmt.Errorf("listing events since %s: %w", cursorTime, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if cursorCreatedAt != nil && cursorID == "" {
	return fmt.Errorf("cursor requires both createdAt and id")
}

Try / catch

events, err := EventsSinceInTx(ctx, tx, cur.CreatedAt, cur.ID, issueID)
if err != nil {
	var driverErr *driverError // inspect wrapped error via errors.As / errors.Is
	if errors.As(err, &driverErr) && isRetryable(driverErr) {
		return retryWithNewTx(ctx)
	}
	return fmt.Errorf("events since %v/%q: %w", cur.CreatedAt, cur.ID, err)
}

Prevention

When it happens

Trigger: Calling EventsSinceInTx (directly or via GetEventsInTx / GetAllEventsSinceInTx pagination) when the underlying SQL query fails: malformed query construction, connection loss mid-transaction, or a schema mismatch on the events table.

Common situations: Database connection dropped while paginating a large event log; running against a database whose events table schema is from an older version; transaction already aborted by a prior statement error.

Understand the failure class

Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.

Related errors


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