fathomdb_engine/
sqlite.rs1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use rusqlite::{Connection, OpenFlags};
5
6use crate::EngineError;
7
8const SHARED_SQLITE_POLICY: &str = include_str!("../sqlite.env");
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct SharedSqlitePolicy {
15 pub minimum_supported_version: String,
16 pub repo_dev_version: String,
17 pub repo_local_binary_relpath: PathBuf,
18}
19
20#[cfg(feature = "tracing")]
21static SQLITE_LOG_INIT: std::sync::Once = std::sync::Once::new();
22
23#[cfg(feature = "tracing")]
29fn sqlite_log_callback(code: std::os::raw::c_int, msg: &str) {
30 let primary = code & 0xFF;
31 if primary == rusqlite::ffi::SQLITE_NOTICE as std::os::raw::c_int {
32 tracing::info!(target: "fathomdb_engine::sqlite", sqlite_error_code = code, "{msg}");
33 } else if primary == rusqlite::ffi::SQLITE_WARNING as std::os::raw::c_int {
34 tracing::warn!(target: "fathomdb_engine::sqlite", sqlite_error_code = code, "{msg}");
35 } else {
36 tracing::error!(target: "fathomdb_engine::sqlite", sqlite_error_code = code, "{msg}");
37 }
38}
39
40#[cfg(all(feature = "tracing", debug_assertions))]
47fn install_trace_v2(conn: &Connection) {
48 use std::os::raw::{c_int, c_uint, c_void};
49
50 unsafe extern "C" fn trace_v2_callback(
51 event_type: c_uint,
52 _ctx: *mut c_void,
53 p: *mut c_void,
54 x: *mut c_void,
55 ) -> c_int {
56 if event_type == rusqlite::ffi::SQLITE_TRACE_PROFILE as c_uint {
57 let stmt = p.cast::<rusqlite::ffi::sqlite3_stmt>();
58 let nanos = unsafe { *(x.cast::<i64>()) };
59 let sql_ptr = unsafe { rusqlite::ffi::sqlite3_sql(stmt) };
60 if !sql_ptr.is_null() {
61 let sql = unsafe { std::ffi::CStr::from_ptr(sql_ptr) }.to_string_lossy();
62 tracing::trace!(
63 target: "fathomdb_engine::sqlite",
64 sql = %sql,
65 duration_us = nanos / 1000,
66 "sqlite statement profile"
67 );
68 }
69 }
70 0
71 }
72
73 unsafe {
74 rusqlite::ffi::sqlite3_trace_v2(
75 conn.handle(),
76 rusqlite::ffi::SQLITE_TRACE_PROFILE as c_uint,
77 Some(trace_v2_callback),
78 std::ptr::null_mut(),
79 );
80 }
81}
82
83pub fn open_connection(path: &Path) -> Result<Connection, EngineError> {
84 #[cfg(feature = "tracing")]
85 SQLITE_LOG_INIT.call_once(|| {
86 unsafe {
89 let _ = rusqlite::trace::config_log(Some(sqlite_log_callback));
90 }
91 });
92
93 let conn = Connection::open_with_flags(
94 path,
95 OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
96 )?;
97 conn.busy_timeout(Duration::from_millis(5_000))?;
98
99 #[cfg(all(feature = "tracing", debug_assertions))]
100 install_trace_v2(&conn);
101
102 Ok(conn)
103}
104
105pub fn open_readonly_connection(path: &Path) -> Result<Connection, EngineError> {
114 #[cfg(feature = "tracing")]
115 SQLITE_LOG_INIT.call_once(|| {
116 unsafe {
119 let _ = rusqlite::trace::config_log(Some(sqlite_log_callback));
120 }
121 });
122
123 let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
124 conn.busy_timeout(Duration::from_millis(5_000))?;
125
126 #[cfg(all(feature = "tracing", debug_assertions))]
127 install_trace_v2(&conn);
128
129 Ok(conn)
130}
131
132#[cfg(feature = "sqlite-vec")]
141pub fn open_readonly_connection_with_vec(path: &Path) -> Result<Connection, EngineError> {
142 unsafe {
144 rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute(
145 sqlite_vec::sqlite3_vec_init as *const (),
146 )));
147 }
148 open_readonly_connection(path)
149}
150
151#[cfg(feature = "sqlite-vec")]
161pub fn open_connection_with_vec(path: &Path) -> Result<Connection, EngineError> {
162 unsafe {
167 rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute(
168 sqlite_vec::sqlite3_vec_init as *const (),
169 )));
170 }
171 open_connection(path)
172}
173
174pub fn shared_sqlite_policy() -> Result<SharedSqlitePolicy, String> {
178 let mut minimum_supported_version = None;
179 let mut repo_dev_version = None;
180
181 for raw_line in SHARED_SQLITE_POLICY.lines() {
182 let line = raw_line.trim();
183 if line.is_empty() || line.starts_with('#') {
184 continue;
185 }
186
187 let Some((key, value)) = line.split_once('=') else {
188 return Err(format!("invalid sqlite policy line: {line}"));
189 };
190
191 match key.trim() {
192 "SQLITE_MIN_VERSION" => minimum_supported_version = Some(value.trim().to_owned()),
193 "SQLITE_VERSION" => repo_dev_version = Some(value.trim().to_owned()),
194 other => return Err(format!("unknown sqlite policy key: {other}")),
195 }
196 }
197
198 let minimum_supported_version =
199 minimum_supported_version.ok_or_else(|| "missing SQLITE_MIN_VERSION".to_owned())?;
200 let repo_dev_version = repo_dev_version.ok_or_else(|| "missing SQLITE_VERSION".to_owned())?;
201 let repo_local_binary_relpath =
202 PathBuf::from(format!(".local/sqlite-{repo_dev_version}/bin/sqlite3"));
203
204 Ok(SharedSqlitePolicy {
205 minimum_supported_version,
206 repo_dev_version,
207 repo_local_binary_relpath,
208 })
209}
210
211#[cfg(test)]
212#[allow(clippy::expect_used)]
213mod tests {
214 use super::shared_sqlite_policy;
215
216 #[test]
217 fn shared_sqlite_policy_matches_repo_defaults() {
218 let policy = shared_sqlite_policy().expect("shared sqlite policy");
219
220 assert_eq!(policy.minimum_supported_version, "3.41.0");
221 assert_eq!(policy.repo_dev_version, "3.46.0");
222 assert!(
223 policy
224 .repo_local_binary_relpath
225 .ends_with("sqlite-3.46.0/bin/sqlite3")
226 );
227 }
228}