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
101 if Self::is_encrypted(path) {
102 return Err(Error::Encryption(
103 "database is already encrypted — decrypt first or delete and reinitialise".into(),
104 ));
105 }
106
107 let tmp_path = format!("{}.encrypting", path);
108 let backup_path = format!("{}.backup", path);
109
110 fs::remove_file(&tmp_path).ok();
112
113 {
115 let conn = Connection::open(path)?;
116 conn.execute_batch(&format!(
117 "ATTACH DATABASE '{}' AS encrypted KEY '{}';",
118 escape_sql_string(&tmp_path),
119 escape_sql_string(passphrase)
120 ))?;
121 conn.execute_batch("SELECT sqlcipher_export('encrypted');")?;
122 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
123 conn.execute_batch(&format!(
124 "PRAGMA encrypted.user_version = {};",
125 ver
126 ))?;
127 conn.execute_batch("DETACH DATABASE encrypted;")?;
128 }
129
130 let verify_result = Store::open(&tmp_path, config.clone(), Some(passphrase));
132 if let Err(e) = verify_result {
133 fs::remove_file(&tmp_path).ok();
134 return Err(Error::Encryption(format!(
135 "encrypted DB failed verification, original preserved: {}", e
136 )));
137 }
138 drop(verify_result);
139
140 fs::rename(path, &backup_path)
142 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
143 cleanup_wal_files(path);
144
145 if let Err(e) = fs::rename(&tmp_path, path) {
146 fs::rename(&backup_path, path).ok();
148 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
149 }
150
151 fs::remove_file(&backup_path).ok();
153 Ok(())
154 }
155
156 #[cfg(feature = "encryption")]
159 pub fn decrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
160 use std::fs;
161 let tmp_path = format!("{}.decrypting", path);
162 let backup_path = format!("{}.backup", path);
163
164 {
166 let conn = Connection::open(path)?;
167 apply_passphrase(&conn, passphrase)?;
168 verify_access(&conn)?;
169 conn.execute_batch(&format!(
170 "ATTACH DATABASE '{}' AS plaintext KEY '';",
171 escape_sql_string(&tmp_path)
172 ))?;
173 conn.execute_batch("SELECT sqlcipher_export('plaintext');")?;
174 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
175 conn.execute_batch(&format!(
176 "PRAGMA plaintext.user_version = {};",
177 ver
178 ))?;
179 conn.execute_batch("DETACH DATABASE plaintext;")?;
180 }
181
182 let mut verify_config = config;
184 verify_config.storage.encryption_enabled = false;
185 let verify_result = Store::open(&tmp_path, verify_config.clone(), None);
186 if let Err(e) = verify_result {
187 fs::remove_file(&tmp_path).ok();
188 return Err(Error::Encryption(format!(
189 "decrypted DB failed verification, original preserved: {}", e
190 )));
191 }
192 drop(verify_result);
193
194 fs::rename(path, &backup_path)
196 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
197 cleanup_wal_files(path);
198
199 if let Err(e) = fs::rename(&tmp_path, path) {
200 fs::rename(&backup_path, path).ok();
201 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
202 }
203
204 fs::remove_file(&backup_path).ok();
206 Ok(())
207 }
208}
209
210fn apply_passphrase(conn: &Connection, passphrase: &str) -> crate::Result<()> {
211 conn.pragma_update(None, "key", passphrase)
213 .map_err(|e| Error::Encryption(format!("failed to set encryption key: {}", e)))?;
214 Ok(())
215}
216
217fn verify_access(conn: &Connection) -> crate::Result<()> {
218 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
219 .map_err(|_| {
220 Error::Encryption("cannot access database — wrong passphrase or not encrypted".into())
221 })?;
222 Ok(())
223}
224
225fn configure_connection(conn: &Connection, config: &Config) -> rusqlite::Result<()> {
226 conn.execute_batch(&format!(
227 "PRAGMA journal_mode = WAL;
228 PRAGMA busy_timeout = {};
229 PRAGMA synchronous = NORMAL;
230 PRAGMA foreign_keys = ON;
231 PRAGMA cache_size = -{};",
232 config.storage.busy_timeout_ms,
233 config.storage.cache_size_kb
234 ))?;
235 Ok(())
236}
237
238fn run_migrations(conn: &mut Connection) -> crate::Result<()> {
239 let migrations = schema::migrations();
240 migrations
241 .to_latest(conn)
242 .map_err(|e| Error::Migration(e.to_string()))?;
243 Ok(())
244}
245
246#[cfg(feature = "encryption")]
247fn escape_sql_string(s: &str) -> String {
248 s.replace('\'', "''")
249}
250
251#[cfg(feature = "encryption")]
255fn cleanup_wal_files(path: &str) {
256 use std::fs;
257 fs::remove_file(format!("{}-shm", path)).ok();
258 fs::remove_file(format!("{}-wal", path)).ok();
259}