Skip to main content

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