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: &Path, read_only: bool) -> DbResult<Self> {
30        Self::open_with_vfs(path, 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    /// Executes one or more SQL statements separated by semicolons.
64    ///
65    /// No result rows are returned. Suitable for DDL, PRAGMAs, and
66    /// multi-statement scripts.
67    ///
68    /// # Errors
69    ///
70    /// Returns `Error` if any statement fails.
71    pub fn execute_batch(&self, sql: &str) -> DbResult<()> {
72        self.db.exec(sql)
73    }
74
75    /// Like [`execute_batch`](Self::execute_batch) but zeroizes the internal
76    /// C string buffer after execution. Use for SQL containing sensitive
77    /// material (e.g. `PRAGMA key`).
78    ///
79    /// # Errors
80    ///
81    /// Returns `Error` if the statement fails.
82    pub fn execute_batch_zeroized(&self, sql: &str) -> DbResult<()> {
83        self.db.exec_zeroized(sql)
84    }
85
86    /// Prepares a single SQL statement.
87    ///
88    /// # Errors
89    ///
90    /// Returns `Error` if the SQL is invalid.
91    pub fn prepare(&self, sql: &str) -> DbResult<Statement<'_>> {
92        let raw_stmt = self.db.prepare(sql)?;
93        Ok(Statement::new(raw_stmt))
94    }
95
96    /// Prepares and executes a single SQL statement with the given parameters.
97    ///
98    /// Returns the number of rows changed.
99    ///
100    /// # Errors
101    ///
102    /// Returns `Error` if preparation or execution fails.
103    pub fn execute(&self, sql: &str, params: &[Value]) -> DbResult<usize> {
104        let mut stmt = self.prepare(sql)?;
105        stmt.bind_values(params)?;
106        stmt.step()?;
107        Ok(usize::try_from(self.db.changes()).unwrap_or(0))
108    }
109
110    /// Prepares and executes a statement, mapping exactly one result row.
111    ///
112    /// Returns an error if no row is returned.
113    ///
114    /// # Errors
115    ///
116    /// Returns `Error` if preparation, execution, or the mapper fails,
117    /// or if the query returns no rows.
118    pub fn query_row<T>(
119        &self,
120        sql: &str,
121        params: &[Value],
122        mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
123    ) -> DbResult<T> {
124        let mut stmt = self.prepare(sql)?;
125        stmt.bind_values(params)?;
126        match stmt.step()? {
127            StepResult::Row(row) => mapper(&row),
128            StepResult::Done => {
129                Err(Error::new(ffi::SQLITE_DONE, "query returned no rows"))
130            }
131        }
132    }
133
134    /// Like [`query_row`](Self::query_row) but returns `Ok(None)` when no row
135    /// is returned.
136    ///
137    /// # Errors
138    ///
139    /// Returns `Error` if preparation, execution, or the mapper fails.
140    pub fn query_row_optional<T>(
141        &self,
142        sql: &str,
143        params: &[Value],
144        mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
145    ) -> DbResult<Option<T>> {
146        let mut stmt = self.prepare(sql)?;
147        stmt.bind_values(params)?;
148        match stmt.step()? {
149            StepResult::Row(row) => mapper(&row).map(Some),
150            StepResult::Done => Ok(None),
151        }
152    }
153
154    /// Begins a deferred transaction.
155    ///
156    /// # Errors
157    ///
158    /// Returns `Error` if `BEGIN DEFERRED` fails.
159    pub fn transaction(&self) -> DbResult<Transaction<'_>> {
160        Transaction::begin(self, false)
161    }
162
163    /// Begins an immediate transaction (acquires a RESERVED lock right away).
164    ///
165    /// # Errors
166    ///
167    /// Returns `Error` if `BEGIN IMMEDIATE` fails.
168    pub fn transaction_immediate(&self) -> DbResult<Transaction<'_>> {
169        Transaction::begin(self, true)
170    }
171
172    /// Returns the rowid of the most recent successful INSERT.
173    #[allow(dead_code)]
174    #[must_use]
175    pub fn last_insert_rowid(&self) -> i64 {
176        self.db.last_insert_rowid()
177    }
178
179    /// Returns the number of rows changed by the most recent statement.
180    #[allow(dead_code)]
181    #[must_use]
182    pub fn changes(&self) -> usize {
183        usize::try_from(self.db.changes()).unwrap_or(0)
184    }
185}
186
187impl std::fmt::Debug for Connection {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.debug_struct("Connection").finish_non_exhaustive()
190    }
191}
192
193#[cfg(test)]
194impl Connection {
195    /// Opens an in-memory database.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Error` if the in-memory database cannot be opened.
200    pub fn open_in_memory() -> DbResult<Self> {
201        Self::open(Path::new(":memory:"), false)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::Connection;
208    use crate::params;
209    use crate::test_utils::init_sqlite;
210    use crate::Value;
211
212    #[test]
213    fn test_open_in_memory() {
214        init_sqlite();
215        let conn = Connection::open_in_memory().expect("open in-memory db");
216        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);")
217            .expect("create table");
218        conn.execute(
219            "INSERT INTO t (id, val) VALUES (?1, ?2)",
220            params![1_i64, "hello"],
221        )
222        .expect("insert");
223        let result = conn
224            .query_row("SELECT val FROM t WHERE id = ?1", params![1_i64], |stmt| {
225                Ok(stmt.column_text(0))
226            })
227            .expect("query");
228        assert_eq!(result, "hello");
229    }
230
231    #[test]
232    fn test_query_row_optional_none() {
233        init_sqlite();
234        let conn = Connection::open_in_memory().expect("open in-memory db");
235        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
236            .expect("create table");
237        let result = conn
238            .query_row_optional("SELECT id FROM t WHERE id = 999", &[], |stmt| {
239                Ok(stmt.column_i64(0))
240            })
241            .expect("query");
242        assert!(result.is_none());
243    }
244
245    #[test]
246    fn test_blob_round_trip() {
247        init_sqlite();
248        let conn = Connection::open_in_memory().expect("open in-memory db");
249        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, data BLOB);")
250            .expect("create table");
251        let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
252        conn.execute(
253            "INSERT INTO t (id, data) VALUES (?1, ?2)",
254            params![1_i64, data.as_slice()],
255        )
256        .expect("insert");
257        let result = conn
258            .query_row("SELECT data FROM t WHERE id = 1", &[], |stmt| {
259                Ok(stmt.column_blob(0))
260            })
261            .expect("query");
262        assert_eq!(result, data);
263    }
264
265    #[test]
266    fn test_null_handling() {
267        init_sqlite();
268        let conn = Connection::open_in_memory().expect("open in-memory db");
269        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);")
270            .expect("create table");
271        conn.execute(
272            "INSERT INTO t (id, val) VALUES (?1, ?2)",
273            params![1_i64, Value::Null],
274        )
275        .expect("insert");
276        let result = conn
277            .query_row("SELECT val FROM t WHERE id = 1", &[], |stmt| {
278                Ok(stmt.is_column_null(0))
279            })
280            .expect("query");
281        assert!(result);
282    }
283}