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")]
97 pub fn encrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
98 use std::fs;
99 let tmp_path = format!("{}.encrypting", path);
100 {
101 let conn = Connection::open(path)?;
102 conn.execute_batch(&format!(
104 "ATTACH DATABASE '{}' AS encrypted KEY '{}';",
105 escape_sql_string(&tmp_path),
106 escape_sql_string(passphrase)
107 ))?;
108 conn.execute_batch("SELECT sqlcipher_export('encrypted');")?;
109 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
111 conn.execute_batch(&format!(
112 "PRAGMA encrypted.user_version = {};",
113 ver
114 ))?;
115 conn.execute_batch("DETACH DATABASE encrypted;")?;
116 }
117 fs::rename(&tmp_path, path)
119 .map_err(|e| Error::Encryption(format!("failed to replace db: {}", e)))?;
120 let _store = Store::open(path, config, Some(passphrase))?;
122 Ok(())
123 }
124
125 #[cfg(feature = "encryption")]
127 pub fn decrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
128 use std::fs;
129 let tmp_path = format!("{}.decrypting", path);
130 {
131 let conn = Connection::open(path)?;
132 apply_passphrase(&conn, passphrase)?;
133 verify_access(&conn)?;
134 conn.execute_batch(&format!(
136 "ATTACH DATABASE '{}' AS plaintext KEY '';",
137 escape_sql_string(&tmp_path)
138 ))?;
139 conn.execute_batch("SELECT sqlcipher_export('plaintext');")?;
140 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
141 conn.execute_batch(&format!(
142 "PRAGMA plaintext.user_version = {};",
143 ver
144 ))?;
145 conn.execute_batch("DETACH DATABASE plaintext;")?;
146 }
147 fs::rename(&tmp_path, path)
148 .map_err(|e| Error::Encryption(format!("failed to replace db: {}", e)))?;
149 let mut verify_config = config;
151 verify_config.storage.encryption_enabled = false;
152 let _store = Store::open(path, verify_config, None)?;
153 Ok(())
154 }
155}
156
157fn apply_passphrase(conn: &Connection, passphrase: &str) -> crate::Result<()> {
158 conn.pragma_update(None, "key", passphrase)
160 .map_err(|e| Error::Encryption(format!("failed to set encryption key: {}", e)))?;
161 Ok(())
162}
163
164fn verify_access(conn: &Connection) -> crate::Result<()> {
165 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
166 .map_err(|_| {
167 Error::Encryption("cannot access database — wrong passphrase or not encrypted".into())
168 })?;
169 Ok(())
170}
171
172fn configure_connection(conn: &Connection, config: &Config) -> rusqlite::Result<()> {
173 conn.execute_batch(&format!(
174 "PRAGMA journal_mode = WAL;
175 PRAGMA busy_timeout = {};
176 PRAGMA synchronous = NORMAL;
177 PRAGMA foreign_keys = ON;
178 PRAGMA cache_size = -{};",
179 config.storage.busy_timeout_ms,
180 config.storage.cache_size_kb
181 ))?;
182 Ok(())
183}
184
185fn run_migrations(conn: &mut Connection) -> crate::Result<()> {
186 let migrations = schema::migrations();
187 migrations
188 .to_latest(conn)
189 .map_err(|e| Error::Migration(e.to_string()))?;
190 Ok(())
191}
192
193#[cfg(feature = "encryption")]
194fn escape_sql_string(s: &str) -> String {
195 s.replace('\'', "''")
196}