engram/storage/
connection.rs1use parking_lot::Mutex;
7use rusqlite::{Connection, OpenFlags};
8use std::path::Path;
9use std::sync::Arc;
10
11use super::migrations::run_migrations;
12use crate::error::Result;
13use crate::types::{StorageConfig, StorageMode};
14
15pub struct Storage {
17 config: StorageConfig,
18 conn: Arc<Mutex<Connection>>,
19}
20
21pub struct StoragePool {
23 config: StorageConfig,
24 pool: Vec<Arc<Mutex<Connection>>>,
25 next: std::sync::atomic::AtomicUsize,
26}
27
28impl Storage {
29 pub fn open(config: StorageConfig) -> Result<Self> {
31 let conn = Self::create_connection(&config)?;
32
33 run_migrations(&conn)?;
35
36 Ok(Self {
37 config,
38 conn: Arc::new(Mutex::new(conn)),
39 })
40 }
41
42 pub fn open_in_memory() -> Result<Self> {
44 let config = StorageConfig {
45 db_path: ":memory:".to_string(),
46 storage_mode: StorageMode::Local,
47 cloud_uri: None,
48 encrypt_cloud: false,
49 confidence_half_life_days: 30.0,
50 auto_sync: false,
51 sync_debounce_ms: 5000,
52 };
53 Self::open(config)
54 }
55
56 fn create_connection(config: &StorageConfig) -> Result<Connection> {
58 let flags = OpenFlags::SQLITE_OPEN_READ_WRITE
59 | OpenFlags::SQLITE_OPEN_CREATE
60 | OpenFlags::SQLITE_OPEN_NO_MUTEX;
61
62 let conn = if config.db_path == ":memory:" {
63 Connection::open_in_memory()?
64 } else {
65 if let Some(parent) = Path::new(&config.db_path).parent() {
67 std::fs::create_dir_all(parent)?;
68 }
69 Connection::open_with_flags(&config.db_path, flags)?
70 };
71
72 Self::configure_pragmas(&conn, config.storage_mode)?;
74
75 Ok(conn)
76 }
77
78 fn configure_pragmas(conn: &Connection, mode: StorageMode) -> Result<()> {
83 match mode {
84 StorageMode::Local => {
85 conn.execute_batch(
87 r#"
88 PRAGMA journal_mode=WAL;
89 PRAGMA synchronous=NORMAL;
90 PRAGMA wal_autocheckpoint=1000;
91 PRAGMA busy_timeout=30000;
92 PRAGMA cache_size=-64000;
93 PRAGMA temp_store=MEMORY;
94 PRAGMA mmap_size=268435456;
95 PRAGMA foreign_keys=ON;
96 "#,
97 )?;
98 }
99 StorageMode::CloudSafe => {
100 conn.execute_batch(
102 r#"
103 PRAGMA journal_mode=DELETE;
104 PRAGMA synchronous=FULL;
105 PRAGMA busy_timeout=30000;
106 PRAGMA cache_size=-32000;
107 PRAGMA temp_store=MEMORY;
108 PRAGMA foreign_keys=ON;
109 "#,
110 )?;
111 }
112 }
113 Ok(())
114 }
115
116 pub fn connection(&self) -> parking_lot::MutexGuard<'_, Connection> {
118 self.conn.lock()
119 }
120
121 pub fn with_connection<F, T>(&self, f: F) -> Result<T>
123 where
124 F: FnOnce(&Connection) -> Result<T>,
125 {
126 let conn = self.conn.lock();
127 f(&conn)
128 }
129
130 pub fn with_transaction<F, T>(&self, f: F) -> Result<T>
132 where
133 F: FnOnce(&Connection) -> Result<T>,
134 {
135 let mut conn = self.conn.lock();
136 let tx = conn.transaction()?;
137 let result = f(&tx)?;
138 tx.commit()?;
139 Ok(result)
140 }
141
142 pub fn storage_mode(&self) -> StorageMode {
144 self.config.storage_mode
145 }
146
147 pub fn db_path(&self) -> &str {
149 &self.config.db_path
150 }
151
152 pub fn is_in_cloud_folder(&self) -> bool {
154 let path = self.config.db_path.to_lowercase();
155 path.contains("dropbox")
156 || path.contains("onedrive")
157 || path.contains("icloud")
158 || path.contains("google drive")
159 }
160
161 pub fn storage_mode_warning(&self) -> Option<String> {
163 if self.is_in_cloud_folder() && self.config.storage_mode == StorageMode::Local {
164 Some(format!(
165 "WARNING: Database '{}' appears to be in a cloud-synced folder. \
166 WAL mode may cause corruption. Consider:\n\
167 1. Set ENGRAM_STORAGE_MODE=cloud-safe\n\
168 2. Move database to a local folder with backup sync",
169 self.config.db_path
170 ))
171 } else {
172 None
173 }
174 }
175
176 pub fn checkpoint(&self) -> Result<()> {
178 if self.config.storage_mode == StorageMode::Local {
179 let conn = self.conn.lock();
180 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
181 }
182 Ok(())
183 }
184
185 pub fn db_size(&self) -> Result<i64> {
187 let conn = self.conn.lock();
188 let size: i64 = conn.query_row(
189 "SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()",
190 [],
191 |row| row.get(0),
192 )?;
193 Ok(size)
194 }
195
196 pub fn vacuum(&self) -> Result<()> {
198 let conn = self.conn.lock();
199 conn.execute_batch("VACUUM;")?;
200 Ok(())
201 }
202
203 pub fn config(&self) -> &StorageConfig {
205 &self.config
206 }
207}
208
209impl StoragePool {
210 pub fn new(config: StorageConfig, pool_size: usize) -> Result<Self> {
212 let mut pool = Vec::with_capacity(pool_size);
213
214 for _ in 0..pool_size {
215 let conn = Storage::create_connection(&config)?;
216 pool.push(Arc::new(Mutex::new(conn)));
217 }
218
219 if let Some(first) = pool.first() {
221 let conn = first.lock();
222 run_migrations(&conn)?;
223 }
224
225 Ok(Self {
226 config,
227 pool,
228 next: std::sync::atomic::AtomicUsize::new(0),
229 })
230 }
231
232 pub fn get(&self) -> Arc<Mutex<Connection>> {
234 let idx = self.next.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % self.pool.len();
235 self.pool[idx].clone()
236 }
237
238 pub fn with_connection<F, T>(&self, f: F) -> Result<T>
240 where
241 F: FnOnce(&Connection) -> Result<T>,
242 {
243 let conn_arc = self.get();
244 let conn = conn_arc.lock();
245 f(&conn)
246 }
247
248 pub fn config(&self) -> &StorageConfig {
250 &self.config
251 }
252}
253
254impl Clone for Storage {
255 fn clone(&self) -> Self {
256 Self {
257 config: self.config.clone(),
258 conn: self.conn.clone(),
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_open_in_memory() {
269 let storage = Storage::open_in_memory().unwrap();
270 assert_eq!(storage.db_path(), ":memory:");
271 }
272
273 #[test]
274 fn test_storage_modes() {
275 let config = StorageConfig {
277 db_path: ":memory:".to_string(),
278 storage_mode: StorageMode::Local,
279 cloud_uri: None,
280 encrypt_cloud: false,
281 confidence_half_life_days: 30.0,
282 auto_sync: false,
283 sync_debounce_ms: 5000,
284 };
285 let storage = Storage::open(config).unwrap();
286 assert_eq!(storage.storage_mode(), StorageMode::Local);
287
288 let config = StorageConfig {
290 db_path: ":memory:".to_string(),
291 storage_mode: StorageMode::CloudSafe,
292 cloud_uri: None,
293 encrypt_cloud: false,
294 confidence_half_life_days: 30.0,
295 auto_sync: false,
296 sync_debounce_ms: 5000,
297 };
298 let storage = Storage::open(config).unwrap();
299 assert_eq!(storage.storage_mode(), StorageMode::CloudSafe);
300 }
301
302 #[test]
303 fn test_cloud_folder_detection() {
304 let config = StorageConfig {
305 db_path: "/Users/test/Dropbox/memories.db".to_string(),
306 storage_mode: StorageMode::Local,
307 cloud_uri: None,
308 encrypt_cloud: false,
309 confidence_half_life_days: 30.0,
310 auto_sync: false,
311 sync_debounce_ms: 5000,
312 };
313 let path = config.db_path.to_lowercase();
315 assert!(path.contains("dropbox"));
316 }
317}