1pub mod schema;
2pub mod memory;
3pub mod metrics;
4pub mod session;
5pub mod scope;
6pub mod dedup;
7pub mod privacy;
8pub mod relations;
9
10use rusqlite::Connection;
11
12use crate::config::Config;
13use crate::Error;
14
15pub struct Store {
16 conn: Connection,
17 config: Config,
18}
19
20impl Store {
21 pub fn open(path: &str, config: Config, passphrase: Option<&str>) -> crate::Result<Self> {
22 if config.storage.encryption_enabled && passphrase.is_none() {
23 return Err(Error::Encryption(
24 "encryption is enabled but no passphrase was provided".into(),
25 ));
26 }
27 let mut conn = Connection::open(path)?;
28 if let Some(key) = passphrase {
29 apply_passphrase(&conn, key)?;
30 verify_access(&conn)?;
31 }
32 configure_connection(&conn, &config)?;
33 schema::check_version(&conn)?;
34 run_migrations(&mut conn)?;
35 Ok(Self { conn, config })
36 }
37
38 pub fn open_in_memory() -> crate::Result<Self> {
39 Self::open_in_memory_with_config(Config::default())
40 }
41
42 pub fn open_in_memory_with_config(config: Config) -> crate::Result<Self> {
43 let mut conn = Connection::open_in_memory()?;
44 configure_connection(&conn, &config)?;
45 run_migrations(&mut conn)?;
46 Ok(Self { conn, config })
47 }
48
49 pub fn config(&self) -> &Config {
50 &self.config
51 }
52
53 pub(crate) fn conn(&self) -> &Connection {
54 &self.conn
55 }
56
57 pub fn get_metadata(&self, key: &str) -> crate::Result<Option<String>> {
58 match self.conn.query_row(
59 "SELECT value FROM _metadata WHERE key = ?1",
60 rusqlite::params![key],
61 |row| row.get::<_, String>(0),
62 ) {
63 Ok(val) => Ok(Some(val)),
64 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
65 Err(e) => Err(e.into()),
66 }
67 }
68
69 pub fn set_metadata(&self, key: &str, value: &str) -> crate::Result<()> {
70 self.conn.execute(
71 "INSERT INTO _metadata (key, value) VALUES (?1, ?2)
72 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
73 rusqlite::params![key, value],
74 )?;
75 Ok(())
76 }
77
78 pub fn schema_version(&self) -> crate::Result<i64> {
79 Ok(self
80 .conn
81 .pragma_query_value(None, "user_version", |r| r.get(0))?)
82 }
83
84 pub fn is_encrypted(path: &str) -> bool {
86 let conn = match Connection::open(path) {
88 Ok(c) => c,
89 Err(_) => return false,
90 };
91 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
92 .is_err()
93 }
94
95 #[cfg(feature = "encryption")]
98 pub fn encrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
99 use std::fs;
100 let tmp_path = format!("{}.encrypting", path);
101 let backup_path = format!("{}.backup", path);
102
103 {
105 let conn = Connection::open(path)?;
106 conn.execute_batch(&format!(
107 "ATTACH DATABASE '{}' AS encrypted KEY '{}';",
108 escape_sql_string(&tmp_path),
109 escape_sql_string(passphrase)
110 ))?;
111 conn.execute_batch("SELECT sqlcipher_export('encrypted');")?;
112 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
113 conn.execute_batch(&format!(
114 "PRAGMA encrypted.user_version = {};",
115 ver
116 ))?;
117 conn.execute_batch("DETACH DATABASE encrypted;")?;
118 }
119
120 let verify_result = Store::open(&tmp_path, config.clone(), Some(passphrase));
122 if let Err(e) = verify_result {
123 fs::remove_file(&tmp_path).ok();
124 return Err(Error::Encryption(format!(
125 "encrypted DB failed verification, original preserved: {}", e
126 )));
127 }
128 drop(verify_result);
129
130 fs::rename(path, &backup_path)
132 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
133 cleanup_wal_files(path);
134
135 if let Err(e) = fs::rename(&tmp_path, path) {
136 fs::rename(&backup_path, path).ok();
138 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
139 }
140
141 fs::remove_file(&backup_path).ok();
143 Ok(())
144 }
145
146 #[cfg(feature = "encryption")]
149 pub fn decrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
150 use std::fs;
151 let tmp_path = format!("{}.decrypting", path);
152 let backup_path = format!("{}.backup", path);
153
154 {
156 let conn = Connection::open(path)?;
157 apply_passphrase(&conn, passphrase)?;
158 verify_access(&conn)?;
159 conn.execute_batch(&format!(
160 "ATTACH DATABASE '{}' AS plaintext KEY '';",
161 escape_sql_string(&tmp_path)
162 ))?;
163 conn.execute_batch("SELECT sqlcipher_export('plaintext');")?;
164 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
165 conn.execute_batch(&format!(
166 "PRAGMA plaintext.user_version = {};",
167 ver
168 ))?;
169 conn.execute_batch("DETACH DATABASE plaintext;")?;
170 }
171
172 let mut verify_config = config;
174 verify_config.storage.encryption_enabled = false;
175 let verify_result = Store::open(&tmp_path, verify_config.clone(), None);
176 if let Err(e) = verify_result {
177 fs::remove_file(&tmp_path).ok();
178 return Err(Error::Encryption(format!(
179 "decrypted DB failed verification, original preserved: {}", e
180 )));
181 }
182 drop(verify_result);
183
184 fs::rename(path, &backup_path)
186 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
187 cleanup_wal_files(path);
188
189 if let Err(e) = fs::rename(&tmp_path, path) {
190 fs::rename(&backup_path, path).ok();
191 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
192 }
193
194 fs::remove_file(&backup_path).ok();
196 Ok(())
197 }
198}
199
200fn apply_passphrase(conn: &Connection, passphrase: &str) -> crate::Result<()> {
201 conn.pragma_update(None, "key", passphrase)
203 .map_err(|e| Error::Encryption(format!("failed to set encryption key: {}", e)))?;
204 Ok(())
205}
206
207fn verify_access(conn: &Connection) -> crate::Result<()> {
208 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
209 .map_err(|_| {
210 Error::Encryption("cannot access database — wrong passphrase or not encrypted".into())
211 })?;
212 Ok(())
213}
214
215fn configure_connection(conn: &Connection, config: &Config) -> rusqlite::Result<()> {
216 conn.execute_batch(&format!(
217 "PRAGMA journal_mode = WAL;
218 PRAGMA busy_timeout = {};
219 PRAGMA synchronous = NORMAL;
220 PRAGMA foreign_keys = ON;
221 PRAGMA cache_size = -{};",
222 config.storage.busy_timeout_ms,
223 config.storage.cache_size_kb
224 ))?;
225 Ok(())
226}
227
228fn run_migrations(conn: &mut Connection) -> crate::Result<()> {
229 let migrations = schema::migrations();
230 migrations
231 .to_latest(conn)
232 .map_err(|e| Error::Migration(e.to_string()))?;
233 Ok(())
234}
235
236#[cfg(feature = "encryption")]
237fn escape_sql_string(s: &str) -> String {
238 s.replace('\'', "''")
239}
240
241#[cfg(feature = "encryption")]
245fn cleanup_wal_files(path: &str) {
246 use std::fs;
247 fs::remove_file(format!("{}-shm", path)).ok();
248 fs::remove_file(format!("{}-wal", path)).ok();
249}