Skip to main content

jellytau_lib/storage/
db_service.rs

1//! Database service abstraction layer
2//!
3//! This module provides an async database interface that abstracts away
4//! the underlying database implementation. This makes it easy to:
5//! - Switch between sync (rusqlite) and async (tokio-rusqlite) implementations
6//! - Prevent blocking the async runtime with synchronous database calls
7//! - Test with different database backends
8//! - Migrate to other database systems in the future
9
10use crate::utils::lock::MutexSafe;
11use async_trait::async_trait;
12use rusqlite::{params_from_iter, Connection, Result as SqliteResult, Row};
13use std::sync::{Arc, Mutex};
14
15/// Database query result type
16pub type DbResult<T> = Result<T, String>;
17
18/// Represents a database query that can be executed
19#[derive(Clone)]
20pub struct Query {
21    pub sql: String,
22    pub params: Vec<QueryParam>,
23}
24
25/// Query parameter types supported by the database
26#[derive(Clone, Debug)]
27pub enum QueryParam {
28    String(String),
29    Int(i32),
30    Int64(i64),
31    Float(f64),
32    #[allow(dead_code)]
33    Bool(bool),
34    Null,
35}
36
37impl Query {
38    pub fn new(sql: impl Into<String>) -> Self {
39        Self {
40            sql: sql.into(),
41            params: Vec::new(),
42        }
43    }
44
45    pub fn with_params(sql: impl Into<String>, params: Vec<QueryParam>) -> Self {
46        Self {
47            sql: sql.into(),
48            params,
49        }
50    }
51}
52
53/// Database service trait - abstraction over database operations
54#[async_trait]
55pub trait DatabaseService: Send + Sync {
56    /// Execute a query that doesn't return results (INSERT, UPDATE, DELETE)
57    async fn execute(&self, query: Query) -> DbResult<usize>;
58
59    /// Execute a batch of SQL statements (for migrations)
60    #[allow(dead_code)]
61    async fn execute_batch(&self, sql: &str) -> DbResult<()>;
62
63    /// Query a single row
64    async fn query_one<T, F>(&self, query: Query, mapper: F) -> DbResult<T>
65    where
66        T: Send + 'static,
67        F: Fn(&Row) -> SqliteResult<T> + Send + 'static;
68
69    /// Query a single optional row
70    async fn query_optional<T, F>(&self, query: Query, mapper: F) -> DbResult<Option<T>>
71    where
72        T: Send + 'static,
73        F: Fn(&Row) -> SqliteResult<T> + Send + 'static;
74
75    /// Query multiple rows
76    async fn query_many<T, F>(&self, query: Query, mapper: F) -> DbResult<Vec<T>>
77    where
78        T: Send + 'static,
79        F: Fn(&Row) -> SqliteResult<T> + Send + 'static;
80
81    /// Run a transaction with multiple operations
82    async fn transaction<F, T>(&self, f: F) -> DbResult<T>
83    where
84        F: FnOnce(&mut Transaction) -> DbResult<T> + Send + 'static,
85        T: Send + 'static;
86
87    /// Get the row ID of the most recent successful INSERT
88    async fn last_insert_rowid(&self) -> DbResult<i64>;
89}
90
91/// Transaction handle for batching multiple operations
92pub struct Transaction<'a> {
93    conn: &'a Connection,
94}
95
96impl<'a> Transaction<'a> {
97    pub fn new(conn: &'a Connection) -> Self {
98        Self { conn }
99    }
100
101    pub fn execute(&mut self, query: Query) -> DbResult<usize> {
102        execute_query(self.conn, query)
103    }
104
105    pub fn query_many<T, F>(&self, query: Query, mapper: F) -> DbResult<Vec<T>>
106    where
107        F: Fn(&Row) -> SqliteResult<T>,
108    {
109        query_many(self.conn, query, mapper)
110    }
111}
112
113/// Rusqlite-based database service implementation
114///
115/// This implementation wraps synchronous rusqlite operations in tokio::task::spawn_blocking
116/// to prevent blocking the async runtime.
117pub struct RusqliteService {
118    conn: Arc<Mutex<Connection>>,
119}
120
121impl RusqliteService {
122    pub fn new(conn: Arc<Mutex<Connection>>) -> Self {
123        Self { conn }
124    }
125}
126
127#[async_trait]
128impl DatabaseService for RusqliteService {
129    async fn execute(&self, query: Query) -> DbResult<usize> {
130        let conn = Arc::clone(&self.conn);
131        tokio::task::spawn_blocking(move || {
132            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
133            // panic under the guard would otherwise poison it, failing every
134            // later query with "poisoned lock" until the process restarts.
135            let conn = conn.lock_safe();
136            execute_query(&conn, query)
137        })
138        .await
139        .map_err(|e| format!("Task join error: {}", e))?
140    }
141
142    async fn execute_batch(&self, sql: &str) -> DbResult<()> {
143        let conn = Arc::clone(&self.conn);
144        let sql = sql.to_string();
145        tokio::task::spawn_blocking(move || {
146            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
147            // panic under the guard would otherwise poison it, failing every
148            // later query with "poisoned lock" until the process restarts.
149            let conn = conn.lock_safe();
150            conn.execute_batch(&sql)
151                .map_err(|e| format!("Execute batch failed: {}", e))
152        })
153        .await
154        .map_err(|e| format!("Task join error: {}", e))?
155    }
156
157    async fn query_one<T, F>(&self, query: Query, mapper: F) -> DbResult<T>
158    where
159        T: Send + 'static,
160        F: Fn(&Row) -> SqliteResult<T> + Send + 'static,
161    {
162        let conn = Arc::clone(&self.conn);
163        tokio::task::spawn_blocking(move || {
164            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
165            // panic under the guard would otherwise poison it, failing every
166            // later query with "poisoned lock" until the process restarts.
167            let conn = conn.lock_safe();
168            query_one(&conn, query, mapper)
169        })
170        .await
171        .map_err(|e| format!("Task join error: {}", e))?
172    }
173
174    async fn query_optional<T, F>(&self, query: Query, mapper: F) -> DbResult<Option<T>>
175    where
176        T: Send + 'static,
177        F: Fn(&Row) -> SqliteResult<T> + Send + 'static,
178    {
179        let conn = Arc::clone(&self.conn);
180        tokio::task::spawn_blocking(move || {
181            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
182            // panic under the guard would otherwise poison it, failing every
183            // later query with "poisoned lock" until the process restarts.
184            let conn = conn.lock_safe();
185            query_optional(&conn, query, mapper)
186        })
187        .await
188        .map_err(|e| format!("Task join error: {}", e))?
189    }
190
191    async fn query_many<T, F>(&self, query: Query, mapper: F) -> DbResult<Vec<T>>
192    where
193        T: Send + 'static,
194        F: Fn(&Row) -> SqliteResult<T> + Send + 'static,
195    {
196        let conn = Arc::clone(&self.conn);
197        tokio::task::spawn_blocking(move || {
198            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
199            // panic under the guard would otherwise poison it, failing every
200            // later query with "poisoned lock" until the process restarts.
201            let conn = conn.lock_safe();
202            query_many(&conn, query, mapper)
203        })
204        .await
205        .map_err(|e| format!("Task join error: {}", e))?
206    }
207
208    async fn transaction<F, T>(&self, f: F) -> DbResult<T>
209    where
210        F: FnOnce(&mut Transaction) -> DbResult<T> + Send + 'static,
211        T: Send + 'static,
212    {
213        let conn = Arc::clone(&self.conn);
214        tokio::task::spawn_blocking(move || {
215            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
216            // panic under the guard would otherwise poison it, failing every
217            // later query with "poisoned lock" until the process restarts.
218            let conn = conn.lock_safe();
219
220            conn.execute("BEGIN TRANSACTION", [])
221                .map_err(|e| format!("Failed to begin transaction: {}", e))?;
222
223            let mut transaction = Transaction::new(&conn);
224            let result = f(&mut transaction);
225
226            match result {
227                Ok(value) => {
228                    conn.execute("COMMIT", [])
229                        .map_err(|e| format!("Failed to commit transaction: {}", e))?;
230                    Ok(value)
231                }
232                Err(e) => {
233                    conn.execute("ROLLBACK", [])
234                        .map_err(|e| format!("Failed to rollback transaction: {}", e))?;
235                    Err(e)
236                }
237            }
238        })
239        .await
240        .map_err(|e| format!("Task join error: {}", e))?
241    }
242
243    async fn last_insert_rowid(&self) -> DbResult<i64> {
244        let conn = Arc::clone(&self.conn);
245        tokio::task::spawn_blocking(move || {
246            // `lock_safe`, not `lock`: this is the busiest lock in the app and a
247            // panic under the guard would otherwise poison it, failing every
248            // later query with "poisoned lock" until the process restarts.
249            let conn = conn.lock_safe();
250            Ok(conn.last_insert_rowid())
251        })
252        .await
253        .map_err(|e| format!("Task join error: {}", e))?
254    }
255}
256
257// Helper functions for executing queries synchronously
258
259fn execute_query(conn: &Connection, query: Query) -> DbResult<usize> {
260    let params = convert_params(&query.params);
261    conn.execute(&query.sql, params_from_iter(params.iter()))
262        .map_err(|e| format!("Execute failed: {}", e))
263}
264
265fn query_one<T, F>(conn: &Connection, query: Query, mapper: F) -> DbResult<T>
266where
267    F: Fn(&Row) -> SqliteResult<T>,
268{
269    let params = convert_params(&query.params);
270    conn.query_row(&query.sql, params_from_iter(params.iter()), mapper)
271        .map_err(|e| format!("Query one failed: {}", e))
272}
273
274fn query_optional<T, F>(conn: &Connection, query: Query, mapper: F) -> DbResult<Option<T>>
275where
276    F: Fn(&Row) -> SqliteResult<T>,
277{
278    match query_one(conn, query, mapper) {
279        Ok(value) => Ok(Some(value)),
280        Err(e) if e.contains("Query returned no rows") || e.contains("QueryReturnedNoRows") => {
281            Ok(None)
282        }
283        Err(e) => Err(e),
284    }
285}
286
287fn query_many<T, F>(conn: &Connection, query: Query, mapper: F) -> DbResult<Vec<T>>
288where
289    F: Fn(&Row) -> SqliteResult<T>,
290{
291    let params = convert_params(&query.params);
292    let mut stmt = conn
293        .prepare(&query.sql)
294        .map_err(|e| format!("Prepare failed: {}", e))?;
295
296    let rows = stmt
297        .query_map(params_from_iter(params.iter()), mapper)
298        .map_err(|e| format!("Query map failed: {}", e))?;
299
300    rows.collect::<SqliteResult<Vec<T>>>()
301        .map_err(|e| format!("Collect failed: {}", e))
302}
303
304/// Convert QueryParam to rusqlite::types::Value
305fn convert_params(params: &[QueryParam]) -> Vec<rusqlite::types::Value> {
306    params
307        .iter()
308        .map(|p| match p {
309            QueryParam::String(s) => rusqlite::types::Value::Text(s.clone()),
310            QueryParam::Int(i) => rusqlite::types::Value::Integer(*i as i64),
311            QueryParam::Int64(i) => rusqlite::types::Value::Integer(*i),
312            QueryParam::Float(f) => rusqlite::types::Value::Real(*f),
313            QueryParam::Bool(b) => rusqlite::types::Value::Integer(if *b { 1 } else { 0 }),
314            QueryParam::Null => rusqlite::types::Value::Null,
315        })
316        .collect()
317}
318
319// TRACES: UR-002, UR-012 | DR-012 | UT-014, UT-015, UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023, UT-025
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[tokio::test]
325    async fn test_execute_query() {
326        let conn = Connection::open_in_memory().unwrap();
327        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
328            .unwrap();
329
330        let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
331
332        let query = Query::with_params(
333            "INSERT INTO test (name) VALUES (?)",
334            vec![QueryParam::String("Alice".to_string())],
335        );
336
337        let rows = service.execute(query).await.unwrap();
338        assert_eq!(rows, 1);
339    }
340
341    #[tokio::test]
342    async fn test_query_one() {
343        let conn = Connection::open_in_memory().unwrap();
344        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
345            .unwrap();
346        conn.execute("INSERT INTO test (name) VALUES ('Bob')", [])
347            .unwrap();
348
349        let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
350
351        let query = Query::new("SELECT name FROM test WHERE id = 1");
352        let name: String = service.query_one(query, |row| row.get(0)).await.unwrap();
353
354        assert_eq!(name, "Bob");
355    }
356
357    #[tokio::test]
358    async fn test_query_many() {
359        let conn = Connection::open_in_memory().unwrap();
360        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
361            .unwrap();
362        conn.execute("INSERT INTO test (name) VALUES ('Alice')", [])
363            .unwrap();
364        conn.execute("INSERT INTO test (name) VALUES ('Bob')", [])
365            .unwrap();
366
367        let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
368
369        let query = Query::new("SELECT name FROM test ORDER BY id");
370        let names: Vec<String> = service.query_many(query, |row| row.get(0)).await.unwrap();
371
372        assert_eq!(names, vec!["Alice", "Bob"]);
373    }
374
375    #[tokio::test]
376    async fn test_query_optional() {
377        let conn = Connection::open_in_memory().unwrap();
378        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
379            .unwrap();
380
381        let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
382
383        let query = Query::new("SELECT name FROM test WHERE id = 999");
384        let result: Option<String> = service
385            .query_optional(query, |row| row.get(0))
386            .await
387            .unwrap();
388
389        assert_eq!(result, None);
390    }
391
392    #[tokio::test]
393    async fn test_transaction() {
394        let conn = Connection::open_in_memory().unwrap();
395        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
396            .unwrap();
397
398        let service = RusqliteService::new(Arc::new(Mutex::new(conn)));
399
400        let result = service
401            .transaction(|tx| {
402                tx.execute(Query::with_params(
403                    "INSERT INTO test (name) VALUES (?)",
404                    vec![QueryParam::String("Alice".to_string())],
405                ))?;
406                tx.execute(Query::with_params(
407                    "INSERT INTO test (name) VALUES (?)",
408                    vec![QueryParam::String("Bob".to_string())],
409                ))?;
410                Ok(())
411            })
412            .await;
413
414        assert!(result.is_ok());
415
416        // Verify both rows were inserted
417        let query = Query::new("SELECT COUNT(*) FROM test");
418        let count: i32 = service.query_one(query, |row| row.get(0)).await.unwrap();
419        assert_eq!(count, 2);
420    }
421    /// A panic while the connection guard is held must not brick every later
422    /// query.
423    ///
424    /// This is the single busiest lock in the app — every async DB operation
425    /// goes through it. With a raw `.lock()`, one panic under the guard poisons
426    /// the mutex and every subsequent call returns "poisoned lock" until the
427    /// process restarts, which for a database-backed app means the whole UI
428    /// stops working. `utils::lock` exists precisely to stop that cascade, and
429    /// `storage::Database` already used it; this path did not.
430    ///
431    /// TRACES: UR-002 | DR-012 | UT-014
432    #[tokio::test]
433    async fn a_poisoned_connection_still_serves_queries() {
434        let conn = Arc::new(Mutex::new(Connection::open_in_memory().unwrap()));
435        {
436            let c = conn.lock_safe();
437            c.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY);")
438                .unwrap();
439        }
440
441        // Poison the mutex the way a panicking row mapper would.
442        let poisoner = Arc::clone(&conn);
443        let hook = std::panic::take_hook();
444        std::panic::set_hook(Box::new(|_| {}));
445        let _ = std::thread::spawn(move || {
446            let _guard = poisoner.lock().unwrap();
447            panic!("a row mapper blew up while holding the connection");
448        })
449        .join();
450        std::panic::set_hook(hook);
451        assert!(conn.lock().is_err(), "the mutex should now be poisoned");
452
453        // Every operation must still work.
454        let service = RusqliteService::new(Arc::clone(&conn));
455        service
456            .execute(Query::new("INSERT INTO test (id) VALUES (1)"))
457            .await
458            .expect("execute must survive a poisoned connection");
459        let count: i32 = service
460            .query_one(Query::new("SELECT COUNT(*) FROM test"), |row| row.get(0))
461            .await
462            .expect("query_one must survive a poisoned connection");
463        assert_eq!(count, 1);
464    }
465}