mi6_core/model/storage.rs
1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use chrono::{DateTime, Utc};
6
7use crate::context::GitBranchInfo;
8use crate::input::transcript::FilePosition;
9use crate::model::session::SessionStatus;
10
11use super::error::StorageError;
12use super::{Event, Session, SessionQuery};
13
14/// Aggregated statistics across all sessions from the storage layer.
15///
16/// Provides a summary of session counts, token usage, costs, and API requests
17/// computed at the database level for efficiency.
18///
19/// Note: This is distinct from `mi6_client::GlobalStats` which includes
20/// runtime OS metrics (CPU, memory) for TUI display.
21#[derive(Debug, Clone, Default, PartialEq)]
22pub struct StorageStats {
23 /// Total number of sessions matching the query
24 pub session_count: u32,
25 /// Number of active sessions (ended_at IS NULL)
26 pub active_session_count: u32,
27 /// Total tokens across all sessions (input + output + cache_read + cache_write)
28 pub total_tokens: i64,
29 /// Total cost in USD across all sessions
30 pub total_cost_usd: f64,
31 /// Total API requests across all sessions
32 pub total_api_requests: u32,
33}
34
35/// Query builder for storage statistics.
36///
37/// `StorageStatsQuery` provides a fluent API for filtering which sessions
38/// are included in the aggregation.
39#[derive(Debug, Clone, Default)]
40pub struct StorageStatsQuery {
41 /// When true, only include active sessions (ended_at IS NULL)
42 pub active_only: bool,
43 /// Filter by framework (claude, cursor, gemini, codex)
44 pub framework: Option<String>,
45}
46
47impl StorageStatsQuery {
48 /// Create a new empty query (includes all sessions)
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 /// Filter to only active sessions
54 pub fn active_only(mut self) -> Self {
55 self.active_only = true;
56 self
57 }
58
59 /// Filter by framework
60 pub fn with_framework(mut self, framework: impl Into<String>) -> Self {
61 self.framework = Some(framework.into());
62 self
63 }
64}
65
66/// Sort order for query results
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum Order {
69 /// Ascending order (oldest first for timestamps, lowest first for IDs)
70 Asc,
71 /// Descending order (newest first for timestamps, highest first for IDs)
72 #[default]
73 Desc,
74}
75
76/// Column to order event query results by.
77///
78/// This enum makes the ordering column explicit, eliminating the previous
79/// implicit behavior where ordering was determined by whether `after_id` was set.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum EventOrder {
82 /// Order by event timestamp (default: newest first)
83 #[default]
84 Timestamp,
85 /// Order by event ID (default: ascending for watching new events)
86 Id,
87}
88
89/// Composable query builder for event retrieval.
90///
91/// `EventQuery` provides a fluent API for constructing event queries with
92/// filters, ordering, and pagination.
93///
94/// # Ordering Behavior
95///
96/// The query provides **explicit ordering** via the `order_by` field:
97/// - [`EventOrder::Timestamp`]: Order by event timestamp (default)
98/// - [`EventOrder::Id`]: Order by event ID
99///
100/// Use `order_by_timestamp()` or `order_by_id()` to set the ordering column,
101/// and `with_direction()` to control ascending/descending order.
102///
103/// # Use Cases
104///
105/// - **Recent events**: Use `order_by_timestamp()` with `Order::Desc` (default)
106/// - **Watching new events**: Use `order_by_id()` with `after_id` for pagination
107///
108/// # Defaults
109///
110/// - `order_by`: [`EventOrder::Timestamp`] - orders by timestamp
111/// - `direction`: [`Order::Desc`] for timestamp, [`Order::Asc`] for ID
112///
113/// # Example
114/// ```ignore
115/// // Get 50 most recent events for a session (orders by timestamp DESC)
116/// let query = EventQuery::new()
117/// .order_by_timestamp()
118/// .with_session(Some("session-123".to_string()))
119/// .with_limit(50);
120///
121/// // Watch for new events after ID 100 (orders by ID ASC)
122/// let query = EventQuery::new()
123/// .order_by_id()
124/// .with_after_id(100)
125/// .with_limit(10);
126/// ```
127#[derive(Default, Clone)]
128pub struct EventQuery {
129 /// Filter by session ID (mutually exclusive with `session_ids`)
130 pub session_id: Option<String>,
131 /// Filter by multiple session IDs (mutually exclusive with `session_id`)
132 pub session_ids: Option<Vec<String>>,
133 /// Filter by event type (case-insensitive)
134 pub event_type: Option<String>,
135 /// Filter by permission mode
136 pub permission_mode: Option<String>,
137 /// Filter by framework (claude, cursor, gemini)
138 pub framework: Option<String>,
139 /// Filter events after this timestamp (exclusive)
140 pub after_ts: Option<DateTime<Utc>>,
141 /// Filter events before this timestamp (exclusive)
142 pub before_ts: Option<DateTime<Utc>>,
143 /// Filter events after this ID (exclusive)
144 pub after_id: Option<i64>,
145 /// Maximum number of events to return
146 pub limit: Option<usize>,
147 /// Column to order results by (default: Timestamp)
148 pub order_by: EventOrder,
149 /// Sort direction (default: Desc for timestamp, Asc for ID)
150 pub direction: Option<Order>,
151 /// When true, only return API request events (events with token data)
152 pub api_requests_only: bool,
153 /// When true, exclude API request events
154 pub exclude_api_requests: bool,
155}
156
157impl EventQuery {
158 /// Create a new empty query.
159 ///
160 /// The query defaults to ordering by timestamp (descending).
161 /// Use `order_by_id()` for ID-based ordering (e.g., when watching for new events).
162 pub fn new() -> Self {
163 Self::default()
164 }
165
166 /// Order results by timestamp.
167 ///
168 /// This is the default ordering and is suitable for viewing recent events.
169 /// Default direction is [`Order::Desc`] (newest first).
170 ///
171 /// # Example
172 /// ```ignore
173 /// let query = EventQuery::new()
174 /// .order_by_timestamp()
175 /// .with_limit(50);
176 /// ```
177 pub fn order_by_timestamp(mut self) -> Self {
178 self.order_by = EventOrder::Timestamp;
179 self
180 }
181
182 /// Order results by ID.
183 ///
184 /// Use this when watching for new events with `after_id` pagination.
185 /// Default direction is [`Order::Asc`] (oldest first, for forward pagination).
186 ///
187 /// # Example
188 /// ```ignore
189 /// let query = EventQuery::new()
190 /// .order_by_id()
191 /// .with_after_id(100)
192 /// .with_limit(10);
193 /// ```
194 pub fn order_by_id(mut self) -> Self {
195 self.order_by = EventOrder::Id;
196 self
197 }
198
199 /// Filter events after the given timestamp (exclusive)
200 pub fn with_after_ts(mut self, ts: DateTime<Utc>) -> Self {
201 self.after_ts = Some(ts);
202 self
203 }
204
205 /// Filter events before the given timestamp (exclusive)
206 pub fn with_before_ts(mut self, ts: DateTime<Utc>) -> Self {
207 self.before_ts = Some(ts);
208 self
209 }
210
211 /// Filter events between two timestamps (exclusive on both ends).
212 ///
213 /// Note: If `start >= end`, the filter will return no results.
214 /// The caller is responsible for ensuring `start < end` for meaningful queries.
215 pub fn with_between_ts(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
216 self.after_ts = Some(start);
217 self.before_ts = Some(end);
218 self
219 }
220
221 /// Filter events after the given ID (exclusive).
222 ///
223 /// Use with `order_by_id()` for consistent pagination when watching for new events.
224 ///
225 /// # Example
226 /// ```ignore
227 /// let query = EventQuery::new()
228 /// .order_by_id()
229 /// .with_after_id(last_seen_id)
230 /// .with_limit(10);
231 /// ```
232 pub fn with_after_id(mut self, id: i64) -> Self {
233 self.after_id = Some(id);
234 self
235 }
236
237 /// Set the maximum number of events to return
238 pub fn with_limit(mut self, limit: usize) -> Self {
239 self.limit = Some(limit);
240 self
241 }
242
243 /// Set the sort direction (Asc/Desc).
244 ///
245 /// This controls whether results are in ascending or descending order.
246 /// If not set, defaults to [`Order::Desc`] for timestamp ordering
247 /// and [`Order::Asc`] for ID ordering.
248 pub fn with_direction(mut self, direction: Order) -> Self {
249 self.direction = Some(direction);
250 self
251 }
252
253 /// Set session filter if value is Some.
254 ///
255 /// Note: This clears any previously set `session_ids` filter.
256 pub fn with_session(mut self, session_id: Option<String>) -> Self {
257 self.session_id = session_id;
258 self.session_ids = None;
259 self
260 }
261
262 /// Filter events by multiple session IDs.
263 ///
264 /// Use this to efficiently query events for multiple sessions in a single
265 /// database query, avoiding the N+1 query problem.
266 ///
267 /// Note: This clears any previously set `session_id` filter.
268 /// Passing an empty slice will result in no events being returned.
269 ///
270 /// # Example
271 /// ```ignore
272 /// // Get events for multiple sessions at once
273 /// let events = storage.query(
274 /// &EventQuery::new()
275 /// .with_sessions(&["session-1", "session-2", "session-3"])
276 /// .with_event_type(Some("UserPromptSubmit".to_string()))
277 /// .with_limit(100)
278 /// )?;
279 /// ```
280 pub fn with_sessions(mut self, session_ids: &[impl AsRef<str>]) -> Self {
281 // Store the session_ids - even empty vec is stored to distinguish from None
282 self.session_ids = Some(session_ids.iter().map(|s| s.as_ref().to_string()).collect());
283 self.session_id = None;
284 self
285 }
286
287 /// Set event type filter if value is Some
288 pub fn with_event_type(mut self, event_type: Option<String>) -> Self {
289 self.event_type = event_type;
290 self
291 }
292
293 /// Set permission mode filter if value is Some
294 pub fn with_permission_mode(mut self, permission_mode: Option<String>) -> Self {
295 self.permission_mode = permission_mode;
296 self
297 }
298
299 /// Filter by framework
300 pub fn with_framework(mut self, framework: impl Into<String>) -> Self {
301 self.framework = Some(framework.into());
302 self
303 }
304
305 /// Only return API request events (events with token data)
306 pub fn api_requests_only(mut self) -> Self {
307 self.api_requests_only = true;
308 self.exclude_api_requests = false;
309 self
310 }
311
312 /// Exclude API request events from results
313 pub fn exclude_api_requests(mut self) -> Self {
314 self.exclude_api_requests = true;
315 self.api_requests_only = false;
316 self
317 }
318
319 /// Returns the effective sort direction for this query.
320 ///
321 /// Storage implementations should use this method to determine the sort
322 /// direction when executing queries.
323 ///
324 /// # Returns
325 /// - If `direction` is explicitly set, returns that value.
326 /// - For [`EventOrder::Id`], defaults to [`Order::Asc`] (for watching new events).
327 /// - For [`EventOrder::Timestamp`], defaults to [`Order::Desc`] (most recent first).
328 ///
329 /// # Example
330 /// ```
331 /// use mi6_core::{EventQuery, Order};
332 ///
333 /// // Default: Desc for timestamp ordering
334 /// let query = EventQuery::new();
335 /// assert_eq!(query.effective_direction(), Order::Desc);
336 ///
337 /// // With order_by_id: Asc for watching
338 /// let query = EventQuery::new().order_by_id();
339 /// assert_eq!(query.effective_direction(), Order::Asc);
340 ///
341 /// // Explicit direction overrides default
342 /// let query = EventQuery::new().with_direction(Order::Asc);
343 /// assert_eq!(query.effective_direction(), Order::Asc);
344 /// ```
345 pub fn effective_direction(&self) -> Order {
346 self.direction.unwrap_or(match self.order_by {
347 EventOrder::Id => Order::Asc,
348 EventOrder::Timestamp => Order::Desc,
349 })
350 }
351
352 /// Returns true if this query should order by ID instead of timestamp.
353 ///
354 /// Storage implementations should use this method to determine which column
355 /// to use for ordering results.
356 ///
357 /// # Example
358 /// ```
359 /// use mi6_core::EventQuery;
360 ///
361 /// // Default: order by timestamp
362 /// let query = EventQuery::new();
363 /// assert!(!query.orders_by_id());
364 ///
365 /// // With order_by_id(): order by ID
366 /// let query = EventQuery::new().order_by_id();
367 /// assert!(query.orders_by_id());
368 /// ```
369 pub fn orders_by_id(&self) -> bool {
370 matches!(self.order_by, EventOrder::Id)
371 }
372}
373
374/// Storage backend trait for mi6 events
375///
376/// All events (including API requests) are stored in a unified events table.
377/// API requests are distinguished by having the `ApiRequest` event type and
378/// populated token fields.
379pub trait Storage {
380 /// Insert a new event, returns the event ID
381 fn insert(&self, event: &Event) -> Result<i64, StorageError>;
382
383 /// Query events using the composable EventQuery builder.
384 ///
385 /// This is the primary method for retrieving events. Use `EventQuery` to
386 /// specify filters, ordering, and pagination.
387 ///
388 /// # Ordering
389 /// - Use `order_by_timestamp()` for recent events (default, descending)
390 /// - Use `order_by_id()` for watching new events (ascending by default)
391 ///
392 /// # API Requests
393 /// Use `api_requests_only()` to filter to only API request events,
394 /// or `exclude_api_requests()` to exclude them.
395 ///
396 /// # Examples
397 /// ```ignore
398 /// // Get 50 most recent events (including API requests)
399 /// let events = storage.query(&EventQuery::new().order_by_timestamp().with_limit(50))?;
400 ///
401 /// // Get events for a specific session
402 /// let events = storage.query(&EventQuery::new().with_session(Some("sess-123".to_string())).with_limit(100))?;
403 ///
404 /// // Watch for new events after a known ID
405 /// let events = storage.query(&EventQuery::new().order_by_id().with_after_id(last_id).with_limit(10))?;
406 ///
407 /// // Get only API request events
408 /// let api_events = storage.query(&EventQuery::new().api_requests_only().with_limit(50))?;
409 /// ```
410 fn query(&self, query: &EventQuery) -> Result<Vec<Event>, StorageError>;
411
412 /// Delete events older than the retention period, returns count deleted.
413 fn gc(&self, retention: Duration) -> Result<usize, StorageError>;
414
415 /// Count events older than the retention period (for dry-run).
416 fn count_expired(&self, retention: Duration) -> Result<usize, StorageError>;
417
418 /// Count total events
419 fn count(&self) -> Result<usize, StorageError>;
420
421 /// List sessions matching the query criteria.
422 ///
423 /// Sessions contain denormalized metadata that is incrementally updated
424 /// when events are inserted. This enables single-query monitoring by
425 /// returning all session data in one call.
426 ///
427 /// # Examples
428 /// ```ignore
429 /// // Get all active sessions, most recent first
430 /// let sessions = storage.list_sessions(&SessionQuery::new().active_only())?;
431 ///
432 /// // Get sessions for a specific framework
433 /// let sessions = storage.list_sessions(&SessionQuery::new().with_framework("claude"))?;
434 ///
435 /// // Get 10 most recently active sessions
436 /// let sessions = storage.list_sessions(
437 /// &SessionQuery::new()
438 /// .with_order_by(SessionOrder::LastActivity)
439 /// .with_limit(10)
440 /// )?;
441 /// ```
442 fn list_sessions(&self, query: &SessionQuery) -> Result<Vec<Session>, StorageError>;
443
444 /// Get a single session by ID.
445 ///
446 /// This queries by session_id alone. If multiple sessions exist with the same
447 /// session_id (on different machines), returns the most recently active one.
448 ///
449 /// For exact lookups with a known machine_id, use [`get_session_by_key`].
450 ///
451 /// Returns `Ok(None)` if no session with the given ID exists.
452 fn get_session(&self, session_id: &str) -> Result<Option<Session>, StorageError>;
453
454 /// Get a single session by composite key (machine_id, session_id).
455 ///
456 /// This is the exact lookup using the composite primary key, which ensures
457 /// uniqueness across multiple machines.
458 ///
459 /// Returns `Ok(None)` if no session with the given key exists.
460 fn get_session_by_key(
461 &self,
462 machine_id: &str,
463 session_id: &str,
464 ) -> Result<Option<Session>, StorageError>;
465
466 /// Get a session by its process ID.
467 ///
468 /// Returns the most recently active session associated with the given PID.
469 /// This is useful for looking up sessions when only the PID is known
470 /// (e.g., from process monitoring tools).
471 ///
472 /// Returns `Ok(None)` if no session with the given PID exists.
473 fn get_session_by_pid(&self, pid: i32) -> Result<Option<Session>, StorageError>;
474
475 /// Update git branch information for a session.
476 ///
477 /// This updates the session's git_branch, git_pr_number, and git_issue_number
478 /// fields. It is typically called:
479 /// - On SessionStart, to capture the initial branch
480 /// - After detecting a git branch-changing command in PostToolUse
481 ///
482 /// Returns `Ok(false)` if the session doesn't exist.
483 fn update_session_git_info(
484 &self,
485 session_id: &str,
486 git_info: &GitBranchInfo,
487 ) -> Result<bool, StorageError>;
488
489 /// Update the GitHub repository for a session.
490 ///
491 /// This updates the session's github_repo field if it's currently empty.
492 /// Uses COALESCE semantics to avoid overwriting existing values.
493 ///
494 /// It is typically called:
495 /// - On SessionStart, to capture the initial repo from git remote
496 /// - When a worktree is detected
497 ///
498 /// Returns `Ok(false)` if the session doesn't exist.
499 fn update_session_github_repo(
500 &self,
501 session_id: &str,
502 github_repo: &str,
503 ) -> Result<bool, StorageError>;
504
505 /// Upsert git directory path and GitHub repository for a session.
506 ///
507 /// This is called on every hook event BEFORE the event is inserted, to ensure
508 /// git context is updated first. This allows branch parsing (which happens
509 /// during event insert) to correctly set github_issue/github_pr for the
510 /// current repo.
511 ///
512 /// When github_repo changes, github_issue and github_pr are cleared because
513 /// issue/PR numbers are only meaningful in the context of a specific repository.
514 ///
515 /// Creates the session if it doesn't exist (UPSERT semantics).
516 ///
517 /// This is safe to call frequently (~50µs with direct file access).
518 fn upsert_session_git_context(
519 &self,
520 session_id: &str,
521 machine_id: &str,
522 framework: &str,
523 timestamp: i64,
524 local_git_dir: Option<&str>,
525 github_repo: Option<&str>,
526 ) -> Result<(), StorageError>;
527
528 /// Update the transcript path for a session.
529 ///
530 /// This updates the session's transcript_path field if it's currently empty.
531 /// Uses COALESCE semantics to avoid overwriting existing values.
532 ///
533 /// This is primarily used for Codex sessions where the transcript path
534 /// is not available from hooks but is known from session file scanning.
535 ///
536 /// Returns `Ok(false)` if the session doesn't exist.
537 fn update_session_transcript_path(
538 &self,
539 machine_id: &str,
540 session_id: &str,
541 transcript_path: &str,
542 ) -> Result<bool, StorageError>;
543
544 /// Update the prompt (first_user_message) for a session.
545 ///
546 /// This allows users to rename sessions in the TUI for easier identification.
547 /// Unlike other session updates, this overwrites any existing value.
548 ///
549 /// Returns `Ok(false)` if the session doesn't exist.
550 fn update_session_prompt(
551 &self,
552 machine_id: &str,
553 session_id: &str,
554 prompt: &str,
555 ) -> Result<bool, StorageError>;
556
557 /// Update the status for an Amp session.
558 ///
559 /// This updates the session's status field based on thread file state detection.
560 /// Uses timestamp-guarded updates to prevent stale data from overwriting newer status.
561 ///
562 /// This is only called for Amp sessions since other frameworks use hooks for status.
563 ///
564 /// Returns `Ok(false)` if the session doesn't exist.
565 fn update_amp_session_status(
566 &self,
567 machine_id: &str,
568 session_id: &str,
569 timestamp: i64,
570 status: SessionStatus,
571 ) -> Result<bool, StorageError>;
572
573 /// Get aggregated statistics across all sessions.
574 ///
575 /// Returns session counts, token totals, costs, and API request counts
576 /// computed at the database level via a single aggregation query. This is
577 /// more efficient than iterating over individual sessions in application code.
578 ///
579 /// # Examples
580 /// ```ignore
581 /// // Get stats for all sessions
582 /// let stats = storage.storage_stats(&StorageStatsQuery::new())?;
583 /// println!("Total sessions: {}", stats.session_count);
584 /// println!("Active sessions: {}", stats.active_session_count);
585 /// println!("Total cost: ${:.2}", stats.total_cost_usd);
586 ///
587 /// // Get stats for only active sessions
588 /// let stats = storage.storage_stats(&StorageStatsQuery::new().active_only())?;
589 ///
590 /// // Get stats for a specific framework
591 /// let stats = storage.storage_stats(&StorageStatsQuery::new().with_framework("claude"))?;
592 /// ```
593 fn storage_stats(&self, query: &StorageStatsQuery) -> Result<StorageStats, StorageError>;
594
595 // ========================================================================
596 // Transcript scanning methods (optional, with default no-op implementations)
597 // ========================================================================
598
599 /// Get the last scanned position for a transcript file.
600 ///
601 /// Returns `Ok(None)` if the file has never been scanned.
602 /// Default implementation returns `Ok(None)`.
603 fn get_transcript_position(&self, _path: &Path) -> Result<Option<FilePosition>, StorageError> {
604 Ok(None)
605 }
606
607 /// Set the scanned position for a transcript file.
608 ///
609 /// Default implementation does nothing.
610 fn set_transcript_position(
611 &self,
612 _path: &Path,
613 _position: &FilePosition,
614 ) -> Result<(), StorageError> {
615 Ok(())
616 }
617
618 /// Check if an event with the given UUID already exists for a session.
619 ///
620 /// Used for deduplication when scanning transcripts.
621 /// Default implementation returns `Ok(false)`.
622 fn event_exists_by_uuid(&self, _session_id: &str, _uuid: &str) -> Result<bool, StorageError> {
623 Ok(false)
624 }
625
626 /// Query all transcript file positions.
627 ///
628 /// Returns a list of (path, position) tuples for all tracked transcript files.
629 /// Default implementation returns an empty list.
630 fn query_transcript_positions(&self) -> Result<Vec<(String, FilePosition)>, StorageError> {
631 Ok(vec![])
632 }
633}
634
635/// Blanket implementation of `Storage` for `Arc<T>` where `T: Storage`.
636///
637/// This enables shared ownership of storage instances. Wrap your storage in an
638/// `Arc` and it can be used anywhere a `Storage` implementation is expected.
639///
640/// # Use Cases
641///
642/// - Sharing storage across multiple owned structs
643/// - Single-threaded async runtimes (e.g., `tokio::task::LocalSet`)
644/// - Multi-threaded access when `T: Send + Sync` (see note below)
645///
646/// # Thread Safety Note
647///
648/// For multi-threaded use, `T` must be `Send + Sync`. `SqliteStorage` is not
649/// `Sync` (since `rusqlite::Connection` is not `Sync`), so `Arc<SqliteStorage>`
650/// cannot be shared across threads directly. For multi-threaded scenarios with
651/// `SqliteStorage`, use `Arc<Mutex<SqliteStorage>>` or a connection pool.
652///
653/// # Example
654/// ```ignore
655/// use mi6_storage_sqlite::SqliteStorage;
656/// use mi6_core::{Storage, EventQuery};
657/// use std::sync::Arc;
658///
659/// // Create shared storage
660/// let storage = Arc::new(SqliteStorage::open(path)?);
661///
662/// // Clone for use in multiple places (single-threaded)
663/// let storage_for_queries = Arc::clone(&storage);
664/// let storage_for_inserts = Arc::clone(&storage);
665///
666/// // Both references can use Storage trait methods
667/// let events = storage_for_queries.query(&EventQuery::new().with_limit(10))?;
668/// storage_for_inserts.insert(&event)?;
669/// ```
670impl<T: Storage> Storage for Arc<T> {
671 fn insert(&self, event: &Event) -> Result<i64, StorageError> {
672 (**self).insert(event)
673 }
674
675 fn query(&self, query: &EventQuery) -> Result<Vec<Event>, StorageError> {
676 (**self).query(query)
677 }
678
679 fn gc(&self, retention: Duration) -> Result<usize, StorageError> {
680 (**self).gc(retention)
681 }
682
683 fn count_expired(&self, retention: Duration) -> Result<usize, StorageError> {
684 (**self).count_expired(retention)
685 }
686
687 fn count(&self) -> Result<usize, StorageError> {
688 (**self).count()
689 }
690
691 fn list_sessions(&self, query: &SessionQuery) -> Result<Vec<Session>, StorageError> {
692 (**self).list_sessions(query)
693 }
694
695 fn get_session(&self, session_id: &str) -> Result<Option<Session>, StorageError> {
696 (**self).get_session(session_id)
697 }
698
699 fn get_session_by_key(
700 &self,
701 machine_id: &str,
702 session_id: &str,
703 ) -> Result<Option<Session>, StorageError> {
704 (**self).get_session_by_key(machine_id, session_id)
705 }
706
707 fn get_session_by_pid(&self, pid: i32) -> Result<Option<Session>, StorageError> {
708 (**self).get_session_by_pid(pid)
709 }
710
711 fn update_session_git_info(
712 &self,
713 session_id: &str,
714 git_info: &GitBranchInfo,
715 ) -> Result<bool, StorageError> {
716 (**self).update_session_git_info(session_id, git_info)
717 }
718
719 fn update_session_github_repo(
720 &self,
721 session_id: &str,
722 github_repo: &str,
723 ) -> Result<bool, StorageError> {
724 (**self).update_session_github_repo(session_id, github_repo)
725 }
726
727 fn upsert_session_git_context(
728 &self,
729 session_id: &str,
730 machine_id: &str,
731 framework: &str,
732 timestamp: i64,
733 local_git_dir: Option<&str>,
734 github_repo: Option<&str>,
735 ) -> Result<(), StorageError> {
736 (**self).upsert_session_git_context(
737 session_id,
738 machine_id,
739 framework,
740 timestamp,
741 local_git_dir,
742 github_repo,
743 )
744 }
745
746 fn update_session_transcript_path(
747 &self,
748 machine_id: &str,
749 session_id: &str,
750 transcript_path: &str,
751 ) -> Result<bool, StorageError> {
752 (**self).update_session_transcript_path(machine_id, session_id, transcript_path)
753 }
754
755 fn update_session_prompt(
756 &self,
757 machine_id: &str,
758 session_id: &str,
759 prompt: &str,
760 ) -> Result<bool, StorageError> {
761 (**self).update_session_prompt(machine_id, session_id, prompt)
762 }
763
764 fn update_amp_session_status(
765 &self,
766 machine_id: &str,
767 session_id: &str,
768 timestamp: i64,
769 status: SessionStatus,
770 ) -> Result<bool, StorageError> {
771 (**self).update_amp_session_status(machine_id, session_id, timestamp, status)
772 }
773
774 fn storage_stats(&self, query: &StorageStatsQuery) -> Result<StorageStats, StorageError> {
775 (**self).storage_stats(query)
776 }
777
778 fn get_transcript_position(&self, path: &Path) -> Result<Option<FilePosition>, StorageError> {
779 (**self).get_transcript_position(path)
780 }
781
782 fn set_transcript_position(
783 &self,
784 path: &Path,
785 position: &FilePosition,
786 ) -> Result<(), StorageError> {
787 (**self).set_transcript_position(path, position)
788 }
789
790 fn event_exists_by_uuid(&self, session_id: &str, uuid: &str) -> Result<bool, StorageError> {
791 (**self).event_exists_by_uuid(session_id, uuid)
792 }
793
794 fn query_transcript_positions(&self) -> Result<Vec<(String, FilePosition)>, StorageError> {
795 (**self).query_transcript_positions()
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::StringError;
803 use std::error::Error;
804
805 #[test]
806 fn test_error_source_preserved() {
807 // Create an underlying IO error
808 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
809 let storage_err = StorageError::Connection(Box::new(io_err));
810
811 // Verify the error chain is preserved
812 let source = storage_err.source();
813 assert!(source.is_some(), "StorageError should have a source");
814 assert!(source.is_some_and(|s| s.to_string().contains("file not found")));
815 }
816
817 #[test]
818 fn test_string_error_works() {
819 let msg = "custom error message";
820 let storage_err = StorageError::Query(Box::new(StringError(msg.to_string())));
821
822 // Verify the error message is preserved
823 assert!(storage_err.to_string().contains(msg));
824 assert!(storage_err.source().is_some());
825 }
826}