Skip to main content

fathomdb_engine/
sqlite.rs

1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use rusqlite::{Connection, OpenFlags};
5
6use crate::EngineError;
7
8// Vendored copy of tooling/sqlite.env so the crate is self-contained for
9// `cargo publish`. Keep in sync with the workspace copy at
10// /tooling/sqlite.env (also referenced by the Go bridge and dev scripts).
11const 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/// Forward `SQLite` internal error/warning events into the tracing facade.
24///
25/// Registered once per process via `SQLITE_CONFIG_LOG` before any connections
26/// are opened.  The primary error code determines the tracing level:
27/// `NOTICE` → `INFO`, `WARNING` → `WARN`, everything else → `ERROR`.
28#[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/// Install `sqlite3_trace_v2` with `SQLITE_TRACE_PROFILE` on a connection.
41///
42/// Fires a TRACE-level event for each statement completion with the SQL text
43/// and execution duration.  Only registered in debug builds — TRACE events are
44/// compiled out by `release_max_level_info` in release builds, so registering
45/// the callback would waste FFI overhead on every statement for no output.
46#[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        // Safety: Once guard ensures no concurrent SQLite calls during config.
87        // config_log must be called before any connections are opened.
88        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
105/// Open a read-only database connection.
106///
107/// Uses `SQLITE_OPEN_READONLY` so that any attempt to write through this
108/// connection fails at the `SQLite` level.  Intended for reader-pool connections
109/// where the writer has already created the database and set WAL mode.
110///
111/// # Errors
112/// Returns [`EngineError`] if the database file cannot be opened.
113pub fn open_readonly_connection(path: &Path) -> Result<Connection, EngineError> {
114    #[cfg(feature = "tracing")]
115    SQLITE_LOG_INIT.call_once(|| {
116        // Safety: Once guard ensures no concurrent SQLite calls during config.
117        // config_log must be called before any connections are opened.
118        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/// Open a read-only database connection with the sqlite-vec extension loaded.
133///
134/// Combines [`open_readonly_connection`] with the `sqlite3_vec_init`
135/// auto-extension registration.
136///
137/// # Errors
138/// Returns [`EngineError`] if the underlying database connection cannot be
139/// opened (same failure modes as [`open_readonly_connection`]).
140#[cfg(feature = "sqlite-vec")]
141pub fn open_readonly_connection_with_vec(path: &Path) -> Result<Connection, EngineError> {
142    // Safety: sqlite3_auto_extension is idempotent for the same function pointer.
143    // The transmute converts the sqlite-vec init signature
144    // (db, pz_err_msg, p_api) -> c_int to the erased () -> c_int expected by
145    // sqlite3_auto_extension; SQLite passes the real args at load time.
146    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/// Open a database connection with the sqlite-vec extension loaded.
162///
163/// Registers `sqlite3_vec_init` as a global auto-extension so the extension is
164/// available in every connection opened after this call.  The registration is
165/// idempotent — `SQLite` deduplicates identical function-pointer registrations.
166///
167/// # Errors
168/// Returns [`EngineError`] if the underlying database connection cannot be
169/// opened (same failure modes as [`open_connection`]).
170#[cfg(feature = "sqlite-vec")]
171pub fn open_connection_with_vec(path: &Path) -> Result<Connection, EngineError> {
172    // Safety: sqlite3_auto_extension is idempotent for the same function pointer.
173    // The transmute converts the sqlite-vec init signature
174    // (db, pz_err_msg, p_api) -> c_int to the erased () -> c_int expected by
175    // sqlite3_auto_extension; SQLite passes the real args at load time.
176    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
191/// # Errors
192/// Returns a `String` error if the embedded `sqlite.env` policy file is malformed or missing
193/// required keys (`SQLITE_MIN_VERSION`, `SQLITE_VERSION`).
194pub 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}