mi6_storage_sqlite/lib.rs
1//! SQLite storage backend for mi6 events.
2//!
3//! This crate provides a SQLite-based implementation of the `Storage` trait
4//! from `mi6-core`. It handles event storage, session tracking, and various
5//! query operations.
6//!
7//! # Module Organization
8//!
9//! - `schema`: Database schema initialization and version checking
10//! - `migrations`: Schema migration logic for upgrades
11//! - `sql`: SQL constants (DDL and DML statements)
12//! - `session`: Session tracking and aggregation logic
13//! - `storage`: Storage trait implementation
14//! - `query_builder`: Dynamic SQL query construction
15//! - `row_parsing`: Row-to-struct conversion helpers
16
17// Allow stderr for warning messages in schema initialization
18#![allow(clippy::print_stderr)]
19
20mod migrations;
21mod query_builder;
22mod row_parsing;
23mod schema;
24mod session;
25mod sql;
26mod storage;
27
28use std::path::Path;
29use std::sync::Arc;
30
31use mi6_core::{FilePosition, StorageError};
32use rusqlite::Connection;
33
34/// SQLite-backed storage for mi6 events.
35///
36/// This struct provides the main entry point for database operations.
37/// It implements the `Storage` trait from `mi6-core`.
38///
39/// # Example
40///
41/// ```ignore
42/// use mi6_storage_sqlite::SqliteStorage;
43/// use mi6_core::Storage;
44///
45/// let storage = SqliteStorage::open(Path::new("mi6.db"))?;
46/// let count = storage.count()?;
47/// ```
48pub struct SqliteStorage {
49 conn: Connection,
50}
51
52impl SqliteStorage {
53 /// Open or create a database at the given path.
54 ///
55 /// This will:
56 /// - Create the parent directory if needed
57 /// - Enable WAL mode for better concurrent access
58 /// - Set a busy timeout for handling concurrent writes
59 /// - Initialize or migrate the schema as needed
60 pub fn open(path: &Path) -> Result<Self, StorageError> {
61 // Create parent directory if needed
62 if let Some(parent) = path.parent() {
63 std::fs::create_dir_all(parent)?;
64 }
65
66 let conn = Connection::open(path).map_err(|e| StorageError::Connection(Box::new(e)))?;
67
68 // Enable WAL mode for better concurrent access
69 conn.pragma_update(None, "journal_mode", "WAL")
70 .map_err(|e| StorageError::Connection(Box::new(e)))?;
71
72 // Set busy timeout to handle concurrent writes gracefully
73 conn.busy_timeout(std::time::Duration::from_millis(5000))
74 .map_err(|e| StorageError::Connection(Box::new(e)))?;
75
76 // Initialize schema with path for backup support during migrations
77 schema::init_schema_with_path(&conn, Some(path))?;
78
79 Ok(Self { conn })
80 }
81
82 /// Open or create a database at the given path, wrapped in an `Arc` for shared ownership.
83 ///
84 /// This is a convenience method equivalent to `Arc::new(SqliteStorage::open(path)?)`.
85 /// The returned `Arc<SqliteStorage>` implements the `Storage` trait directly, allowing
86 /// you to use it anywhere a `Storage` implementation is expected.
87 ///
88 /// # Thread Safety
89 ///
90 /// **Note:** `SqliteStorage` itself is not `Sync` because `rusqlite::Connection` is not
91 /// `Sync`. For true multi-threaded access, wrap in a `Mutex`:
92 ///
93 /// ```ignore
94 /// use mi6_storage_sqlite::SqliteStorage;
95 /// use std::sync::{Arc, Mutex};
96 ///
97 /// let storage = Arc::new(Mutex::new(SqliteStorage::open(path)?));
98 /// ```
99 ///
100 /// Or use a connection pool for high-concurrency scenarios.
101 ///
102 /// # Example
103 /// ```ignore
104 /// use mi6_storage_sqlite::SqliteStorage;
105 /// use mi6_core::Storage;
106 ///
107 /// let storage = SqliteStorage::open_shared(path)?;
108 /// // Arc<SqliteStorage> implements Storage, so you can call methods directly
109 /// let count = storage.count()?;
110 /// ```
111 #[expect(
112 clippy::arc_with_non_send_sync,
113 reason = "Arc is for shared ownership; users needing thread-safety should use Mutex"
114 )]
115 pub fn open_shared(path: &Path) -> Result<Arc<Self>, StorageError> {
116 Ok(Arc::new(Self::open(path)?))
117 }
118
119 /// Query all transcript file positions.
120 ///
121 /// Returns a list of (file_path, position) pairs.
122 pub fn query_transcript_positions(&self) -> Result<Vec<(String, FilePosition)>, StorageError> {
123 storage::query_transcript_positions(&self.conn)
124 }
125}
126
127#[cfg(test)]
128mod tests;