1use 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: 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 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 deserialize_readonly(&self, schema: &str, bytes: &[u8]) -> DbResult<()> {
76 self.db.deserialize_readonly(schema, bytes)
77 }
78
79 pub fn execute_batch(&self, sql: &str) -> DbResult<()> {
88 self.db.exec(sql)
89 }
90
91 pub fn execute_batch_zeroized(&self, sql: &str) -> DbResult<()> {
99 self.db.exec_zeroized(sql)
100 }
101
102 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 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 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 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 pub fn transaction(&self) -> DbResult<Transaction<'_>> {
176 Transaction::begin(self, false)
177 }
178
179 pub fn transaction_immediate(&self) -> DbResult<Transaction<'_>> {
185 Transaction::begin(self, true)
186 }
187
188 #[allow(dead_code)]
190 #[must_use]
191 pub fn last_insert_rowid(&self) -> i64 {
192 self.db.last_insert_rowid()
193 }
194
195 #[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 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}