linkmarks_core/
storage.rs1use crate::errors::CoreError;
3use rusqlite::Connection;
4use std::path::Path;
5
6pub const BUSY_TIMEOUT_MS: u32 = 5000;
8
9pub fn open(path: &Path) -> Result<Connection, CoreError> {
11 let conn = Connection::open(path).map_err(|e| CoreError::Storage(e.to_string()))?;
12 apply_pragmas(&conn)?;
13 Ok(conn)
14}
15
16pub fn open_in_memory() -> Result<Connection, CoreError> {
18 let conn = Connection::open_in_memory().map_err(|e| CoreError::Storage(e.to_string()))?;
19 apply_pragmas(&conn)?;
20 Ok(conn)
21}
22
23fn apply_pragmas(conn: &Connection) -> Result<(), CoreError> {
24 conn.execute_batch(&format!(
25 "PRAGMA journal_mode = WAL; PRAGMA busy_timeout = {BUSY_TIMEOUT_MS}; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON; PRAGMA temp_store = MEMORY;"
26 )).map_err(|e| CoreError::Storage(e.to_string()))?;
27 Ok(())
28}
29
30pub fn current_user_version(conn: &Connection) -> Result<i64, CoreError> {
34 conn.query_row("PRAGMA user_version", [], |r| r.get::<_, i64>(0))
35 .map_err(|e| CoreError::Storage(format!("read user_version: {e}")))
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41 use rusqlite::Connection;
42 use std::thread;
43 use tempfile::tempdir;
44
45 #[test]
46 fn opens_with_wal_mode() {
47 let dir = tempdir().unwrap();
48 let conn = open(&dir.path().join("test.db")).unwrap();
49 let mode: String = conn
50 .query_row("PRAGMA journal_mode", [], |r| r.get(0))
51 .unwrap();
52 assert_eq!(mode.to_lowercase(), "wal");
53 }
54
55 #[test]
56 fn busy_timeout_configured() {
57 let conn = open_in_memory().unwrap();
58 let timeout: i64 = conn
59 .query_row("PRAGMA busy_timeout", [], |r| r.get(0))
60 .unwrap();
61 assert_eq!(timeout, BUSY_TIMEOUT_MS as i64);
62 }
63
64 #[test]
65 fn foreign_keys_enabled() {
66 let conn = open_in_memory().unwrap();
67 let fk: i64 = conn
68 .query_row("PRAGMA foreign_keys", [], |r| r.get(0))
69 .unwrap();
70 assert_eq!(fk, 1);
71 }
72
73 #[test]
74 fn concurrent_readers_do_not_block_writer() {
75 let dir = tempdir().unwrap();
80 let path = dir.path().join("concurrent.db");
81 let setup = open(&path).unwrap();
82 setup
83 .execute_batch("CREATE TABLE t (x INTEGER); INSERT INTO t VALUES (1);")
84 .unwrap();
85 drop(setup);
86
87 let path_w = path.clone();
88 let writer = thread::spawn(move || {
89 let conn: Connection = open(&path_w).unwrap();
90 conn.execute("INSERT INTO t VALUES (2)", []).unwrap();
91 });
92
93 let reader = open(&path).unwrap();
97 let count: i64 = reader
98 .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
99 .unwrap();
100 assert!(count >= 1, "reader starved: count={count}");
101
102 writer.join().expect("writer thread panicked");
103
104 let final_count: i64 = reader
106 .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
107 .unwrap();
108 assert_eq!(final_count, 2);
109 }
110
111 #[test]
112 fn synchronous_normal_with_wal_is_safe() {
113 let conn = open_in_memory().unwrap();
117 let sync: i64 = conn
118 .query_row("PRAGMA synchronous", [], |r| r.get(0))
119 .unwrap();
120 assert_eq!(sync, 1, "expected synchronous=NORMAL (1)");
123 }
124
125 #[test]
126 fn temp_store_in_memory() {
127 let conn = open_in_memory().unwrap();
128 let mode: i64 = conn
129 .query_row("PRAGMA temp_store", [], |r| r.get(0))
130 .unwrap();
131 assert_eq!(mode, 2, "expected temp_store=MEMORY (2)");
133 }
134}