{"record":{"id":"dc99b63e1046f459","repo":"gastownhall/beads","slug":"get-events-w","errorCode":null,"errorMessage":"get events: %w","messagePattern":"get events: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/events.go","lineNumber":32,"sourceCode":"//nolint:gosec // G201: table is hardcoded via WispTableRouting\nfunc GetEventsInTx(ctx context.Context, tx DBTX, issueID string, limit int) ([]*types.Event, error) {\n\t_, _, eventTable, _ := WispTableRouting(IsActiveWispInTx(ctx, tx, issueID))\n\n\tquery := fmt.Sprintf(`\n\t\tSELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at\n\t\tFROM %s\n\t\tWHERE issue_id = ?\n\t\tORDER BY created_at DESC\n\t`, eventTable)\n\targs := []interface{}{issueID}\n\n\tif limit > 0 {\n\t\tquery += fmt.Sprintf(\" LIMIT %d\", limit)\n\t}\n\n\trows, err := tx.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get events: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\treturn scanEvents(rows)\n}\n\n// GetAllEventsSinceInTx returns all events created after the given time,\n// querying both events and wisp_events tables.\nfunc GetAllEventsSinceInTx(ctx context.Context, tx *sql.Tx, since time.Time) ([]*types.Event, error) {\n\trows, err := tx.QueryContext(ctx, `\n\t\tSELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at\n\t\tFROM events\n\t\tWHERE created_at > ?\n\t\tUNION ALL\n\t\tSELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at\n\t\tFROM wisp_events\n\t\tWHERE created_at > ?\n\t\tORDER BY created_at ASC","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/events.go#L14-L50","documentation":"GetEventsInTx builds a parameterized query against the events table (with optional filters and LIMIT) and executes it. Any query-execution failure is wrapped as 'get events'. It means the event history could not be read, commonly due to filter/limit issues or database unavailability.","triggerScenarios":"Calling GetEventsInTx when the composed SELECT on events fails — unknown filter column, malformed limit, connection error, or missing events table.","commonSituations":"Querying events with an issue_id that hits an index/schema mismatch; pre-migration databases lacking the events table; database locked by another writer.","solutions":["Read the wrapped cause: 'no such table: events' means run migrations; 'unknown column' means schema drift.","Retry if the cause is a lock or transient connection error.","Verify filter parameters (issue_id, event_type, since) are valid values/columns.","Check connectivity to the Dolt database."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"var hasEvents int\n_ = db.QueryRow(\"SELECT COUNT(*) FROM information_schema.tables WHERE table_name='events'\").Scan(&hasEvents)\nif hasEvents == 0 { return errors.New(\"events table missing; run migrations\") }\nif limit < 0 { return errors.New(\"limit must be >= 0\") }","typeGuard":"func isGetEventsErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"get events:\")\n}","tryCatchPattern":"events, err := GetEventsInTx(ctx, tx, opts)\nif err != nil {\n    if isGetEventsErr(err) && strings.Contains(err.Error(), \"no such table\") {\n        return migrateThenRetry(ctx)\n    }\n    return fmt.Errorf(\"could not read events: %w\", err)\n}","preventionTips":["Run migrations before reading event history.","Use only documented filter fields and non-negative limits.","Verify DB connectivity before bulk event reads.","Handle lock timeouts by serializing with writers."],"tags":["go","sql","storage","query"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}