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_secs(5))?;
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_secs(5))?;
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 {
147 rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute::<
148 *const (),
149 unsafe extern "C" fn(
150 *mut rusqlite::ffi::sqlite3,
151 *mut *mut std::os::raw::c_char,
152 *const rusqlite::ffi::sqlite3_api_routines,
153 ) -> i32,
154 >(
155 sqlite_vec::sqlite3_vec_init as *const ()
156 )));
157 }
158 open_readonly_connection(path)
159}
160
161#[cfg(feature = "sqlite-vec")]
171pub fn open_connection_with_vec(path: &Path) -> Result<Connection, EngineError> {
172 unsafe {
177 rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute::<
178 *const (),
179 unsafe extern "C" fn(
180 *mut rusqlite::ffi::sqlite3,
181 *mut *mut std::os::raw::c_char,
182 *const rusqlite::ffi::sqlite3_api_routines,
183 ) -> i32,
184 >(
185 sqlite_vec::sqlite3_vec_init as *const ()
186 )));
187 }
188 open_connection(path)
189}
190
191pub fn shared_sqlite_policy() -> Result<SharedSqlitePolicy, String> {
195 let mut minimum_supported_version = None;
196 let mut repo_dev_version = None;
197
198 for raw_line in SHARED_SQLITE_POLICY.lines() {
199 let line = raw_line.trim();
200 if line.is_empty() || line.starts_with('#') {
201 continue;
202 }
203
204 let Some((key, value)) = line.split_once('=') else {
205 return Err(format!("invalid sqlite policy line: {line}"));
206 };
207
208 match key.trim() {
209 "SQLITE_MIN_VERSION" => minimum_supported_version = Some(value.trim().to_owned()),
210 "SQLITE_VERSION" => repo_dev_version = Some(value.trim().to_owned()),
211 other => return Err(format!("unknown sqlite policy key: {other}")),
212 }
213 }
214
215 let minimum_supported_version =
216 minimum_supported_version.ok_or_else(|| "missing SQLITE_MIN_VERSION".to_owned())?;
217 let repo_dev_version = repo_dev_version.ok_or_else(|| "missing SQLITE_VERSION".to_owned())?;
218 let repo_local_binary_relpath =
219 PathBuf::from(format!(".local/sqlite-{repo_dev_version}/bin/sqlite3"));
220
221 Ok(SharedSqlitePolicy {
222 minimum_supported_version,
223 repo_dev_version,
224 repo_local_binary_relpath,
225 })
226}
227
228#[cfg(test)]
229#[allow(clippy::expect_used)]
230mod tests {
231 use super::shared_sqlite_policy;
232
233 #[test]
234 fn shared_sqlite_policy_matches_repo_defaults() {
235 let policy = shared_sqlite_policy().expect("shared sqlite policy");
236
237 assert_eq!(policy.minimum_supported_version, "3.41.0");
238 assert_eq!(policy.repo_dev_version, "3.46.0");
239 assert!(
240 policy
241 .repo_local_binary_relpath
242 .ends_with("sqlite-3.46.0/bin/sqlite3")
243 );
244 }
245}