Skip to main content

memo_cli/storage/
mod.rs

1pub mod derivations;
2pub mod migrate;
3pub mod repository;
4pub mod search;
5
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use rusqlite::{Connection, Transaction};
11
12use crate::errors::AppError;
13
14#[derive(Debug, Clone)]
15pub struct Storage {
16    db_path: PathBuf,
17}
18
19impl Storage {
20    pub fn new(db_path: PathBuf) -> Self {
21        Self { db_path }
22    }
23
24    pub fn db_path(&self) -> &Path {
25        &self.db_path
26    }
27
28    pub fn init(&self) -> Result<(), AppError> {
29        let _conn = self.open_connection()?;
30        Ok(())
31    }
32
33    pub fn with_connection<T, F>(&self, f: F) -> Result<T, AppError>
34    where
35        F: FnOnce(&Connection) -> Result<T, AppError>,
36    {
37        let conn = self.open_connection()?;
38        f(&conn)
39    }
40
41    pub fn with_transaction<T, F>(&self, f: F) -> Result<T, AppError>
42    where
43        F: FnOnce(&Transaction<'_>) -> Result<T, AppError>,
44    {
45        let mut conn = self.open_connection()?;
46        let tx = conn.transaction().map_err(AppError::db_write)?;
47        let out = f(&tx)?;
48        tx.commit().map_err(AppError::db_write)?;
49        Ok(out)
50    }
51
52    fn open_connection(&self) -> Result<Connection, AppError> {
53        if let Some(parent) = self.db_path.parent()
54            && !parent.as_os_str().is_empty()
55        {
56            fs::create_dir_all(parent).map_err(AppError::db_open)?;
57        }
58
59        let conn = Connection::open(&self.db_path).map_err(AppError::db_open)?;
60        conn.pragma_update(None, "foreign_keys", "ON")
61            .map_err(AppError::db_open)?;
62        conn.pragma_update(None, "journal_mode", "WAL")
63            .map_err(AppError::db_open)?;
64        conn.busy_timeout(Duration::from_secs(2))
65            .map_err(AppError::db_open)?;
66
67        migrate::apply(&conn)?;
68        Ok(conn)
69    }
70}
71
72#[cfg(test)]
73pub(crate) mod tests {
74    use std::path::PathBuf;
75
76    use pretty_assertions::assert_eq;
77
78    use super::Storage;
79
80    fn test_db_path(name: &str) -> PathBuf {
81        let dir = tempfile::tempdir().expect("tempdir should be created");
82        dir.keep().join(format!("{name}.db"))
83    }
84
85    #[test]
86    fn init_db() {
87        let db_path = test_db_path("init_db");
88        let storage = Storage::new(db_path);
89        storage.init().expect("storage init should succeed");
90
91        let table_name: String = storage
92            .with_connection(|conn| {
93                conn.query_row(
94                    "select name from sqlite_master where type='table' and name='inbox_items'",
95                    [],
96                    |row| row.get(0),
97                )
98                .map_err(crate::errors::AppError::db_query)
99            })
100            .expect("inbox_items table should exist");
101
102        assert_eq!(table_name, "inbox_items");
103    }
104
105    #[test]
106    fn migration_idempotent() {
107        let db_path = test_db_path("migration_idempotent");
108        let storage = Storage::new(db_path);
109        storage.init().expect("first init should succeed");
110        storage.init().expect("second init should succeed");
111
112        let applied_count_v1: i64 = storage
113            .with_connection(|conn| {
114                conn.query_row(
115                    "select count(*) from schema_migrations where version = 1",
116                    [],
117                    |row| row.get(0),
118                )
119                .map_err(crate::errors::AppError::db_query)
120            })
121            .expect("schema migration count query should succeed");
122        let applied_count_total: i64 = storage
123            .with_connection(|conn| {
124                conn.query_row("select count(*) from schema_migrations", [], |row| {
125                    row.get(0)
126                })
127                .map_err(crate::errors::AppError::db_query)
128            })
129            .expect("schema migration count query should succeed");
130
131        assert_eq!(applied_count_v1, 1);
132        assert_eq!(applied_count_total, 1);
133    }
134}