1#![allow(
9 rustdoc::bare_urls,
10 rustdoc::invalid_html_tags,
11 rustdoc::broken_intra_doc_links
12)]
13#![allow(
14 clippy::collapsible_match,
15 clippy::doc_overindented_list_items,
16 clippy::from_over_into
17)]
18
19pub mod params;
20pub mod value;
21
22pub use value::Value;
23
24pub use params::params_from_iter;
25
26use crate::params::*;
27use std::borrow::Cow;
28use std::fmt::Debug;
29use std::num::NonZero;
30use std::sync::{Arc, Mutex};
31
32#[derive(Debug, thiserror::Error)]
33pub enum Error {
34 #[error("SQL conversion failure: `{0}`")]
35 ToSqlConversionFailure(BoxError),
36 #[error("Mutex lock error: {0}")]
37 MutexError(String),
38 #[error("SQL execution failure: `{0}`")]
39 SqlExecutionFailure(String),
40 #[error("database schema has changed")]
43 SchemaChanged,
44}
45
46impl Error {
47 pub fn is_schema_changed(&self) -> bool {
51 matches!(self, Error::SchemaChanged)
52 }
53}
54
55impl From<limbo_core::LimboError> for Error {
56 fn from(err: limbo_core::LimboError) -> Self {
57 match err {
58 limbo_core::LimboError::SchemaChanged => Error::SchemaChanged,
59 other => Error::SqlExecutionFailure(other.to_string()),
60 }
61 }
62}
63
64pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
65
66pub type Result<T> = std::result::Result<T, Error>;
67pub struct Builder {
68 path: String,
69}
70
71impl Builder {
72 pub fn new_local(path: &str) -> Self {
73 Self {
74 path: path.to_string(),
75 }
76 }
77
78 #[allow(unused_variables, clippy::arc_with_non_send_sync)]
79 pub async fn build(self) -> Result<Database> {
80 match self.path.as_str() {
81 ":memory:" => {
82 let io: Arc<dyn limbo_core::IO> = Arc::new(limbo_core::MemoryIO::new());
83 let db = limbo_core::Database::open_file(io, self.path.as_str(), false)?;
84 Ok(Database { inner: db })
85 }
86 path => {
87 let io: Arc<dyn limbo_core::IO> = Arc::new(limbo_core::PlatformIO::new()?);
88 let db = limbo_core::Database::open_file(io, path, false)?;
89 Ok(Database { inner: db })
90 }
91 }
92 }
93}
94
95#[derive(Clone)]
96pub struct Database {
97 inner: Arc<limbo_core::Database>,
98}
99
100unsafe impl Send for Database {}
101unsafe impl Sync for Database {}
102
103impl Debug for Database {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("Database").finish()
106 }
107}
108
109impl Database {
110 pub fn connect(&self) -> Result<Connection> {
111 let conn = self.inner.connect()?;
112 #[allow(clippy::arc_with_non_send_sync)]
113 let connection = Connection {
114 inner: Arc::new(Mutex::new(conn)),
115 };
116 Ok(connection)
117 }
118
119 #[allow(clippy::arc_with_non_send_sync)]
137 pub fn open_from_bytes(bytes: &[u8]) -> Result<Database> {
138 let inner = limbo_core::Database::open_from_bytes(bytes, false)?;
139 Ok(Database { inner })
140 }
141}
142
143pub struct Connection {
144 inner: Arc<Mutex<Arc<limbo_core::Connection>>>,
145}
146
147impl Clone for Connection {
148 fn clone(&self) -> Self {
149 Self {
150 inner: Arc::clone(&self.inner),
151 }
152 }
153}
154
155unsafe impl Send for Connection {}
156unsafe impl Sync for Connection {}
157
158impl Connection {
159 pub async fn query(&self, sql: &str, params: impl IntoParams) -> Result<Rows> {
160 let mut stmt = self.prepare(sql).await?;
161 stmt.query(params).await
162 }
163
164 pub async fn execute(&self, sql: &str, params: impl IntoParams) -> Result<u64> {
165 let mut stmt = self.prepare(sql).await?;
166 stmt.execute(params).await
167 }
168
169 pub async fn prepare(&self, sql: &str) -> Result<Statement> {
170 let conn = self
171 .inner
172 .lock()
173 .map_err(|e| Error::MutexError(e.to_string()))?;
174
175 let stmt = conn.prepare(sql)?;
176
177 #[allow(clippy::arc_with_non_send_sync)]
178 let statement = Statement {
179 inner: Arc::new(Mutex::new(stmt)),
180 };
181 Ok(statement)
182 }
183
184 pub fn changes(&self) -> Result<i64> {
188 let conn = self
189 .inner
190 .lock()
191 .map_err(|e| Error::MutexError(e.to_string()))?;
192 Ok(conn.changes())
193 }
194
195 pub fn pragma_query<F>(&self, pragma_name: &str, mut f: F) -> Result<()>
196 where
197 F: FnMut(&Row) -> limbo_core::Result<()>,
198 {
199 let conn = self
200 .inner
201 .lock()
202 .map_err(|e| Error::MutexError(e.to_string()))?;
203
204 let rows: Vec<Row> = conn
205 .pragma_query(pragma_name)
206 .map_err(|e| Error::SqlExecutionFailure(e.to_string()))?
207 .iter()
208 .map(|row| row.iter().collect::<Row>())
209 .collect();
210
211 rows.iter().try_for_each(|row| {
212 f(row).map_err(|e| {
213 Error::SqlExecutionFailure(format!("Error executing user defined function: {}", e))
214 })
215 })?;
216 Ok(())
217 }
218}
219
220pub struct Statement {
221 inner: Arc<Mutex<limbo_core::Statement>>,
222}
223
224impl Clone for Statement {
225 fn clone(&self) -> Self {
226 Self {
227 inner: Arc::clone(&self.inner),
228 }
229 }
230}
231
232unsafe impl Send for Statement {}
233unsafe impl Sync for Statement {}
234
235impl Statement {
236 pub async fn query(&mut self, params: impl IntoParams) -> Result<Rows> {
237 let params = params.into_params()?;
238 match params {
239 params::Params::None => (),
240 params::Params::Positional(values) => {
241 for (i, value) in values.into_iter().enumerate() {
242 let mut stmt = self
243 .inner
244 .lock()
245 .map_err(|e| Error::MutexError(e.to_string()))?;
246 if let Some(idx) = NonZero::new(i + 1) {
247 stmt.bind_at(idx, value.into());
248 }
249 }
250 }
251 params::Params::Named(items) => self.bind_named_params(items)?,
252 }
253 #[allow(clippy::arc_with_non_send_sync)]
254 let rows = Rows {
255 inner: Arc::clone(&self.inner),
256 };
257 Ok(rows)
258 }
259
260 pub async fn execute(&mut self, params: impl IntoParams) -> Result<u64> {
261 {
262 self.inner
264 .lock()
265 .map_err(|e| Error::MutexError(e.to_string()))?
266 .reset();
267 }
268 let params = params.into_params()?;
269 match params {
270 params::Params::None => (),
271 params::Params::Positional(values) => {
272 for (i, value) in values.into_iter().enumerate() {
273 let mut stmt = self
274 .inner
275 .lock()
276 .map_err(|e| Error::MutexError(e.to_string()))?;
277 if let Some(idx) = NonZero::new(i + 1) {
278 stmt.bind_at(idx, value.into());
279 }
280 }
281 }
282 params::Params::Named(items) => self.bind_named_params(items)?,
283 }
284 loop {
285 let mut stmt = self
286 .inner
287 .lock()
288 .map_err(|e| Error::MutexError(e.to_string()))?;
289 match stmt.step() {
290 Ok(limbo_core::StepResult::Row) => {
291 return Ok(2);
293 }
294 Ok(limbo_core::StepResult::Done) => {
295 return Ok(0);
296 }
297 Ok(limbo_core::StepResult::IO) => {
298 let _ = stmt.run_once();
299 }
301 Ok(limbo_core::StepResult::Busy) => {
302 return Ok(4);
303 }
304 Ok(limbo_core::StepResult::Interrupt) => {
305 return Ok(3);
306 }
307 Err(err) => {
308 return Err(err.into());
309 }
310 }
311 }
312 }
313
314 fn bind_named_params(&self, items: Vec<(Cow<'static, str>, Value)>) -> Result<()> {
333 let mut stmt = self
334 .inner
335 .lock()
336 .map_err(|e| Error::MutexError(e.to_string()))?;
337 for (name, value) in items {
338 let idx = stmt.parameters().index(name.as_ref()).ok_or_else(|| {
339 Error::SqlExecutionFailure(format!(
340 "no bind parameter named `{name}` in this prepared statement"
341 ))
342 })?;
343 stmt.bind_at(idx, value.into());
344 }
345 Ok(())
346 }
347
348 pub fn columns(&self) -> Vec<Column> {
349 let Ok(stmt) = self.inner.lock() else {
350 return Vec::new();
351 };
352
353 let n = stmt.num_columns();
354
355 let mut cols = Vec::with_capacity(n);
356
357 for i in 0..n {
358 let name = stmt.get_column_name(i).into_owned();
359 let decl_type = stmt.get_column_decl_type(i).map(|s| s.into_owned());
360 cols.push(Column { name, decl_type });
361 }
362
363 cols
364 }
365}
366
367pub struct Column {
368 name: String,
369 decl_type: Option<String>,
370}
371
372impl Column {
373 pub fn name(&self) -> &str {
374 &self.name
375 }
376
377 pub fn decl_type(&self) -> Option<&str> {
378 self.decl_type.as_deref()
379 }
380}
381
382pub trait IntoValue {
383 fn into_value(self) -> Result<Value>;
384}
385
386#[derive(Debug, Clone)]
387pub enum Params {
388 None,
389 Positional(Vec<Value>),
390 Named(Vec<(String, Value)>),
391}
392pub struct Transaction {}
393
394pub struct Rows {
395 inner: Arc<Mutex<limbo_core::Statement>>,
396}
397
398impl Clone for Rows {
399 fn clone(&self) -> Self {
400 Self {
401 inner: Arc::clone(&self.inner),
402 }
403 }
404}
405
406unsafe impl Send for Rows {}
407unsafe impl Sync for Rows {}
408
409impl Rows {
410 pub async fn next(&mut self) -> Result<Option<Row>> {
411 loop {
412 let mut stmt = self
413 .inner
414 .lock()
415 .map_err(|e| Error::MutexError(e.to_string()))?;
416 match stmt.step() {
417 Ok(limbo_core::StepResult::Row) => {
418 let row = stmt.row().ok_or_else(|| {
419 Error::SqlExecutionFailure(
420 "row unavailable after Row step result".to_string(),
421 )
422 })?;
423 return Ok(Some(Row {
424 values: row.get_values().map(|v| v.to_owned()).collect(),
425 }));
426 }
427 Ok(limbo_core::StepResult::Done) => return Ok(None),
428 Ok(limbo_core::StepResult::IO) => {
429 if let Err(e) = stmt.run_once() {
430 return Err(e.into());
431 }
432 continue;
433 }
434 Ok(limbo_core::StepResult::Busy) => return Ok(None),
435 Ok(limbo_core::StepResult::Interrupt) => return Ok(None),
436 _ => return Ok(None),
437 }
438 }
439 }
440}
441
442#[derive(Debug)]
443pub struct Row {
444 values: Vec<limbo_core::Value>,
445}
446
447unsafe impl Send for Row {}
448unsafe impl Sync for Row {}
449
450impl Row {
451 pub fn get_value(&self, index: usize) -> Result<Value> {
452 let value = &self.values[index];
453 match value {
454 limbo_core::Value::Integer(i) => Ok(Value::Integer(*i)),
455 limbo_core::Value::Null => Ok(Value::Null),
456 limbo_core::Value::Float(f) => Ok(Value::Real(*f)),
457 limbo_core::Value::Text(text) => Ok(Value::Text(text.to_string())),
458 limbo_core::Value::Blob(items) => Ok(Value::Blob(items.to_vec())),
459 }
460 }
461
462 pub fn column_count(&self) -> usize {
463 self.values.len()
464 }
465}
466
467impl<'a> FromIterator<&'a limbo_core::Value> for Row {
468 fn from_iter<T: IntoIterator<Item = &'a limbo_core::Value>>(iter: T) -> Self {
469 let values = iter
470 .into_iter()
471 .map(|v| match v {
472 limbo_core::Value::Integer(i) => limbo_core::Value::Integer(*i),
473 limbo_core::Value::Null => limbo_core::Value::Null,
474 limbo_core::Value::Float(f) => limbo_core::Value::Float(*f),
475 limbo_core::Value::Text(s) => limbo_core::Value::Text(s.clone()),
476 limbo_core::Value::Blob(b) => limbo_core::Value::Blob(b.clone()),
477 })
478 .collect();
479
480 Row { values }
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use tempfile::NamedTempFile;
488
489 #[tokio::test]
490 async fn test_database_persistence() -> Result<()> {
491 let temp_file = NamedTempFile::new().unwrap();
492 let db_path = temp_file.path().to_str().unwrap();
493
494 {
496 let db = Builder::new_local(db_path).build().await?;
497 let conn = db.connect()?;
498 conn.execute(
499 "CREATE TABLE test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
500 (),
501 )
502 .await?;
503 conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ())
504 .await?;
505 conn.execute("INSERT INTO test_persistence (name) VALUES ('Bob');", ())
506 .await?;
507 } let db = Builder::new_local(db_path).build().await?;
511 let conn = db.connect()?;
512
513 let mut rows = conn
514 .query("SELECT name FROM test_persistence ORDER BY id;", ())
515 .await?;
516
517 let row1 = rows.next().await?.expect("Expected first row");
518 assert_eq!(row1.get_value(0)?, Value::Text("Alice".to_string()));
519
520 let row2 = rows.next().await?.expect("Expected second row");
521 assert_eq!(row2.get_value(0)?, Value::Text("Bob".to_string()));
522
523 assert!(rows.next().await?.is_none(), "Expected no more rows");
524
525 Ok(())
526 }
527
528 #[tokio::test]
529 async fn test_database_persistence_many_frames() -> Result<()> {
530 let temp_file = NamedTempFile::new().unwrap();
531 let db_path = temp_file.path().to_str().unwrap();
532
533 const NUM_INSERTS: usize = 100;
534 const TARGET_STRING_LEN: usize = 1024; let mut original_data = Vec::with_capacity(NUM_INSERTS);
537 for i in 0..NUM_INSERTS {
538 let prefix = format!("test_string_{:04}_", i);
539 let padding_len = TARGET_STRING_LEN.saturating_sub(prefix.len());
540 let padding: String = "A".repeat(padding_len);
541 original_data.push(format!("{}{}", prefix, padding));
542 }
543
544 {
546 let db = Builder::new_local(db_path).build().await?;
547 let conn = db.connect()?;
548 conn.execute(
549 "CREATE TABLE test_large_persistence (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL);",
550 (),
551 )
552 .await?;
553
554 for data_val in &original_data {
555 conn.execute(
556 "INSERT INTO test_large_persistence (data) VALUES (?);",
557 params::Params::Positional(vec![Value::Text(data_val.clone())]),
558 )
559 .await?;
560 }
561 } let db = Builder::new_local(db_path).build().await?;
565 let conn = db.connect()?;
566
567 let mut rows = conn
568 .query("SELECT data FROM test_large_persistence ORDER BY id;", ())
569 .await?;
570
571 for (i, expected) in original_data.iter().enumerate().take(NUM_INSERTS) {
572 let row = rows
573 .next()
574 .await?
575 .unwrap_or_else(|| panic!("Expected row {} but found None", i));
576 assert_eq!(
577 row.get_value(0)?,
578 Value::Text(expected.clone()),
579 "Mismatch in retrieved data for row {}",
580 i
581 );
582 }
583
584 assert!(
585 rows.next().await?.is_none(),
586 "Expected no more rows after retrieving all inserted data"
587 );
588
589 let wal_path = format!("{}-wal", db_path);
591 std::fs::remove_file(&wal_path)
592 .map_err(|e| eprintln!("Warning: Failed to delete WAL file for test: {}", e))
593 .unwrap();
594
595 let db_after_wal_delete = Builder::new_local(db_path).build().await?;
601 let conn_after_wal_delete = db_after_wal_delete.connect()?;
602
603 let mut rows_after_wal_delete = conn_after_wal_delete
604 .query("SELECT data FROM test_large_persistence ORDER BY id;", ())
605 .await?;
606
607 for (i, expected) in original_data.iter().enumerate().take(NUM_INSERTS) {
608 let row = rows_after_wal_delete.next().await?.unwrap_or_else(|| {
609 panic!(
610 "Expected row {} after WAL deletion but found None; \
611 checkpoint-on-close should have persisted it into the main DB",
612 i
613 )
614 });
615 assert_eq!(
616 row.get_value(0)?,
617 Value::Text(expected.clone()),
618 "Mismatch in retrieved data for row {} after WAL deletion",
619 i
620 );
621 }
622
623 assert!(
624 rows_after_wal_delete.next().await?.is_none(),
625 "Expected no more rows after WAL deletion once all checkpointed data was retrieved"
626 );
627
628 Ok(())
629 }
630
631 #[tokio::test]
632 async fn test_database_persistence_write_one_frame_many_times() -> Result<()> {
633 let temp_file = NamedTempFile::new().unwrap();
634 let db_path = temp_file.path().to_str().unwrap();
635
636 for i in 0..100 {
637 {
638 let db = Builder::new_local(db_path).build().await?;
639 let conn = db.connect()?;
640
641 conn.execute("CREATE TABLE IF NOT EXISTS test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
642 conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ())
643 .await?;
644 }
645 {
646 let db = Builder::new_local(db_path).build().await?;
647 let conn = db.connect()?;
648
649 let mut rows_iter = conn
650 .query("SELECT count(*) FROM test_persistence;", ())
651 .await?;
652 let rows = rows_iter.next().await?.unwrap();
653 assert_eq!(rows.get_value(0)?, Value::Integer(i as i64 + 1));
654 assert!(rows_iter.next().await?.is_none());
655 }
656 }
657
658 Ok(())
659 }
660
661 async fn query_scalar_i64(conn: &Connection, sql: &str) -> Result<i64> {
667 let mut rows = conn.query(sql, ()).await?;
668 let row = rows
669 .next()
670 .await?
671 .unwrap_or_else(|| panic!("expected a row from `{sql}`"));
672 match row.get_value(0)? {
673 Value::Integer(i) => Ok(i),
674 other => panic!("expected Integer from `{sql}`, got {other:?}"),
675 }
676 }
677
678 #[tokio::test]
679 async fn test_application_id_write_read_round_trip() -> Result<()> {
680 let db = Builder::new_local(":memory:").build().await?;
681 let conn = db.connect()?;
682
683 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, 0);
685
686 conn.execute("PRAGMA application_id = 1196444487;", ())
688 .await?;
689 assert_eq!(
690 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
691 1196444487
692 );
693
694 conn.execute("PRAGMA application_id = 42;", ()).await?;
696 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, 42);
697
698 Ok(())
699 }
700
701 #[tokio::test]
702 async fn test_application_id_negative_round_trip() -> Result<()> {
703 let db = Builder::new_local(":memory:").build().await?;
706 let conn = db.connect()?;
707
708 conn.execute("PRAGMA application_id = -1;", ()).await?;
709 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, -1);
710
711 conn.execute("PRAGMA application_id = -2147483648;", ())
712 .await?;
713 assert_eq!(
714 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
715 -2147483648
716 );
717
718 Ok(())
719 }
720
721 struct TempDbPath {
727 path: std::path::PathBuf,
728 }
729
730 impl TempDbPath {
731 fn new(tag: &str) -> Self {
732 use std::sync::atomic::{AtomicU64, Ordering};
733 static COUNTER: AtomicU64 = AtomicU64::new(0);
734 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
735 let mut path = std::env::temp_dir();
736 path.push(format!(
737 "oxisqlite_dur_{}_{}_{}.db",
738 tag,
739 std::process::id(),
740 n
741 ));
742 let _ = std::fs::remove_file(&path);
744 let _ = std::fs::remove_file(format!("{}-wal", path.display()));
745 Self { path }
746 }
747
748 fn as_str(&self) -> &str {
749 self.path
750 .to_str()
751 .expect("temp db path is valid UTF-8 on the test platforms")
752 }
753 }
754
755 impl Drop for TempDbPath {
756 fn drop(&mut self) {
757 let _ = std::fs::remove_file(&self.path);
758 let _ = std::fs::remove_file(format!("{}-wal", self.path.display()));
759 }
760 }
761
762 #[tokio::test]
768 async fn test_application_id_persistence() -> Result<()> {
769 let temp = TempDbPath::new("app_id_persist");
770 let db_path = temp.as_str();
771
772 {
773 let db = Builder::new_local(db_path).build().await?;
774 let conn = db.connect()?;
775 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);", ())
776 .await?;
777 conn.execute("PRAGMA application_id = -12345;", ()).await?;
778
779 assert_eq!(
781 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
782 -12345
783 );
784 } let db = Builder::new_local(db_path).build().await?;
788 let conn = db.connect()?;
789 assert_eq!(
790 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
791 -12345,
792 "application_id must survive close/reopen"
793 );
794
795 Ok(())
796 }
797
798 #[tokio::test]
801 async fn test_application_id_durable_reopen() -> Result<()> {
802 let temp = TempDbPath::new("app_id_reopen");
803 let db_path = temp.as_str();
804
805 {
806 let db = Builder::new_local(db_path).build().await?;
807 let conn = db.connect()?;
808 conn.execute("PRAGMA application_id = 12345;", ()).await?;
809 assert_eq!(
810 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
811 12345
812 );
813 }
814
815 let db = Builder::new_local(db_path).build().await?;
816 let conn = db.connect()?;
817 assert_eq!(
818 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
819 12345,
820 "application_id = 12345 must survive close/reopen"
821 );
822
823 Ok(())
824 }
825
826 #[tokio::test]
829 async fn test_user_version_durable_reopen() -> Result<()> {
830 let temp = TempDbPath::new("user_version_reopen");
831 let db_path = temp.as_str();
832
833 {
834 let db = Builder::new_local(db_path).build().await?;
835 let conn = db.connect()?;
836 conn.execute("PRAGMA user_version = 12345;", ()).await?;
837 assert_eq!(
838 query_scalar_i64(&conn, "PRAGMA user_version;").await?,
839 12345
840 );
841 }
842
843 let db = Builder::new_local(db_path).build().await?;
844 let conn = db.connect()?;
845 assert_eq!(
846 query_scalar_i64(&conn, "PRAGMA user_version;").await?,
847 12345,
848 "user_version = 12345 must survive close/reopen"
849 );
850
851 Ok(())
852 }
853
854 #[tokio::test]
858 async fn test_application_id_negative_durable_reopen() -> Result<()> {
859 let temp = TempDbPath::new("app_id_negative_reopen");
860 let db_path = temp.as_str();
861
862 {
863 let db = Builder::new_local(db_path).build().await?;
864 let conn = db.connect()?;
865 conn.execute("PRAGMA application_id = -1;", ()).await?;
866 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, -1);
867 }
868
869 let db = Builder::new_local(db_path).build().await?;
870 let conn = db.connect()?;
871 assert_eq!(
872 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
873 -1,
874 "application_id = -1 must survive close/reopen as the signed value -1"
875 );
876
877 let conn = db.connect()?;
880 let _ = conn.execute("PRAGMA wal_checkpoint;", ()).await;
881 drop(conn);
882 drop(db);
883 let bytes = std::fs::read(db_path).expect("read database file");
884 assert!(bytes.len() >= 72, "database file shorter than the header");
885 assert_eq!(
886 &bytes[68..72],
887 &0xFFFF_FFFFu32.to_be_bytes(),
888 "application_id = -1 must be encoded as 0xFFFFFFFF at offset 68"
889 );
890
891 Ok(())
892 }
893
894 #[tokio::test]
899 async fn test_application_id_byte_level_on_disk() -> Result<()> {
900 const GPKG_MAGIC: u32 = 1196444487; const USER_VERSION: i32 = 10201; let temp = TempDbPath::new("app_id_bytes");
904 let db_path = temp.as_str();
905
906 {
907 let db = Builder::new_local(db_path).build().await?;
908 let conn = db.connect()?;
909 conn.execute("CREATE TABLE gpkg_contents (id INTEGER PRIMARY KEY);", ())
911 .await?;
912 conn.execute(&format!("PRAGMA application_id = {GPKG_MAGIC};"), ())
913 .await?;
914 conn.execute(&format!("PRAGMA user_version = {USER_VERSION};"), ())
915 .await?;
916 let _ = conn.execute("PRAGMA wal_checkpoint;", ()).await;
921 }
922
923 let bytes = std::fs::read(db_path).expect("read database file");
924 assert!(
925 bytes.len() >= 72,
926 "database file is shorter than the 100-byte header"
927 );
928
929 assert_eq!(
931 &bytes[68..72],
932 &GPKG_MAGIC.to_be_bytes(),
933 "GPKG magic must be stored big-endian at file offset 68"
934 );
935 assert_eq!(
936 u32::from_be_bytes([bytes[68], bytes[69], bytes[70], bytes[71]]),
937 0x4750_4B47,
938 "application_id bytes must decode to 0x47504B47"
939 );
940
941 assert_eq!(
943 &bytes[60..64],
944 &USER_VERSION.to_be_bytes(),
945 "user_version must be stored big-endian at file offset 60"
946 );
947
948 let db = Builder::new_local(db_path).build().await?;
950 let conn = db.connect()?;
951 assert_eq!(
952 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
953 GPKG_MAGIC as i64
954 );
955 assert_eq!(
956 query_scalar_i64(&conn, "PRAGMA user_version;").await?,
957 USER_VERSION as i64
958 );
959
960 Ok(())
961 }
962
963 #[tokio::test]
968 async fn test_insert_or_ignore_rowid_conflict_skipped() -> Result<()> {
969 let db = Builder::new_local(":memory:").build().await?;
970 let conn = db.connect()?;
971 conn.execute(
972 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
973 (),
974 )
975 .await?;
976 conn.execute("INSERT INTO t (id, name) VALUES (1, 'Alice');", ())
977 .await?;
978
979 conn.execute("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'Bob');", ())
981 .await?;
982
983 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
985 let mut rows = conn.query("SELECT name FROM t WHERE id = 1;", ()).await?;
986 let row = rows.next().await?.expect("row");
987 assert_eq!(row.get_value(0)?, Value::Text("Alice".to_string()));
988
989 Ok(())
990 }
991
992 #[tokio::test]
993 async fn test_insert_or_ignore_multi_row_other_rows_land() -> Result<()> {
994 let db = Builder::new_local(":memory:").build().await?;
995 let conn = db.connect()?;
996 conn.execute(
997 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
998 (),
999 )
1000 .await?;
1001 conn.execute("INSERT INTO t (id, name) VALUES (2, 'Two');", ())
1002 .await?;
1003
1004 conn.execute(
1007 "INSERT OR IGNORE INTO t (id, name) VALUES (1, 'One'), (2, 'Dup'), (3, 'Three');",
1008 (),
1009 )
1010 .await?;
1011
1012 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 3);
1013 let mut rows = conn.query("SELECT name FROM t WHERE id = 2;", ()).await?;
1015 assert_eq!(
1016 rows.next().await?.expect("row").get_value(0)?,
1017 Value::Text("Two".to_string())
1018 );
1019 let mut rows = conn.query("SELECT id FROM t ORDER BY id;", ()).await?;
1021 assert_eq!(
1022 rows.next().await?.expect("row").get_value(0)?,
1023 Value::Integer(1)
1024 );
1025 assert_eq!(
1026 rows.next().await?.expect("row").get_value(0)?,
1027 Value::Integer(2)
1028 );
1029 assert_eq!(
1030 rows.next().await?.expect("row").get_value(0)?,
1031 Value::Integer(3)
1032 );
1033
1034 Ok(())
1035 }
1036
1037 #[cfg(feature = "index_experimental")]
1038 #[tokio::test]
1039 async fn test_insert_or_ignore_unique_index_conflict_skipped() -> Result<()> {
1040 let db = Builder::new_local(":memory:").build().await?;
1041 let conn = db.connect()?;
1042 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT);", ())
1043 .await?;
1044 conn.execute("CREATE UNIQUE INDEX idx_email ON t (email);", ())
1045 .await?;
1046 conn.execute("INSERT INTO t (id, email) VALUES (1, 'a@example.com');", ())
1047 .await?;
1048
1049 conn.execute(
1052 "INSERT OR IGNORE INTO t (id, email) VALUES (2, 'a@example.com');",
1053 (),
1054 )
1055 .await?;
1056
1057 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1058 let mut rows = conn.query("SELECT id FROM t ORDER BY id;", ()).await?;
1060 assert_eq!(
1061 rows.next().await?.expect("row").get_value(0)?,
1062 Value::Integer(1)
1063 );
1064 assert!(rows.next().await?.is_none());
1065
1066 Ok(())
1067 }
1068
1069 #[tokio::test]
1074 async fn test_insert_or_replace_rowid_conflict_replaces() -> Result<()> {
1075 let db = Builder::new_local(":memory:").build().await?;
1076 let conn = db.connect()?;
1077 conn.execute(
1078 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1079 (),
1080 )
1081 .await?;
1082 conn.execute("INSERT INTO t (id, name) VALUES (1, 'Alice');", ())
1083 .await?;
1084
1085 conn.execute("INSERT OR REPLACE INTO t (id, name) VALUES (1, 'Bob');", ())
1087 .await?;
1088
1089 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1090 let mut rows = conn.query("SELECT name FROM t WHERE id = 1;", ()).await?;
1091 assert_eq!(
1092 rows.next().await?.expect("row").get_value(0)?,
1093 Value::Text("Bob".to_string())
1094 );
1095
1096 Ok(())
1097 }
1098
1099 #[tokio::test]
1100 async fn test_insert_or_replace_multi_row_conflict_with_prior_row() -> Result<()> {
1101 let db = Builder::new_local(":memory:").build().await?;
1102 let conn = db.connect()?;
1103 conn.execute(
1104 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1105 (),
1106 )
1107 .await?;
1108
1109 conn.execute(
1112 "INSERT OR REPLACE INTO t (id, name) VALUES (1, 'First'), (1, 'Second'), (2, 'Other');",
1113 (),
1114 )
1115 .await?;
1116
1117 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 2);
1118 let mut rows = conn.query("SELECT name FROM t WHERE id = 1;", ()).await?;
1119 assert_eq!(
1120 rows.next().await?.expect("row").get_value(0)?,
1121 Value::Text("Second".to_string())
1122 );
1123
1124 Ok(())
1125 }
1126
1127 #[cfg(feature = "index_experimental")]
1128 #[tokio::test]
1129 async fn test_insert_or_replace_single_unique_index_conflict() -> Result<()> {
1130 let db = Builder::new_local(":memory:").build().await?;
1131 let conn = db.connect()?;
1132 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT);", ())
1133 .await?;
1134 conn.execute("CREATE UNIQUE INDEX idx_email ON t (email);", ())
1135 .await?;
1136 conn.execute("INSERT INTO t (id, email) VALUES (1, 'a@example.com');", ())
1137 .await?;
1138
1139 conn.execute(
1142 "INSERT OR REPLACE INTO t (id, email) VALUES (2, 'a@example.com');",
1143 (),
1144 )
1145 .await?;
1146
1147 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1148 let mut rows = conn
1150 .query("SELECT id FROM t WHERE email = 'a@example.com';", ())
1151 .await?;
1152 assert_eq!(
1153 rows.next().await?.expect("row").get_value(0)?,
1154 Value::Integer(2)
1155 );
1156 assert!(rows.next().await?.is_none());
1157
1158 Ok(())
1159 }
1160
1161 #[cfg(feature = "index_experimental")]
1162 #[tokio::test]
1163 async fn test_insert_or_replace_multiple_unique_indexes_different_victims() -> Result<()> {
1164 let db = Builder::new_local(":memory:").build().await?;
1168 let conn = db.connect()?;
1169 conn.execute(
1170 "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT);",
1171 (),
1172 )
1173 .await?;
1174 conn.execute("CREATE UNIQUE INDEX idx_a ON t (a);", ())
1175 .await?;
1176 conn.execute("CREATE UNIQUE INDEX idx_b ON t (b);", ())
1177 .await?;
1178
1179 conn.execute("INSERT INTO t (id, a, b) VALUES (1, 'A1', 'B1');", ())
1182 .await?;
1183 conn.execute("INSERT INTO t (id, a, b) VALUES (2, 'A2', 'B2');", ())
1184 .await?;
1185
1186 conn.execute(
1187 "INSERT OR REPLACE INTO t (id, a, b) VALUES (3, 'A1', 'B2');",
1188 (),
1189 )
1190 .await?;
1191
1192 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1194 let mut rows = conn.query("SELECT id, a, b FROM t;", ()).await?;
1195 let row = rows.next().await?.expect("row");
1196 assert_eq!(row.get_value(0)?, Value::Integer(3));
1197 assert_eq!(row.get_value(1)?, Value::Text("A1".to_string()));
1198 assert_eq!(row.get_value(2)?, Value::Text("B2".to_string()));
1199 assert!(rows.next().await?.is_none());
1200
1201 assert_eq!(
1203 query_scalar_i64(&conn, "SELECT id FROM t WHERE a = 'A1';").await?,
1204 3
1205 );
1206 assert_eq!(
1207 query_scalar_i64(&conn, "SELECT id FROM t WHERE b = 'B2';").await?,
1208 3
1209 );
1210
1211 Ok(())
1212 }
1213
1214 #[tokio::test]
1219 async fn test_plain_insert_rowid_conflict_still_errors() -> Result<()> {
1220 let db = Builder::new_local(":memory:").build().await?;
1221 let conn = db.connect()?;
1222 conn.execute(
1223 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1224 (),
1225 )
1226 .await?;
1227 conn.execute("INSERT INTO t (id, name) VALUES (1, 'Alice');", ())
1228 .await?;
1229
1230 let result = conn
1232 .execute("INSERT INTO t (id, name) VALUES (1, 'Bob');", ())
1233 .await;
1234 assert!(
1235 result.is_err(),
1236 "plain INSERT on duplicate PRIMARY KEY must error, got Ok"
1237 );
1238
1239 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1241
1242 Ok(())
1243 }
1244
1245 #[cfg(feature = "index_experimental")]
1268 async fn query_row_count(conn: &Connection, sql: &str) -> Result<i64> {
1269 let mut rows = conn.query(sql, ()).await?;
1270 let mut n = 0i64;
1271 while rows.next().await?.is_some() {
1272 n += 1;
1273 }
1274 Ok(n)
1275 }
1276
1277 #[cfg(feature = "index_experimental")]
1279 async fn create_messages_schema(conn: &Connection) -> Result<()> {
1280 conn.execute(
1281 "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, timestamp INTEGER NOT NULL, data BLOB NOT NULL);",
1282 (),
1283 )
1284 .await?;
1285 conn.execute(
1286 "CREATE INDEX IF NOT EXISTS idx_messages_topic ON messages (topic);",
1287 (),
1288 )
1289 .await?;
1290 conn.execute(
1291 "CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages (timestamp);",
1292 (),
1293 )
1294 .await?;
1295 Ok(())
1296 }
1297
1298 #[cfg(feature = "index_experimental")]
1301 async fn insert_message(
1302 conn: &Connection,
1303 topic: &str,
1304 timestamp: i64,
1305 data: Vec<u8>,
1306 ) -> Result<()> {
1307 conn.execute(
1308 "INSERT INTO messages (topic, timestamp, data) VALUES (?, ?, ?);",
1309 params::Params::Positional(vec![
1310 Value::Text(topic.to_string()),
1311 Value::Integer(timestamp),
1312 Value::Blob(data),
1313 ]),
1314 )
1315 .await?;
1316 Ok(())
1317 }
1318
1319 #[cfg(feature = "index_experimental")]
1326 #[tokio::test]
1327 async fn test_orphaned_wal_does_not_duplicate_rows_two_indexes() -> Result<()> {
1328 let dir = std::env::temp_dir().join(format!(
1329 "oxisqlite_orphan_wal_{}_{:?}",
1330 std::process::id(),
1331 std::thread::current().id()
1332 ));
1333 let _ = std::fs::remove_dir_all(&dir);
1334 std::fs::create_dir_all(&dir).expect("create temp dir");
1335 let db_path = dir.join("messages.db3");
1336 let p = db_path.to_str().expect("utf-8 path").to_string();
1337
1338 async fn cycle(p: &str) -> Result<(i64, i64)> {
1341 let _ = std::fs::remove_file(p);
1343 {
1344 let db = Builder::new_local(p).build().await?;
1345 let conn = db.connect()?;
1346 create_messages_schema(&conn).await?;
1347 insert_message(&conn, "/imu", 1_000_000_000, vec![0xDE, 0xAD]).await?;
1348 insert_message(&conn, "/gps", 2_000_000_000, vec![0xBE, 0xEF]).await?;
1349 }
1350 let db = Builder::new_local(p).build().await?;
1351 let conn = db.connect()?;
1352 create_messages_schema(&conn).await?; let scan =
1354 query_row_count(&conn, "SELECT timestamp, topic, data FROM messages;").await?;
1355 let ordered = query_row_count(
1356 &conn,
1357 "SELECT timestamp, topic, data FROM messages ORDER BY timestamp;",
1358 )
1359 .await?;
1360 Ok((scan, ordered))
1361 }
1362
1363 let (scan1, ord1) = cycle(&p).await?;
1365 assert_eq!(scan1, 2, "cycle 1 table scan");
1366 assert_eq!(ord1, 2, "cycle 1 ORDER BY timestamp (index scan)");
1367
1368 for c in 2..=3 {
1371 let (scan, ord) = cycle(&p).await?;
1372 assert_eq!(scan, 2, "cycle {c} table scan must stay 2");
1373 assert_eq!(ord, 2, "cycle {c} ORDER BY timestamp must stay 2");
1374 }
1375
1376 let _ = std::fs::remove_dir_all(&dir);
1377 Ok(())
1378 }
1379
1380 #[cfg(feature = "index_experimental")]
1384 #[tokio::test]
1385 async fn test_two_non_unique_indexes_roundtrip_values() -> Result<()> {
1386 let db = Builder::new_local(":memory:").build().await?;
1387 let conn = db.connect()?;
1388 create_messages_schema(&conn).await?;
1389 insert_message(&conn, "/imu", 1_000_000_000, vec![0xDE, 0xAD]).await?;
1390 insert_message(&conn, "/gps", 2_000_000_000, vec![0xBE, 0xEF]).await?;
1391
1392 assert_eq!(
1393 query_scalar_i64(&conn, "SELECT count(*) FROM messages;").await?,
1394 2
1395 );
1396
1397 let mut rows = conn
1398 .query(
1399 "SELECT timestamp, topic, data FROM messages ORDER BY timestamp;",
1400 (),
1401 )
1402 .await?;
1403 let r0 = rows.next().await?.expect("row 0");
1404 assert_eq!(r0.get_value(0)?, Value::Integer(1_000_000_000));
1405 assert_eq!(r0.get_value(1)?, Value::Text("/imu".to_string()));
1406 assert_eq!(r0.get_value(2)?, Value::Blob(vec![0xDE, 0xAD]));
1407 let r1 = rows.next().await?.expect("row 1");
1408 assert_eq!(r1.get_value(0)?, Value::Integer(2_000_000_000));
1409 assert_eq!(r1.get_value(1)?, Value::Text("/gps".to_string()));
1410 assert_eq!(r1.get_value(2)?, Value::Blob(vec![0xBE, 0xEF]));
1411 assert!(rows.next().await?.is_none(), "exactly two rows");
1412
1413 Ok(())
1414 }
1415
1416 #[cfg(feature = "index_experimental")]
1421 #[tokio::test]
1422 async fn test_index_count_matrix_single_inserts() -> Result<()> {
1423 for n_idx in 0..=3usize {
1424 let db = Builder::new_local(":memory:").build().await?;
1425 let conn = db.connect()?;
1426 conn.execute(
1427 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, a INTEGER, b INTEGER, c INTEGER);",
1428 (),
1429 )
1430 .await?;
1431 let cols = ["a", "b", "c"];
1432 for col in cols.iter().take(n_idx) {
1433 conn.execute(&format!("CREATE INDEX idx_{col} ON t ({col});"), ())
1434 .await?;
1435 }
1436 conn.execute("INSERT INTO t (a, b, c) VALUES (1, 1, 1);", ())
1437 .await?;
1438 conn.execute("INSERT INTO t (a, b, c) VALUES (2, 2, 2);", ())
1439 .await?;
1440
1441 assert_eq!(
1442 query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?,
1443 2,
1444 "n_idx={n_idx}: count(*)"
1445 );
1446 assert_eq!(
1447 query_row_count(&conn, "SELECT a FROM t;").await?,
1448 2,
1449 "n_idx={n_idx}: table scan"
1450 );
1451 assert_eq!(
1452 query_row_count(&conn, "SELECT a FROM t ORDER BY a;").await?,
1453 2,
1454 "n_idx={n_idx}: ORDER BY a (index scan)"
1455 );
1456 }
1457 Ok(())
1458 }
1459
1460 #[cfg(feature = "index_experimental")]
1463 #[tokio::test]
1464 async fn test_multi_row_insert_two_indexes() -> Result<()> {
1465 let db = Builder::new_local(":memory:").build().await?;
1466 let conn = db.connect()?;
1467 conn.execute(
1468 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, a INTEGER, b INTEGER);",
1469 (),
1470 )
1471 .await?;
1472 conn.execute("CREATE INDEX idx_a ON t (a);", ()).await?;
1473 conn.execute("CREATE INDEX idx_b ON t (b);", ()).await?;
1474 conn.execute("INSERT INTO t (a, b) VALUES (1, 10), (2, 20), (3, 30);", ())
1475 .await?;
1476
1477 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 3);
1478 assert_eq!(query_row_count(&conn, "SELECT a FROM t;").await?, 3);
1479 assert_eq!(
1480 query_row_count(&conn, "SELECT a FROM t ORDER BY a;").await?,
1481 3
1482 );
1483 assert_eq!(
1484 query_row_count(&conn, "SELECT b FROM t ORDER BY b;").await?,
1485 3
1486 );
1487 Ok(())
1488 }
1489
1490 #[cfg(feature = "index_experimental")]
1494 #[tokio::test]
1495 async fn test_insert_or_ignore_two_non_unique_indexes() -> Result<()> {
1496 let db = Builder::new_local(":memory:").build().await?;
1497 let conn = db.connect()?;
1498 conn.execute(
1499 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, a INTEGER, b INTEGER);",
1500 (),
1501 )
1502 .await?;
1503 conn.execute("CREATE INDEX idx_a ON t (a);", ()).await?;
1504 conn.execute("CREATE INDEX idx_b ON t (b);", ()).await?;
1505
1506 conn.execute("INSERT OR IGNORE INTO t (a, b) VALUES (1, 10);", ())
1509 .await?;
1510 conn.execute("INSERT OR IGNORE INTO t (a, b) VALUES (1, 10);", ())
1511 .await?;
1512
1513 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 2);
1514 assert_eq!(
1515 query_row_count(&conn, "SELECT a FROM t ORDER BY a;").await?,
1516 2
1517 );
1518 Ok(())
1519 }
1520
1521 #[cfg(feature = "index_experimental")]
1526 #[tokio::test]
1527 async fn test_insert_or_replace_unique_plus_non_unique_index() -> Result<()> {
1528 let db = Builder::new_local(":memory:").build().await?;
1529 let conn = db.connect()?;
1530 conn.execute(
1531 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT, tag INTEGER);",
1532 (),
1533 )
1534 .await?;
1535 conn.execute("CREATE UNIQUE INDEX idx_email ON t (email);", ())
1536 .await?;
1537 conn.execute("CREATE INDEX idx_tag ON t (tag);", ()).await?;
1538 conn.execute(
1539 "INSERT INTO t (id, email, tag) VALUES (1, 'a@example.com', 7);",
1540 (),
1541 )
1542 .await?;
1543
1544 conn.execute(
1546 "INSERT OR REPLACE INTO t (id, email, tag) VALUES (2, 'a@example.com', 9);",
1547 (),
1548 )
1549 .await?;
1550
1551 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1552 assert_eq!(
1555 query_row_count(&conn, "SELECT id FROM t ORDER BY tag;").await?,
1556 1
1557 );
1558 assert_eq!(
1559 query_scalar_i64(&conn, "SELECT id FROM t WHERE email = 'a@example.com';").await?,
1560 2
1561 );
1562 assert_eq!(
1563 query_scalar_i64(&conn, "SELECT id FROM t WHERE tag = 9;").await?,
1564 2
1565 );
1566 assert_eq!(
1568 query_row_count(&conn, "SELECT id FROM t WHERE tag = 7;").await?,
1569 0
1570 );
1571 Ok(())
1572 }
1573
1574 #[tokio::test]
1579 async fn test_named_params_colon_prefix_round_trip() -> Result<()> {
1580 let db = Builder::new_local(":memory:").build().await?;
1581 let conn = db.connect()?;
1582 conn.execute(
1583 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1584 (),
1585 )
1586 .await?;
1587
1588 conn.execute(
1590 "INSERT INTO t (id, name) VALUES (:id, :name);",
1591 ((":id", 1i64), (":name", "Alice")),
1592 )
1593 .await?;
1594
1595 let mut rows = conn
1596 .query("SELECT name FROM t WHERE id = :id;", [(":id", 1i64)])
1597 .await?;
1598 let row = rows.next().await?.expect("expected one row");
1599 assert_eq!(row.get_value(0)?, Value::Text("Alice".to_string()));
1600 assert!(rows.next().await?.is_none());
1601
1602 Ok(())
1603 }
1604
1605 #[tokio::test]
1606 async fn test_named_params_at_prefix_round_trip() -> Result<()> {
1607 let db = Builder::new_local(":memory:").build().await?;
1608 let conn = db.connect()?;
1609 conn.execute(
1610 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1611 (),
1612 )
1613 .await?;
1614
1615 conn.execute(
1616 "INSERT INTO t (id, name) VALUES (@id, @name);",
1617 (("@id", 1i64), ("@name", "Bob")),
1618 )
1619 .await?;
1620
1621 let mut rows = conn
1622 .query("SELECT name FROM t WHERE id = @id;", [("@id", 1i64)])
1623 .await?;
1624 let row = rows.next().await?.expect("expected one row");
1625 assert_eq!(row.get_value(0)?, Value::Text("Bob".to_string()));
1626 assert!(rows.next().await?.is_none());
1627
1628 Ok(())
1629 }
1630
1631 #[tokio::test]
1632 async fn test_named_params_dollar_prefix_round_trip() -> Result<()> {
1633 let db = Builder::new_local(":memory:").build().await?;
1634 let conn = db.connect()?;
1635 conn.execute(
1636 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1637 (),
1638 )
1639 .await?;
1640
1641 conn.execute(
1642 "INSERT INTO t (id, name) VALUES ($id, $name);",
1643 (("$id", 1i64), ("$name", "Carol")),
1644 )
1645 .await?;
1646
1647 let mut rows = conn
1648 .query("SELECT name FROM t WHERE id = $id;", [("$id", 1i64)])
1649 .await?;
1650 let row = rows.next().await?.expect("expected one row");
1651 assert_eq!(row.get_value(0)?, Value::Text("Carol".to_string()));
1652 assert!(rows.next().await?.is_none());
1653
1654 Ok(())
1655 }
1656
1657 #[tokio::test]
1658 async fn test_named_params_hash_prefix_round_trip() -> Result<()> {
1659 let db = Builder::new_local(":memory:").build().await?;
1660 let conn = db.connect()?;
1661 conn.execute(
1662 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1663 (),
1664 )
1665 .await?;
1666
1667 conn.execute(
1668 "INSERT INTO t (id, name) VALUES (#id, #name);",
1669 (("#id", 1i64), ("#name", "Dave")),
1670 )
1671 .await?;
1672
1673 let mut rows = conn
1674 .query("SELECT name FROM t WHERE id = #id;", [("#id", 1i64)])
1675 .await?;
1676 let row = rows.next().await?.expect("expected one row");
1677 assert_eq!(row.get_value(0)?, Value::Text("Dave".to_string()));
1678 assert!(rows.next().await?.is_none());
1679
1680 Ok(())
1681 }
1682
1683 #[tokio::test]
1687 async fn test_named_params_repeated_placeholder_shares_value() -> Result<()> {
1688 let db = Builder::new_local(":memory:").build().await?;
1689 let conn = db.connect()?;
1690
1691 let mut rows = conn
1692 .query("SELECT :x + :x AS doubled;", [(":x", 21i64)])
1693 .await?;
1694 let row = rows.next().await?.expect("expected one row");
1695 assert_eq!(row.get_value(0)?, Value::Integer(42));
1696 assert!(rows.next().await?.is_none());
1697
1698 Ok(())
1699 }
1700
1701 #[tokio::test]
1705 async fn test_named_params_array_literal_multi() -> Result<()> {
1706 let db = Builder::new_local(":memory:").build().await?;
1707 let conn = db.connect()?;
1708 conn.execute("CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER);", ())
1709 .await?;
1710
1711 conn.execute(
1712 "INSERT INTO t (a, b, c) VALUES (:a, :b, :c);",
1713 [(":a", 1i64), (":b", 2i64), (":c", 3i64)],
1714 )
1715 .await?;
1716
1717 let mut rows = conn.query("SELECT a, b, c FROM t;", ()).await?;
1718 let row = rows.next().await?.expect("expected one row");
1719 assert_eq!(row.get_value(0)?, Value::Integer(1));
1720 assert_eq!(row.get_value(1)?, Value::Integer(2));
1721 assert_eq!(row.get_value(2)?, Value::Integer(3));
1722 assert!(rows.next().await?.is_none());
1723
1724 Ok(())
1725 }
1726
1727 #[tokio::test]
1731 async fn test_named_params_owned_string_keys() -> Result<()> {
1732 let db = Builder::new_local(":memory:").build().await?;
1733 let conn = db.connect()?;
1734 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);", ())
1735 .await?;
1736
1737 let key = String::from(":id");
1738 conn.execute("INSERT INTO t (id) VALUES (:id);", vec![(key, 7i64)])
1739 .await?;
1740
1741 assert_eq!(query_scalar_i64(&conn, "SELECT id FROM t;").await?, 7);
1742
1743 Ok(())
1744 }
1745
1746 #[tokio::test]
1750 async fn test_named_param_unknown_name_errors() -> Result<()> {
1751 let db = Builder::new_local(":memory:").build().await?;
1752 let conn = db.connect()?;
1753 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);", ())
1754 .await?;
1755
1756 let result = conn
1757 .execute("INSERT INTO t (id) VALUES (:id);", [(":nope", 1i64)])
1758 .await;
1759 assert!(
1760 result.is_err(),
1761 "binding an unknown named parameter must error, not silently no-op"
1762 );
1763 if let Err(e) = result {
1764 let msg = e.to_string();
1765 assert!(
1766 msg.contains(":nope"),
1767 "error should name the offending parameter, got: {msg}"
1768 );
1769 }
1770
1771 Ok(())
1772 }
1773
1774 #[tokio::test]
1780 async fn test_named_param_unknown_name_errors_on_query() -> Result<()> {
1781 let db = Builder::new_local(":memory:").build().await?;
1782 let conn = db.connect()?;
1783 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);", ())
1784 .await?;
1785 conn.execute("INSERT INTO t (id) VALUES (1);", ()).await?;
1786
1787 let result = conn
1788 .query("SELECT id FROM t WHERE id = :id;", [(":typo", 1i64)])
1789 .await;
1790 assert!(
1791 result.is_err(),
1792 "query()-side unknown named parameter must error, not silently no-op"
1793 );
1794
1795 Ok(())
1796 }
1797}
1798
1799#[cfg(test)]
1800mod open_from_bytes_tests {
1801 use super::*;
1805 use tempfile::NamedTempFile;
1806
1807 async fn build_bytes() -> Result<Vec<u8>> {
1810 let temp = NamedTempFile::new().expect("temp file");
1811 let path = temp.path().to_str().expect("utf-8 path").to_string();
1812 {
1813 let db = Builder::new_local(&path).build().await?;
1814 let conn = db.connect()?;
1815 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT);", ())
1816 .await?;
1817 conn.execute("INSERT INTO t (id, v) VALUES (1, 'one'), (2, 'two');", ())
1818 .await?;
1819 conn.execute("PRAGMA wal_checkpoint;", ()).await?;
1820 }
1821 Ok(std::fs::read(&path).expect("read db bytes"))
1822 }
1823
1824 async fn count(conn: &Connection) -> Result<i64> {
1825 let mut rows = conn.query("SELECT count(*) FROM t;", ()).await?;
1826 let row = rows.next().await?.expect("count row");
1827 match row.get_value(0)? {
1828 Value::Integer(i) => Ok(i),
1829 other => panic!("expected integer, got {other:?}"),
1830 }
1831 }
1832
1833 #[tokio::test]
1834 async fn test_open_from_bytes_round_trip() -> Result<()> {
1835 let bytes = build_bytes().await?;
1836 let db = Database::open_from_bytes(&bytes)?;
1837 let conn = db.connect()?;
1838 assert_eq!(count(&conn).await?, 2);
1839 Ok(())
1840 }
1841
1842 #[tokio::test]
1849 async fn test_open_from_bytes_shared_across_connections() -> Result<()> {
1850 let bytes = build_bytes().await?;
1851 let db = Database::open_from_bytes(&bytes)?;
1852 let writer = db.connect()?;
1853 let reader = db.connect()?;
1854
1855 assert_eq!(count(&reader).await?, 2);
1857
1858 writer
1859 .execute("INSERT INTO t (id, v) VALUES (3, 'three');", ())
1860 .await?;
1861
1862 assert_eq!(count(&writer).await?, 3);
1864
1865 assert_eq!(
1867 count(&reader).await?,
1868 2,
1869 "an existing read snapshot is isolated from a later commit"
1870 );
1871
1872 let fresh = db.connect()?;
1875 assert_eq!(
1876 count(&fresh).await?,
1877 3,
1878 "a new connection must observe the committed write"
1879 );
1880 Ok(())
1881 }
1882
1883 #[tokio::test]
1884 async fn test_open_from_bytes_invalid_is_err() {
1885 assert!(Database::open_from_bytes(&[]).is_err());
1886 assert!(Database::open_from_bytes(&[0xFFu8; 4096]).is_err());
1887 }
1888}