walletkit_sqlite/
connection.rs1use 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
14pub struct Connection {
20 db: RawDb,
21}
22
23impl Connection {
24 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 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 pub fn execute_batch(&self, sql: &str) -> DbResult<()> {
72 self.db.exec(sql)
73 }
74
75 pub fn execute_batch_zeroized(&self, sql: &str) -> DbResult<()> {
83 self.db.exec_zeroized(sql)
84 }
85
86 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 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 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 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 pub fn transaction(&self) -> DbResult<Transaction<'_>> {
160 Transaction::begin(self, false)
161 }
162
163 pub fn transaction_immediate(&self) -> DbResult<Transaction<'_>> {
169 Transaction::begin(self, true)
170 }
171
172 #[allow(dead_code)]
174 #[must_use]
175 pub fn last_insert_rowid(&self) -> i64 {
176 self.db.last_insert_rowid()
177 }
178
179 #[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 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}