garmin_cli/db/
mod.rs

1//! Database module for DuckDB persistence
2//!
3//! Provides connection management, schema migrations, and query helpers.
4
5pub mod models;
6pub mod schema;
7
8use duckdb::Connection;
9use std::path::Path;
10use std::sync::{Arc, Mutex};
11
12/// Database connection wrapper with thread-safe access
13pub struct Database {
14    conn: Arc<Mutex<Connection>>,
15    path: Option<String>,
16}
17
18impl Database {
19    /// Open or create a database at the given path
20    pub fn open<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
21        let path_str = path.as_ref().to_string_lossy().to_string();
22        let conn = Connection::open(&path_str)
23            .map_err(|e| crate::error::GarminError::Database(e.to_string()))?;
24
25        let db = Self {
26            conn: Arc::new(Mutex::new(conn)),
27            path: Some(path_str),
28        };
29
30        // Run migrations on open
31        db.migrate()?;
32
33        Ok(db)
34    }
35
36    /// Create an in-memory database (for testing)
37    pub fn in_memory() -> crate::Result<Self> {
38        let conn = Connection::open_in_memory()
39            .map_err(|e| crate::error::GarminError::Database(e.to_string()))?;
40
41        let db = Self {
42            conn: Arc::new(Mutex::new(conn)),
43            path: None,
44        };
45
46        db.migrate()?;
47
48        Ok(db)
49    }
50
51    /// Run schema migrations
52    pub fn migrate(&self) -> crate::Result<()> {
53        let conn = self.conn.lock().unwrap();
54        schema::migrate(&conn)
55    }
56
57    /// Execute a query with no return value
58    pub fn execute(&self, sql: &str) -> crate::Result<usize> {
59        let conn = self.conn.lock().unwrap();
60        conn.execute(sql, [])
61            .map_err(|e| crate::error::GarminError::Database(e.to_string()))
62    }
63
64    /// Execute a query with parameters
65    pub fn execute_params<P: duckdb::Params>(&self, sql: &str, params: P) -> crate::Result<usize> {
66        let conn = self.conn.lock().unwrap();
67        conn.execute(sql, params)
68            .map_err(|e| crate::error::GarminError::Database(e.to_string()))
69    }
70
71    /// Get a connection for complex operations
72    /// Note: Caller must handle locking
73    pub fn connection(&self) -> Arc<Mutex<Connection>> {
74        Arc::clone(&self.conn)
75    }
76
77    /// Get database path (None for in-memory)
78    pub fn path(&self) -> Option<&str> {
79        self.path.as_deref()
80    }
81}
82
83impl Clone for Database {
84    fn clone(&self) -> Self {
85        Self {
86            conn: Arc::clone(&self.conn),
87            path: self.path.clone(),
88        }
89    }
90}
91
92/// Default database path in user's data directory
93pub fn default_db_path() -> crate::Result<String> {
94    let data_dir = crate::config::data_dir()?;
95    Ok(data_dir.join("garmin.duckdb").to_string_lossy().to_string())
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_in_memory_database() {
104        let db = Database::in_memory().expect("Failed to create in-memory db");
105        assert!(db.path().is_none());
106    }
107
108    #[test]
109    #[ignore] // Requires DuckDB extensions that may not be available in CI
110    fn test_execute_query() {
111        let db = Database::in_memory().expect("Failed to create db");
112        let result = db.execute("SELECT 1");
113        assert!(result.is_ok());
114    }
115}