Skip to main content

walletkit_sqlite/
connection.rs

1//! Safe wrapper around a `SQLite` database connection.
2//!
3//! This file contains **no `unsafe` code**. All FFI interaction is delegated to
4//! [`ffi::RawDb`] which encapsulates the raw pointers and C type conversions.
5
6use std::path::Path;
7
8use super::error::{DbResult, Error};
9use super::ffi::{self, RawDb};
10use super::statement::{Row, Statement, StepResult};
11use super::transaction::Transaction;
12use super::value::Value;
13
14/// A `SQLite` database connection.
15///
16/// Closed when dropped. Not `Sync` -- all access must happen from a single
17/// thread (matches the WASM single-thread constraint and the native
18/// `Mutex`-guarded usage in `CredentialStoreInner`).
19pub struct Connection {
20    db: RawDb,
21}
22
23impl Connection {
24    /// Opens (or creates) a database at `path`.
25    ///
26    /// # Errors
27    ///
28    /// Returns `Error` if `SQLite` cannot open the file.
29    pub fn open(path: impl AsRef<Path>, read_only: bool) -> DbResult<Self> {
30        Self::open_with_vfs(path.as_ref(), read_only, None)
31    }
32
33    #[cfg(target_arch = "wasm32")]
34    pub(crate) fn open_with_opfs_vfs(path: &Path, read_only: bool) -> DbResult<Self> {
35        if !crate::opfs::is_installed() {
36            return Err(Error::new(
37                -1,
38                "persistent OPFS storage must be installed before opening a database",
39            ));
40        }
41
42        Self::open_with_vfs(path, read_only, Some(crate::opfs::ENCRYPTED_VFS_NAME))
43    }
44
45    /// Opens (or creates) a database using an explicitly selected VFS.
46    fn open_with_vfs(
47        path: &Path,
48        read_only: bool,
49        vfs: Option<&str>,
50    ) -> DbResult<Self> {
51        let path_str = path.to_string_lossy();
52        let flags = if read_only {
53            ffi::SQLITE_OPEN_READONLY | ffi::SQLITE_OPEN_FULLMUTEX
54        } else {
55            ffi::SQLITE_OPEN_READWRITE
56                | ffi::SQLITE_OPEN_CREATE
57                | ffi::SQLITE_OPEN_FULLMUTEX
58        };
59        let db = RawDb::open(&path_str, flags, vfs)?;
60        Ok(Self { db })
61    }
62
63    /// Replaces an existing schema with a read-only in-memory database image.
64    ///
65    /// Copies `bytes` into SQLite-owned memory, freed when the schema is detached
66    /// or the connection closes. The input can be dropped immediately afterward.
67    /// Use a disposable connection for untrusted images: malformed schemas can
68    /// prevent further operations on that connection, including detaching them.
69    /// The image must be a complete plaintext database in rollback-journal mode;
70    /// WAL sidecars are not loaded. SQLite validates database contents when queried.
71    ///
72    /// # Errors
73    /// Returns an error if the schema is unavailable or busy, the image is empty,
74    /// or allocating or deserializing it fails.
75    pub fn deserialize_readonly(&self, schema: &str, bytes: &[u8]) -> DbResult<()> {
76        self.db.deserialize_readonly(schema, bytes)
77    }
78
79    /// Executes one or more SQL statements separated by semicolons.
80    ///
81    /// No result rows are returned. Suitable for DDL, PRAGMAs, and
82    /// multi-statement scripts.
83    ///
84    /// # Errors
85    ///
86    /// Returns `Error` if any statement fails.
87    pub fn execute_batch(&self, sql: &str) -> DbResult<()> {
88        self.db.exec(sql)
89    }
90
91    /// Like [`execute_batch`](Self::execute_batch) but zeroizes the internal
92    /// C string buffer after execution. Use for SQL containing sensitive
93    /// material (e.g. `PRAGMA key`).
94    ///
95    /// # Errors
96    ///
97    /// Returns `Error` if the statement fails.
98    pub fn execute_batch_zeroized(&self, sql: &str) -> DbResult<()> {
99        self.db.exec_zeroized(sql)
100    }
101
102    /// Prepares a single SQL statement.
103    ///
104    /// # Errors
105    ///
106    /// Returns `Error` if the SQL is invalid.
107    pub fn prepare(&self, sql: &str) -> DbResult<Statement<'_>> {
108        let raw_stmt = self.db.prepare(sql)?;
109        Ok(Statement::new(raw_stmt))
110    }
111
112    /// Prepares and executes a single SQL statement with the given parameters.
113    ///
114    /// Returns the number of rows changed.
115    ///
116    /// # Errors
117    ///
118    /// Returns `Error` if preparation or execution fails.
119    pub fn execute(&self, sql: &str, params: &[Value]) -> DbResult<usize> {
120        let mut stmt = self.prepare(sql)?;
121        stmt.bind_values(params)?;
122        stmt.step()?;
123        Ok(usize::try_from(self.db.changes()).unwrap_or(0))
124    }
125
126    /// Prepares and executes a statement, mapping exactly one result row.
127    ///
128    /// Returns an error if no row is returned.
129    ///
130    /// # Errors
131    ///
132    /// Returns `Error` if preparation, execution, or the mapper fails,
133    /// or if the query returns no rows.
134    pub fn query_row<T>(
135        &self,
136        sql: &str,
137        params: &[Value],
138        mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
139    ) -> DbResult<T> {
140        let mut stmt = self.prepare(sql)?;
141        stmt.bind_values(params)?;
142        match stmt.step()? {
143            StepResult::Row(row) => mapper(&row),
144            StepResult::Done => {
145                Err(Error::new(ffi::SQLITE_DONE, "query returned no rows"))
146            }
147        }
148    }
149
150    /// Like [`query_row`](Self::query_row) but returns `Ok(None)` when no row
151    /// is returned.
152    ///
153    /// # Errors
154    ///
155    /// Returns `Error` if preparation, execution, or the mapper fails.
156    pub fn query_row_optional<T>(
157        &self,
158        sql: &str,
159        params: &[Value],
160        mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
161    ) -> DbResult<Option<T>> {
162        let mut stmt = self.prepare(sql)?;
163        stmt.bind_values(params)?;
164        match stmt.step()? {
165            StepResult::Row(row) => mapper(&row).map(Some),
166            StepResult::Done => Ok(None),
167        }
168    }
169
170    /// Begins a deferred transaction.
171    ///
172    /// # Errors
173    ///
174    /// Returns `Error` if `BEGIN DEFERRED` fails.
175    pub fn transaction(&self) -> DbResult<Transaction<'_>> {
176        Transaction::begin(self, false)
177    }
178
179    /// Begins an immediate transaction (acquires a RESERVED lock right away).
180    ///
181    /// # Errors
182    ///
183    /// Returns `Error` if `BEGIN IMMEDIATE` fails.
184    pub fn transaction_immediate(&self) -> DbResult<Transaction<'_>> {
185        Transaction::begin(self, true)
186    }
187
188    /// Returns the rowid of the most recent successful INSERT.
189    #[allow(dead_code)]
190    #[must_use]
191    pub fn last_insert_rowid(&self) -> i64 {
192        self.db.last_insert_rowid()
193    }
194
195    /// Returns the number of rows changed by the most recent statement.
196    #[allow(dead_code)]
197    #[must_use]
198    pub fn changes(&self) -> usize {
199        usize::try_from(self.db.changes()).unwrap_or(0)
200    }
201}
202
203impl std::fmt::Debug for Connection {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        f.debug_struct("Connection").finish_non_exhaustive()
206    }
207}
208
209#[cfg(test)]
210impl Connection {
211    /// Opens an in-memory database.
212    ///
213    /// # Errors
214    ///
215    /// Returns `Error` if the in-memory database cannot be opened.
216    pub fn open_in_memory() -> DbResult<Self> {
217        Self::open(":memory:", false)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::Connection;
224    use crate::params;
225    use crate::test_utils::init_sqlite;
226    use crate::Value;
227
228    #[test]
229    fn test_open_in_memory() {
230        init_sqlite();
231        let conn = Connection::open_in_memory().expect("open in-memory db");
232        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);")
233            .expect("create table");
234        conn.execute(
235            "INSERT INTO t (id, val) VALUES (?1, ?2)",
236            params![1_i64, "hello"],
237        )
238        .expect("insert");
239        let result = conn
240            .query_row("SELECT val FROM t WHERE id = ?1", params![1_i64], |stmt| {
241                Ok(stmt.column_text(0))
242            })
243            .expect("query");
244        assert_eq!(result, "hello");
245    }
246
247    #[cfg(not(target_arch = "wasm32"))]
248    #[test]
249    fn test_deserialize_readonly_owns_image_and_preserves_main() {
250        init_sqlite();
251        let dir = tempfile::tempdir().unwrap();
252        let path = dir.path().join("source.sqlite");
253        {
254            let source = Connection::open(&path, false).unwrap();
255            source
256                .execute_batch(
257                    "CREATE TABLE t(value TEXT); INSERT INTO t VALUES ('backup');",
258                )
259                .unwrap();
260        }
261        let mut image = std::fs::read(&path).unwrap();
262        let conn = Connection::open_in_memory().unwrap();
263        conn.execute_batch("CREATE TABLE local(value TEXT); ATTACH DATABASE ':memory:' AS incoming KEY '';")
264            .unwrap();
265        conn.deserialize_readonly("incoming", &image).unwrap();
266        image.fill(0);
267        drop(image);
268
269        let source_value = conn
270            .query_row("SELECT value FROM incoming.t", &[], |row| {
271                Ok(row.column_text(0))
272            })
273            .unwrap();
274        assert_eq!(source_value, "backup");
275        assert!(conn
276            .execute_batch("INSERT INTO incoming.t VALUES ('changed');")
277            .is_err());
278        conn.execute_batch("INSERT INTO local SELECT value FROM incoming.t;")
279            .unwrap();
280        conn.execute_batch("DETACH DATABASE incoming;").unwrap();
281        let destination_value = conn
282            .query_row("SELECT value FROM local", &[], |row| Ok(row.column_text(0)))
283            .unwrap();
284        assert_eq!(destination_value, "backup");
285
286        let image = std::fs::read(&path).unwrap();
287        assert!(conn.deserialize_readonly("missing", &image).is_err());
288        assert!(conn.deserialize_readonly("temp", &image).is_err());
289        conn.execute_batch("ATTACH DATABASE ':memory:' AS incoming KEY '';")
290            .unwrap();
291        conn.deserialize_readonly("incoming", &image).unwrap();
292        let reloaded_count = conn
293            .query_row("SELECT COUNT(*) FROM incoming.t", &[], |row| {
294                Ok(row.column_i64(0))
295            })
296            .unwrap();
297        assert_eq!(reloaded_count, 1);
298        conn.execute_batch("DETACH DATABASE incoming;").unwrap();
299    }
300
301    #[test]
302    fn test_query_row_optional_none() {
303        init_sqlite();
304        let conn = Connection::open_in_memory().expect("open in-memory db");
305        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
306            .expect("create table");
307        let result = conn
308            .query_row_optional("SELECT id FROM t WHERE id = 999", &[], |stmt| {
309                Ok(stmt.column_i64(0))
310            })
311            .expect("query");
312        assert!(result.is_none());
313    }
314
315    #[test]
316    fn test_blob_round_trip() {
317        init_sqlite();
318        let conn = Connection::open_in_memory().expect("open in-memory db");
319        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, data BLOB);")
320            .expect("create table");
321        let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
322        conn.execute(
323            "INSERT INTO t (id, data) VALUES (?1, ?2)",
324            params![1_i64, data.as_slice()],
325        )
326        .expect("insert");
327        let result = conn
328            .query_row("SELECT data FROM t WHERE id = 1", &[], |stmt| {
329                Ok(stmt.column_blob(0))
330            })
331            .expect("query");
332        assert_eq!(result, data);
333    }
334
335    #[test]
336    fn test_null_handling() {
337        init_sqlite();
338        let conn = Connection::open_in_memory().expect("open in-memory db");
339        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);")
340            .expect("create table");
341        conn.execute(
342            "INSERT INTO t (id, val) VALUES (?1, ?2)",
343            params![1_i64, Value::Null],
344        )
345        .expect("insert");
346        let result = conn
347            .query_row("SELECT val FROM t WHERE id = 1", &[], |stmt| {
348                Ok(stmt.is_column_null(0))
349            })
350            .expect("query");
351        assert!(result);
352    }
353}