{"record":{"id":"ebbadb1fa9cfb636","repo":"gastownhall/beads","slug":"events-since-cursor-v-q-issue-q-w","errorCode":null,"errorMessage":"events since cursor (%v, %q) issue %q: %w","messagePattern":"events since cursor \\((.+?), %q\\) issue %q: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/events.go","lineNumber":107,"sourceCode":"// Scope is the durable `events` table only — wisp_events are deliberately not\n// unioned in, unlike GetAllEventsSince, so the feed stays durable-only.\nfunc EventsSinceInTx(ctx context.Context, tx DBTX, cursorCreatedAt time.Time, cursorID, issueID string, limit int) ([]*types.Event, error) {\n\tif limit <= 0 {\n\t\tlimit = defaultEventsSinceLimit\n\t}\n\tif limit > maxEventsSinceLimit {\n\t\tlimit = maxEventsSinceLimit\n\t}\n\n\tquery := EventsSinceQuery(issueID, limit)\n\targs := []any{cursorCreatedAt, cursorCreatedAt, cursorID}\n\tif issueID != \"\" {\n\t\targs = append(args, issueID)\n\t}\n\n\trows, err := tx.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"events since cursor (%v, %q) issue %q: %w\", cursorCreatedAt, cursorID, issueID, err)\n\t}\n\tdefer rows.Close()\n\n\treturn scanEvents(rows)\n}\n\n// EventsSinceQuery returns the exact SQL EventsSinceInTx executes for the given\n// issueID scope and already-clamped limit, with ? placeholders bound in order by\n// the caller: created_at (sargable lower bound), created_at (strict), id\n// (same-second tie-break), and issue_id when issueID != \"\". It is exported so\n// the backend sargability guard EXPLAINs this production string rather than a\n// hand-copied literal — a change to the SARGABLE predicate here then breaks the\n// guard.\n//\n//nolint:gosec // G201: limit is an int the caller clamps; every runtime value is a bound parameter.\nfunc EventsSinceQuery(issueID string, limit int) string {\n\tquery := `\n\t\tSELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/events.go#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check database connectivity and retry the operation; if the tx is dead, start a new transaction.","Verify the events table schema matches the version of the beads storage layer (run schema migration).","Inspect the wrapped driver error (%w) in the message chain for the root cause.","Validate cursor arguments (non-empty cursorID when cursorCreatedAt is set) before calling."],"exampleFix":"// before\nevents, err := EventsSinceInTx(ctx, tx, cursorTime, cursorID, issueID) // err swallowed\n// after\nif err != nil {\n\treturn fmt.Errorf(\"listing events since %s: %w\", cursorTime, err)\n}","handlingStrategy":"try-catch","validationCode":"if cursorCreatedAt != nil && cursorID == \"\" {\n\treturn fmt.Errorf(\"cursor requires both createdAt and id\")\n}","typeGuard":null,"tryCatchPattern":"events, err := EventsSinceInTx(ctx, tx, cur.CreatedAt, cur.ID, issueID)\nif err != nil {\n\tvar driverErr *driverError // inspect wrapped error via errors.As / errors.Is\n\tif errors.As(err, &driverErr) && isRetryable(driverErr) {\n\t\treturn retryWithNewTx(ctx)\n\t}\n\treturn fmt.Errorf(\"events since %v/%q: %w\", cur.CreatedAt, cur.ID, err)\n}","preventionTips":["Retry with a fresh transaction on connection errors; never reuse an aborted tx.","Keep schema migrations for the events table in lockstep with library upgrades.","Always pass a paired cursor (createdAt + id) to make pagination stable."],"tags":["database","query-failure","events","storage"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}