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::fmt::Debug;
28use std::num::NonZero;
29use std::sync::{Arc, Mutex};
30
31#[derive(Debug, thiserror::Error)]
32pub enum Error {
33 #[error("SQL conversion failure: `{0}`")]
34 ToSqlConversionFailure(BoxError),
35 #[error("Mutex lock error: {0}")]
36 MutexError(String),
37 #[error("SQL execution failure: `{0}`")]
38 SqlExecutionFailure(String),
39 #[error("database schema has changed")]
42 SchemaChanged,
43}
44
45impl Error {
46 pub fn is_schema_changed(&self) -> bool {
50 matches!(self, Error::SchemaChanged)
51 }
52}
53
54impl From<limbo_core::LimboError> for Error {
55 fn from(err: limbo_core::LimboError) -> Self {
56 match err {
57 limbo_core::LimboError::SchemaChanged => Error::SchemaChanged,
58 other => Error::SqlExecutionFailure(other.to_string()),
59 }
60 }
61}
62
63pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
64
65pub type Result<T> = std::result::Result<T, Error>;
66pub struct Builder {
67 path: String,
68}
69
70impl Builder {
71 pub fn new_local(path: &str) -> Self {
72 Self {
73 path: path.to_string(),
74 }
75 }
76
77 #[allow(unused_variables, clippy::arc_with_non_send_sync)]
78 pub async fn build(self) -> Result<Database> {
79 match self.path.as_str() {
80 ":memory:" => {
81 let io: Arc<dyn limbo_core::IO> = Arc::new(limbo_core::MemoryIO::new());
82 let db = limbo_core::Database::open_file(io, self.path.as_str(), false)?;
83 Ok(Database { inner: db })
84 }
85 path => {
86 let io: Arc<dyn limbo_core::IO> = Arc::new(limbo_core::PlatformIO::new()?);
87 let db = limbo_core::Database::open_file(io, path, false)?;
88 Ok(Database { inner: db })
89 }
90 }
91 }
92}
93
94#[derive(Clone)]
95pub struct Database {
96 inner: Arc<limbo_core::Database>,
97}
98
99unsafe impl Send for Database {}
100unsafe impl Sync for Database {}
101
102impl Debug for Database {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("Database").finish()
105 }
106}
107
108impl Database {
109 pub fn connect(&self) -> Result<Connection> {
110 let conn = self.inner.connect()?;
111 #[allow(clippy::arc_with_non_send_sync)]
112 let connection = Connection {
113 inner: Arc::new(Mutex::new(conn)),
114 };
115 Ok(connection)
116 }
117}
118
119pub struct Connection {
120 inner: Arc<Mutex<Arc<limbo_core::Connection>>>,
121}
122
123impl Clone for Connection {
124 fn clone(&self) -> Self {
125 Self {
126 inner: Arc::clone(&self.inner),
127 }
128 }
129}
130
131unsafe impl Send for Connection {}
132unsafe impl Sync for Connection {}
133
134impl Connection {
135 pub async fn query(&self, sql: &str, params: impl IntoParams) -> Result<Rows> {
136 let mut stmt = self.prepare(sql).await?;
137 stmt.query(params).await
138 }
139
140 pub async fn execute(&self, sql: &str, params: impl IntoParams) -> Result<u64> {
141 let mut stmt = self.prepare(sql).await?;
142 stmt.execute(params).await
143 }
144
145 pub async fn prepare(&self, sql: &str) -> Result<Statement> {
146 let conn = self
147 .inner
148 .lock()
149 .map_err(|e| Error::MutexError(e.to_string()))?;
150
151 let stmt = conn.prepare(sql)?;
152
153 #[allow(clippy::arc_with_non_send_sync)]
154 let statement = Statement {
155 inner: Arc::new(Mutex::new(stmt)),
156 };
157 Ok(statement)
158 }
159
160 pub fn changes(&self) -> Result<i64> {
164 let conn = self
165 .inner
166 .lock()
167 .map_err(|e| Error::MutexError(e.to_string()))?;
168 Ok(conn.changes())
169 }
170
171 pub fn pragma_query<F>(&self, pragma_name: &str, mut f: F) -> Result<()>
172 where
173 F: FnMut(&Row) -> limbo_core::Result<()>,
174 {
175 let conn = self
176 .inner
177 .lock()
178 .map_err(|e| Error::MutexError(e.to_string()))?;
179
180 let rows: Vec<Row> = conn
181 .pragma_query(pragma_name)
182 .map_err(|e| Error::SqlExecutionFailure(e.to_string()))?
183 .iter()
184 .map(|row| row.iter().collect::<Row>())
185 .collect();
186
187 rows.iter().try_for_each(|row| {
188 f(row).map_err(|e| {
189 Error::SqlExecutionFailure(format!("Error executing user defined function: {}", e))
190 })
191 })?;
192 Ok(())
193 }
194}
195
196pub struct Statement {
197 inner: Arc<Mutex<limbo_core::Statement>>,
198}
199
200impl Clone for Statement {
201 fn clone(&self) -> Self {
202 Self {
203 inner: Arc::clone(&self.inner),
204 }
205 }
206}
207
208unsafe impl Send for Statement {}
209unsafe impl Sync for Statement {}
210
211impl Statement {
212 pub async fn query(&mut self, params: impl IntoParams) -> Result<Rows> {
213 let params = params.into_params()?;
214 match params {
215 params::Params::None => (),
216 params::Params::Positional(values) => {
217 for (i, value) in values.into_iter().enumerate() {
218 let mut stmt = self
219 .inner
220 .lock()
221 .map_err(|e| Error::MutexError(e.to_string()))?;
222 if let Some(idx) = NonZero::new(i + 1) {
223 stmt.bind_at(idx, value.into());
224 }
225 }
226 }
227 params::Params::Named(_items) => todo!(),
228 }
229 #[allow(clippy::arc_with_non_send_sync)]
230 let rows = Rows {
231 inner: Arc::clone(&self.inner),
232 };
233 Ok(rows)
234 }
235
236 pub async fn execute(&mut self, params: impl IntoParams) -> Result<u64> {
237 {
238 self.inner
240 .lock()
241 .map_err(|e| Error::MutexError(e.to_string()))?
242 .reset();
243 }
244 let params = params.into_params()?;
245 match params {
246 params::Params::None => (),
247 params::Params::Positional(values) => {
248 for (i, value) in values.into_iter().enumerate() {
249 let mut stmt = self
250 .inner
251 .lock()
252 .map_err(|e| Error::MutexError(e.to_string()))?;
253 if let Some(idx) = NonZero::new(i + 1) {
254 stmt.bind_at(idx, value.into());
255 }
256 }
257 }
258 params::Params::Named(_items) => todo!(),
259 }
260 loop {
261 let mut stmt = self
262 .inner
263 .lock()
264 .map_err(|e| Error::MutexError(e.to_string()))?;
265 match stmt.step() {
266 Ok(limbo_core::StepResult::Row) => {
267 return Ok(2);
269 }
270 Ok(limbo_core::StepResult::Done) => {
271 return Ok(0);
272 }
273 Ok(limbo_core::StepResult::IO) => {
274 let _ = stmt.run_once();
275 }
277 Ok(limbo_core::StepResult::Busy) => {
278 return Ok(4);
279 }
280 Ok(limbo_core::StepResult::Interrupt) => {
281 return Ok(3);
282 }
283 Err(err) => {
284 return Err(err.into());
285 }
286 }
287 }
288 }
289
290 pub fn columns(&self) -> Vec<Column> {
291 let Ok(stmt) = self.inner.lock() else {
292 return Vec::new();
293 };
294
295 let n = stmt.num_columns();
296
297 let mut cols = Vec::with_capacity(n);
298
299 for i in 0..n {
300 let name = stmt.get_column_name(i).into_owned();
301 let decl_type = stmt.get_column_decl_type(i).map(|s| s.into_owned());
302 cols.push(Column { name, decl_type });
303 }
304
305 cols
306 }
307}
308
309pub struct Column {
310 name: String,
311 decl_type: Option<String>,
312}
313
314impl Column {
315 pub fn name(&self) -> &str {
316 &self.name
317 }
318
319 pub fn decl_type(&self) -> Option<&str> {
320 self.decl_type.as_deref()
321 }
322}
323
324pub trait IntoValue {
325 fn into_value(self) -> Result<Value>;
326}
327
328#[derive(Debug, Clone)]
329pub enum Params {
330 None,
331 Positional(Vec<Value>),
332 Named(Vec<(String, Value)>),
333}
334pub struct Transaction {}
335
336pub struct Rows {
337 inner: Arc<Mutex<limbo_core::Statement>>,
338}
339
340impl Clone for Rows {
341 fn clone(&self) -> Self {
342 Self {
343 inner: Arc::clone(&self.inner),
344 }
345 }
346}
347
348unsafe impl Send for Rows {}
349unsafe impl Sync for Rows {}
350
351impl Rows {
352 pub async fn next(&mut self) -> Result<Option<Row>> {
353 loop {
354 let mut stmt = self
355 .inner
356 .lock()
357 .map_err(|e| Error::MutexError(e.to_string()))?;
358 match stmt.step() {
359 Ok(limbo_core::StepResult::Row) => {
360 let row = stmt.row().ok_or_else(|| {
361 Error::SqlExecutionFailure(
362 "row unavailable after Row step result".to_string(),
363 )
364 })?;
365 return Ok(Some(Row {
366 values: row.get_values().map(|v| v.to_owned()).collect(),
367 }));
368 }
369 Ok(limbo_core::StepResult::Done) => return Ok(None),
370 Ok(limbo_core::StepResult::IO) => {
371 if let Err(e) = stmt.run_once() {
372 return Err(e.into());
373 }
374 continue;
375 }
376 Ok(limbo_core::StepResult::Busy) => return Ok(None),
377 Ok(limbo_core::StepResult::Interrupt) => return Ok(None),
378 _ => return Ok(None),
379 }
380 }
381 }
382}
383
384#[derive(Debug)]
385pub struct Row {
386 values: Vec<limbo_core::Value>,
387}
388
389unsafe impl Send for Row {}
390unsafe impl Sync for Row {}
391
392impl Row {
393 pub fn get_value(&self, index: usize) -> Result<Value> {
394 let value = &self.values[index];
395 match value {
396 limbo_core::Value::Integer(i) => Ok(Value::Integer(*i)),
397 limbo_core::Value::Null => Ok(Value::Null),
398 limbo_core::Value::Float(f) => Ok(Value::Real(*f)),
399 limbo_core::Value::Text(text) => Ok(Value::Text(text.to_string())),
400 limbo_core::Value::Blob(items) => Ok(Value::Blob(items.to_vec())),
401 }
402 }
403
404 pub fn column_count(&self) -> usize {
405 self.values.len()
406 }
407}
408
409impl<'a> FromIterator<&'a limbo_core::Value> for Row {
410 fn from_iter<T: IntoIterator<Item = &'a limbo_core::Value>>(iter: T) -> Self {
411 let values = iter
412 .into_iter()
413 .map(|v| match v {
414 limbo_core::Value::Integer(i) => limbo_core::Value::Integer(*i),
415 limbo_core::Value::Null => limbo_core::Value::Null,
416 limbo_core::Value::Float(f) => limbo_core::Value::Float(*f),
417 limbo_core::Value::Text(s) => limbo_core::Value::Text(s.clone()),
418 limbo_core::Value::Blob(b) => limbo_core::Value::Blob(b.clone()),
419 })
420 .collect();
421
422 Row { values }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use tempfile::NamedTempFile;
430
431 #[tokio::test]
432 async fn test_database_persistence() -> Result<()> {
433 let temp_file = NamedTempFile::new().unwrap();
434 let db_path = temp_file.path().to_str().unwrap();
435
436 {
438 let db = Builder::new_local(db_path).build().await?;
439 let conn = db.connect()?;
440 conn.execute(
441 "CREATE TABLE test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
442 (),
443 )
444 .await?;
445 conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ())
446 .await?;
447 conn.execute("INSERT INTO test_persistence (name) VALUES ('Bob');", ())
448 .await?;
449 } let db = Builder::new_local(db_path).build().await?;
453 let conn = db.connect()?;
454
455 let mut rows = conn
456 .query("SELECT name FROM test_persistence ORDER BY id;", ())
457 .await?;
458
459 let row1 = rows.next().await?.expect("Expected first row");
460 assert_eq!(row1.get_value(0)?, Value::Text("Alice".to_string()));
461
462 let row2 = rows.next().await?.expect("Expected second row");
463 assert_eq!(row2.get_value(0)?, Value::Text("Bob".to_string()));
464
465 assert!(rows.next().await?.is_none(), "Expected no more rows");
466
467 Ok(())
468 }
469
470 #[tokio::test]
471 async fn test_database_persistence_many_frames() -> Result<()> {
472 let temp_file = NamedTempFile::new().unwrap();
473 let db_path = temp_file.path().to_str().unwrap();
474
475 const NUM_INSERTS: usize = 100;
476 const TARGET_STRING_LEN: usize = 1024; let mut original_data = Vec::with_capacity(NUM_INSERTS);
479 for i in 0..NUM_INSERTS {
480 let prefix = format!("test_string_{:04}_", i);
481 let padding_len = TARGET_STRING_LEN.saturating_sub(prefix.len());
482 let padding: String = "A".repeat(padding_len);
483 original_data.push(format!("{}{}", prefix, padding));
484 }
485
486 {
488 let db = Builder::new_local(db_path).build().await?;
489 let conn = db.connect()?;
490 conn.execute(
491 "CREATE TABLE test_large_persistence (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL);",
492 (),
493 )
494 .await?;
495
496 for data_val in &original_data {
497 conn.execute(
498 "INSERT INTO test_large_persistence (data) VALUES (?);",
499 params::Params::Positional(vec![Value::Text(data_val.clone())]),
500 )
501 .await?;
502 }
503 } let db = Builder::new_local(db_path).build().await?;
507 let conn = db.connect()?;
508
509 let mut rows = conn
510 .query("SELECT data FROM test_large_persistence ORDER BY id;", ())
511 .await?;
512
513 for (i, expected) in original_data.iter().enumerate().take(NUM_INSERTS) {
514 let row = rows
515 .next()
516 .await?
517 .unwrap_or_else(|| panic!("Expected row {} but found None", i));
518 assert_eq!(
519 row.get_value(0)?,
520 Value::Text(expected.clone()),
521 "Mismatch in retrieved data for row {}",
522 i
523 );
524 }
525
526 assert!(
527 rows.next().await?.is_none(),
528 "Expected no more rows after retrieving all inserted data"
529 );
530
531 let wal_path = format!("{}-wal", db_path);
533 std::fs::remove_file(&wal_path)
534 .map_err(|e| eprintln!("Warning: Failed to delete WAL file for test: {}", e))
535 .unwrap();
536
537 let db_after_wal_delete = Builder::new_local(db_path).build().await?;
543 let conn_after_wal_delete = db_after_wal_delete.connect()?;
544
545 let mut rows_after_wal_delete = conn_after_wal_delete
546 .query("SELECT data FROM test_large_persistence ORDER BY id;", ())
547 .await?;
548
549 for (i, expected) in original_data.iter().enumerate().take(NUM_INSERTS) {
550 let row = rows_after_wal_delete.next().await?.unwrap_or_else(|| {
551 panic!(
552 "Expected row {} after WAL deletion but found None; \
553 checkpoint-on-close should have persisted it into the main DB",
554 i
555 )
556 });
557 assert_eq!(
558 row.get_value(0)?,
559 Value::Text(expected.clone()),
560 "Mismatch in retrieved data for row {} after WAL deletion",
561 i
562 );
563 }
564
565 assert!(
566 rows_after_wal_delete.next().await?.is_none(),
567 "Expected no more rows after WAL deletion once all checkpointed data was retrieved"
568 );
569
570 Ok(())
571 }
572
573 #[tokio::test]
574 async fn test_database_persistence_write_one_frame_many_times() -> Result<()> {
575 let temp_file = NamedTempFile::new().unwrap();
576 let db_path = temp_file.path().to_str().unwrap();
577
578 for i in 0..100 {
579 {
580 let db = Builder::new_local(db_path).build().await?;
581 let conn = db.connect()?;
582
583 conn.execute("CREATE TABLE IF NOT EXISTS test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
584 conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ())
585 .await?;
586 }
587 {
588 let db = Builder::new_local(db_path).build().await?;
589 let conn = db.connect()?;
590
591 let mut rows_iter = conn
592 .query("SELECT count(*) FROM test_persistence;", ())
593 .await?;
594 let rows = rows_iter.next().await?.unwrap();
595 assert_eq!(rows.get_value(0)?, Value::Integer(i as i64 + 1));
596 assert!(rows_iter.next().await?.is_none());
597 }
598 }
599
600 Ok(())
601 }
602
603 async fn query_scalar_i64(conn: &Connection, sql: &str) -> Result<i64> {
609 let mut rows = conn.query(sql, ()).await?;
610 let row = rows
611 .next()
612 .await?
613 .unwrap_or_else(|| panic!("expected a row from `{sql}`"));
614 match row.get_value(0)? {
615 Value::Integer(i) => Ok(i),
616 other => panic!("expected Integer from `{sql}`, got {other:?}"),
617 }
618 }
619
620 #[tokio::test]
621 async fn test_application_id_write_read_round_trip() -> Result<()> {
622 let db = Builder::new_local(":memory:").build().await?;
623 let conn = db.connect()?;
624
625 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, 0);
627
628 conn.execute("PRAGMA application_id = 1196444487;", ())
630 .await?;
631 assert_eq!(
632 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
633 1196444487
634 );
635
636 conn.execute("PRAGMA application_id = 42;", ()).await?;
638 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, 42);
639
640 Ok(())
641 }
642
643 #[tokio::test]
644 async fn test_application_id_negative_round_trip() -> Result<()> {
645 let db = Builder::new_local(":memory:").build().await?;
648 let conn = db.connect()?;
649
650 conn.execute("PRAGMA application_id = -1;", ()).await?;
651 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, -1);
652
653 conn.execute("PRAGMA application_id = -2147483648;", ())
654 .await?;
655 assert_eq!(
656 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
657 -2147483648
658 );
659
660 Ok(())
661 }
662
663 struct TempDbPath {
669 path: std::path::PathBuf,
670 }
671
672 impl TempDbPath {
673 fn new(tag: &str) -> Self {
674 use std::sync::atomic::{AtomicU64, Ordering};
675 static COUNTER: AtomicU64 = AtomicU64::new(0);
676 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
677 let mut path = std::env::temp_dir();
678 path.push(format!(
679 "oxisqlite_dur_{}_{}_{}.db",
680 tag,
681 std::process::id(),
682 n
683 ));
684 let _ = std::fs::remove_file(&path);
686 let _ = std::fs::remove_file(format!("{}-wal", path.display()));
687 Self { path }
688 }
689
690 fn as_str(&self) -> &str {
691 self.path
692 .to_str()
693 .expect("temp db path is valid UTF-8 on the test platforms")
694 }
695 }
696
697 impl Drop for TempDbPath {
698 fn drop(&mut self) {
699 let _ = std::fs::remove_file(&self.path);
700 let _ = std::fs::remove_file(format!("{}-wal", self.path.display()));
701 }
702 }
703
704 #[tokio::test]
710 async fn test_application_id_persistence() -> Result<()> {
711 let temp = TempDbPath::new("app_id_persist");
712 let db_path = temp.as_str();
713
714 {
715 let db = Builder::new_local(db_path).build().await?;
716 let conn = db.connect()?;
717 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY);", ())
718 .await?;
719 conn.execute("PRAGMA application_id = -12345;", ()).await?;
720
721 assert_eq!(
723 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
724 -12345
725 );
726 } let db = Builder::new_local(db_path).build().await?;
730 let conn = db.connect()?;
731 assert_eq!(
732 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
733 -12345,
734 "application_id must survive close/reopen"
735 );
736
737 Ok(())
738 }
739
740 #[tokio::test]
743 async fn test_application_id_durable_reopen() -> Result<()> {
744 let temp = TempDbPath::new("app_id_reopen");
745 let db_path = temp.as_str();
746
747 {
748 let db = Builder::new_local(db_path).build().await?;
749 let conn = db.connect()?;
750 conn.execute("PRAGMA application_id = 12345;", ()).await?;
751 assert_eq!(
752 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
753 12345
754 );
755 }
756
757 let db = Builder::new_local(db_path).build().await?;
758 let conn = db.connect()?;
759 assert_eq!(
760 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
761 12345,
762 "application_id = 12345 must survive close/reopen"
763 );
764
765 Ok(())
766 }
767
768 #[tokio::test]
771 async fn test_user_version_durable_reopen() -> Result<()> {
772 let temp = TempDbPath::new("user_version_reopen");
773 let db_path = temp.as_str();
774
775 {
776 let db = Builder::new_local(db_path).build().await?;
777 let conn = db.connect()?;
778 conn.execute("PRAGMA user_version = 12345;", ()).await?;
779 assert_eq!(
780 query_scalar_i64(&conn, "PRAGMA user_version;").await?,
781 12345
782 );
783 }
784
785 let db = Builder::new_local(db_path).build().await?;
786 let conn = db.connect()?;
787 assert_eq!(
788 query_scalar_i64(&conn, "PRAGMA user_version;").await?,
789 12345,
790 "user_version = 12345 must survive close/reopen"
791 );
792
793 Ok(())
794 }
795
796 #[tokio::test]
800 async fn test_application_id_negative_durable_reopen() -> Result<()> {
801 let temp = TempDbPath::new("app_id_negative_reopen");
802 let db_path = temp.as_str();
803
804 {
805 let db = Builder::new_local(db_path).build().await?;
806 let conn = db.connect()?;
807 conn.execute("PRAGMA application_id = -1;", ()).await?;
808 assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, -1);
809 }
810
811 let db = Builder::new_local(db_path).build().await?;
812 let conn = db.connect()?;
813 assert_eq!(
814 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
815 -1,
816 "application_id = -1 must survive close/reopen as the signed value -1"
817 );
818
819 let conn = db.connect()?;
822 let _ = conn.execute("PRAGMA wal_checkpoint;", ()).await;
823 drop(conn);
824 drop(db);
825 let bytes = std::fs::read(db_path).expect("read database file");
826 assert!(bytes.len() >= 72, "database file shorter than the header");
827 assert_eq!(
828 &bytes[68..72],
829 &0xFFFF_FFFFu32.to_be_bytes(),
830 "application_id = -1 must be encoded as 0xFFFFFFFF at offset 68"
831 );
832
833 Ok(())
834 }
835
836 #[tokio::test]
841 async fn test_application_id_byte_level_on_disk() -> Result<()> {
842 const GPKG_MAGIC: u32 = 1196444487; const USER_VERSION: i32 = 10201; let temp = TempDbPath::new("app_id_bytes");
846 let db_path = temp.as_str();
847
848 {
849 let db = Builder::new_local(db_path).build().await?;
850 let conn = db.connect()?;
851 conn.execute("CREATE TABLE gpkg_contents (id INTEGER PRIMARY KEY);", ())
853 .await?;
854 conn.execute(&format!("PRAGMA application_id = {GPKG_MAGIC};"), ())
855 .await?;
856 conn.execute(&format!("PRAGMA user_version = {USER_VERSION};"), ())
857 .await?;
858 let _ = conn.execute("PRAGMA wal_checkpoint;", ()).await;
863 }
864
865 let bytes = std::fs::read(db_path).expect("read database file");
866 assert!(
867 bytes.len() >= 72,
868 "database file is shorter than the 100-byte header"
869 );
870
871 assert_eq!(
873 &bytes[68..72],
874 &GPKG_MAGIC.to_be_bytes(),
875 "GPKG magic must be stored big-endian at file offset 68"
876 );
877 assert_eq!(
878 u32::from_be_bytes([bytes[68], bytes[69], bytes[70], bytes[71]]),
879 0x4750_4B47,
880 "application_id bytes must decode to 0x47504B47"
881 );
882
883 assert_eq!(
885 &bytes[60..64],
886 &USER_VERSION.to_be_bytes(),
887 "user_version must be stored big-endian at file offset 60"
888 );
889
890 let db = Builder::new_local(db_path).build().await?;
892 let conn = db.connect()?;
893 assert_eq!(
894 query_scalar_i64(&conn, "PRAGMA application_id;").await?,
895 GPKG_MAGIC as i64
896 );
897 assert_eq!(
898 query_scalar_i64(&conn, "PRAGMA user_version;").await?,
899 USER_VERSION as i64
900 );
901
902 Ok(())
903 }
904
905 #[tokio::test]
910 async fn test_insert_or_ignore_rowid_conflict_skipped() -> Result<()> {
911 let db = Builder::new_local(":memory:").build().await?;
912 let conn = db.connect()?;
913 conn.execute(
914 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
915 (),
916 )
917 .await?;
918 conn.execute("INSERT INTO t (id, name) VALUES (1, 'Alice');", ())
919 .await?;
920
921 conn.execute("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'Bob');", ())
923 .await?;
924
925 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
927 let mut rows = conn.query("SELECT name FROM t WHERE id = 1;", ()).await?;
928 let row = rows.next().await?.expect("row");
929 assert_eq!(row.get_value(0)?, Value::Text("Alice".to_string()));
930
931 Ok(())
932 }
933
934 #[tokio::test]
935 async fn test_insert_or_ignore_multi_row_other_rows_land() -> Result<()> {
936 let db = Builder::new_local(":memory:").build().await?;
937 let conn = db.connect()?;
938 conn.execute(
939 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
940 (),
941 )
942 .await?;
943 conn.execute("INSERT INTO t (id, name) VALUES (2, 'Two');", ())
944 .await?;
945
946 conn.execute(
949 "INSERT OR IGNORE INTO t (id, name) VALUES (1, 'One'), (2, 'Dup'), (3, 'Three');",
950 (),
951 )
952 .await?;
953
954 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 3);
955 let mut rows = conn.query("SELECT name FROM t WHERE id = 2;", ()).await?;
957 assert_eq!(
958 rows.next().await?.expect("row").get_value(0)?,
959 Value::Text("Two".to_string())
960 );
961 let mut rows = conn.query("SELECT id FROM t ORDER BY id;", ()).await?;
963 assert_eq!(
964 rows.next().await?.expect("row").get_value(0)?,
965 Value::Integer(1)
966 );
967 assert_eq!(
968 rows.next().await?.expect("row").get_value(0)?,
969 Value::Integer(2)
970 );
971 assert_eq!(
972 rows.next().await?.expect("row").get_value(0)?,
973 Value::Integer(3)
974 );
975
976 Ok(())
977 }
978
979 #[cfg(feature = "index_experimental")]
980 #[tokio::test]
981 async fn test_insert_or_ignore_unique_index_conflict_skipped() -> Result<()> {
982 let db = Builder::new_local(":memory:").build().await?;
983 let conn = db.connect()?;
984 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT);", ())
985 .await?;
986 conn.execute("CREATE UNIQUE INDEX idx_email ON t (email);", ())
987 .await?;
988 conn.execute("INSERT INTO t (id, email) VALUES (1, 'a@example.com');", ())
989 .await?;
990
991 conn.execute(
994 "INSERT OR IGNORE INTO t (id, email) VALUES (2, 'a@example.com');",
995 (),
996 )
997 .await?;
998
999 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1000 let mut rows = conn.query("SELECT id FROM t ORDER BY id;", ()).await?;
1002 assert_eq!(
1003 rows.next().await?.expect("row").get_value(0)?,
1004 Value::Integer(1)
1005 );
1006 assert!(rows.next().await?.is_none());
1007
1008 Ok(())
1009 }
1010
1011 #[tokio::test]
1016 async fn test_insert_or_replace_rowid_conflict_replaces() -> Result<()> {
1017 let db = Builder::new_local(":memory:").build().await?;
1018 let conn = db.connect()?;
1019 conn.execute(
1020 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1021 (),
1022 )
1023 .await?;
1024 conn.execute("INSERT INTO t (id, name) VALUES (1, 'Alice');", ())
1025 .await?;
1026
1027 conn.execute("INSERT OR REPLACE INTO t (id, name) VALUES (1, 'Bob');", ())
1029 .await?;
1030
1031 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1032 let mut rows = conn.query("SELECT name FROM t WHERE id = 1;", ()).await?;
1033 assert_eq!(
1034 rows.next().await?.expect("row").get_value(0)?,
1035 Value::Text("Bob".to_string())
1036 );
1037
1038 Ok(())
1039 }
1040
1041 #[tokio::test]
1042 async fn test_insert_or_replace_multi_row_conflict_with_prior_row() -> Result<()> {
1043 let db = Builder::new_local(":memory:").build().await?;
1044 let conn = db.connect()?;
1045 conn.execute(
1046 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1047 (),
1048 )
1049 .await?;
1050
1051 conn.execute(
1054 "INSERT OR REPLACE INTO t (id, name) VALUES (1, 'First'), (1, 'Second'), (2, 'Other');",
1055 (),
1056 )
1057 .await?;
1058
1059 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 2);
1060 let mut rows = conn.query("SELECT name FROM t WHERE id = 1;", ()).await?;
1061 assert_eq!(
1062 rows.next().await?.expect("row").get_value(0)?,
1063 Value::Text("Second".to_string())
1064 );
1065
1066 Ok(())
1067 }
1068
1069 #[cfg(feature = "index_experimental")]
1070 #[tokio::test]
1071 async fn test_insert_or_replace_single_unique_index_conflict() -> Result<()> {
1072 let db = Builder::new_local(":memory:").build().await?;
1073 let conn = db.connect()?;
1074 conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT);", ())
1075 .await?;
1076 conn.execute("CREATE UNIQUE INDEX idx_email ON t (email);", ())
1077 .await?;
1078 conn.execute("INSERT INTO t (id, email) VALUES (1, 'a@example.com');", ())
1079 .await?;
1080
1081 conn.execute(
1084 "INSERT OR REPLACE INTO t (id, email) VALUES (2, 'a@example.com');",
1085 (),
1086 )
1087 .await?;
1088
1089 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1090 let mut rows = conn
1092 .query("SELECT id FROM t WHERE email = 'a@example.com';", ())
1093 .await?;
1094 assert_eq!(
1095 rows.next().await?.expect("row").get_value(0)?,
1096 Value::Integer(2)
1097 );
1098 assert!(rows.next().await?.is_none());
1099
1100 Ok(())
1101 }
1102
1103 #[cfg(feature = "index_experimental")]
1104 #[tokio::test]
1105 async fn test_insert_or_replace_multiple_unique_indexes_different_victims() -> Result<()> {
1106 let db = Builder::new_local(":memory:").build().await?;
1110 let conn = db.connect()?;
1111 conn.execute(
1112 "CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT);",
1113 (),
1114 )
1115 .await?;
1116 conn.execute("CREATE UNIQUE INDEX idx_a ON t (a);", ())
1117 .await?;
1118 conn.execute("CREATE UNIQUE INDEX idx_b ON t (b);", ())
1119 .await?;
1120
1121 conn.execute("INSERT INTO t (id, a, b) VALUES (1, 'A1', 'B1');", ())
1124 .await?;
1125 conn.execute("INSERT INTO t (id, a, b) VALUES (2, 'A2', 'B2');", ())
1126 .await?;
1127
1128 conn.execute(
1129 "INSERT OR REPLACE INTO t (id, a, b) VALUES (3, 'A1', 'B2');",
1130 (),
1131 )
1132 .await?;
1133
1134 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1136 let mut rows = conn.query("SELECT id, a, b FROM t;", ()).await?;
1137 let row = rows.next().await?.expect("row");
1138 assert_eq!(row.get_value(0)?, Value::Integer(3));
1139 assert_eq!(row.get_value(1)?, Value::Text("A1".to_string()));
1140 assert_eq!(row.get_value(2)?, Value::Text("B2".to_string()));
1141 assert!(rows.next().await?.is_none());
1142
1143 assert_eq!(
1145 query_scalar_i64(&conn, "SELECT id FROM t WHERE a = 'A1';").await?,
1146 3
1147 );
1148 assert_eq!(
1149 query_scalar_i64(&conn, "SELECT id FROM t WHERE b = 'B2';").await?,
1150 3
1151 );
1152
1153 Ok(())
1154 }
1155
1156 #[tokio::test]
1161 async fn test_plain_insert_rowid_conflict_still_errors() -> Result<()> {
1162 let db = Builder::new_local(":memory:").build().await?;
1163 let conn = db.connect()?;
1164 conn.execute(
1165 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
1166 (),
1167 )
1168 .await?;
1169 conn.execute("INSERT INTO t (id, name) VALUES (1, 'Alice');", ())
1170 .await?;
1171
1172 let result = conn
1174 .execute("INSERT INTO t (id, name) VALUES (1, 'Bob');", ())
1175 .await;
1176 assert!(
1177 result.is_err(),
1178 "plain INSERT on duplicate PRIMARY KEY must error, got Ok"
1179 );
1180
1181 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1183
1184 Ok(())
1185 }
1186
1187 #[cfg(feature = "index_experimental")]
1210 async fn query_row_count(conn: &Connection, sql: &str) -> Result<i64> {
1211 let mut rows = conn.query(sql, ()).await?;
1212 let mut n = 0i64;
1213 while rows.next().await?.is_some() {
1214 n += 1;
1215 }
1216 Ok(n)
1217 }
1218
1219 #[cfg(feature = "index_experimental")]
1221 async fn create_messages_schema(conn: &Connection) -> Result<()> {
1222 conn.execute(
1223 "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, timestamp INTEGER NOT NULL, data BLOB NOT NULL);",
1224 (),
1225 )
1226 .await?;
1227 conn.execute(
1228 "CREATE INDEX IF NOT EXISTS idx_messages_topic ON messages (topic);",
1229 (),
1230 )
1231 .await?;
1232 conn.execute(
1233 "CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages (timestamp);",
1234 (),
1235 )
1236 .await?;
1237 Ok(())
1238 }
1239
1240 #[cfg(feature = "index_experimental")]
1243 async fn insert_message(
1244 conn: &Connection,
1245 topic: &str,
1246 timestamp: i64,
1247 data: Vec<u8>,
1248 ) -> Result<()> {
1249 conn.execute(
1250 "INSERT INTO messages (topic, timestamp, data) VALUES (?, ?, ?);",
1251 params::Params::Positional(vec![
1252 Value::Text(topic.to_string()),
1253 Value::Integer(timestamp),
1254 Value::Blob(data),
1255 ]),
1256 )
1257 .await?;
1258 Ok(())
1259 }
1260
1261 #[cfg(feature = "index_experimental")]
1268 #[tokio::test]
1269 async fn test_orphaned_wal_does_not_duplicate_rows_two_indexes() -> Result<()> {
1270 let dir = std::env::temp_dir().join(format!(
1271 "oxisqlite_orphan_wal_{}_{:?}",
1272 std::process::id(),
1273 std::thread::current().id()
1274 ));
1275 let _ = std::fs::remove_dir_all(&dir);
1276 std::fs::create_dir_all(&dir).expect("create temp dir");
1277 let db_path = dir.join("messages.db3");
1278 let p = db_path.to_str().expect("utf-8 path").to_string();
1279
1280 async fn cycle(p: &str) -> Result<(i64, i64)> {
1283 let _ = std::fs::remove_file(p);
1285 {
1286 let db = Builder::new_local(p).build().await?;
1287 let conn = db.connect()?;
1288 create_messages_schema(&conn).await?;
1289 insert_message(&conn, "/imu", 1_000_000_000, vec![0xDE, 0xAD]).await?;
1290 insert_message(&conn, "/gps", 2_000_000_000, vec![0xBE, 0xEF]).await?;
1291 }
1292 let db = Builder::new_local(p).build().await?;
1293 let conn = db.connect()?;
1294 create_messages_schema(&conn).await?; let scan =
1296 query_row_count(&conn, "SELECT timestamp, topic, data FROM messages;").await?;
1297 let ordered = query_row_count(
1298 &conn,
1299 "SELECT timestamp, topic, data FROM messages ORDER BY timestamp;",
1300 )
1301 .await?;
1302 Ok((scan, ordered))
1303 }
1304
1305 let (scan1, ord1) = cycle(&p).await?;
1307 assert_eq!(scan1, 2, "cycle 1 table scan");
1308 assert_eq!(ord1, 2, "cycle 1 ORDER BY timestamp (index scan)");
1309
1310 for c in 2..=3 {
1313 let (scan, ord) = cycle(&p).await?;
1314 assert_eq!(scan, 2, "cycle {c} table scan must stay 2");
1315 assert_eq!(ord, 2, "cycle {c} ORDER BY timestamp must stay 2");
1316 }
1317
1318 let _ = std::fs::remove_dir_all(&dir);
1319 Ok(())
1320 }
1321
1322 #[cfg(feature = "index_experimental")]
1326 #[tokio::test]
1327 async fn test_two_non_unique_indexes_roundtrip_values() -> Result<()> {
1328 let db = Builder::new_local(":memory:").build().await?;
1329 let conn = db.connect()?;
1330 create_messages_schema(&conn).await?;
1331 insert_message(&conn, "/imu", 1_000_000_000, vec![0xDE, 0xAD]).await?;
1332 insert_message(&conn, "/gps", 2_000_000_000, vec![0xBE, 0xEF]).await?;
1333
1334 assert_eq!(
1335 query_scalar_i64(&conn, "SELECT count(*) FROM messages;").await?,
1336 2
1337 );
1338
1339 let mut rows = conn
1340 .query(
1341 "SELECT timestamp, topic, data FROM messages ORDER BY timestamp;",
1342 (),
1343 )
1344 .await?;
1345 let r0 = rows.next().await?.expect("row 0");
1346 assert_eq!(r0.get_value(0)?, Value::Integer(1_000_000_000));
1347 assert_eq!(r0.get_value(1)?, Value::Text("/imu".to_string()));
1348 assert_eq!(r0.get_value(2)?, Value::Blob(vec![0xDE, 0xAD]));
1349 let r1 = rows.next().await?.expect("row 1");
1350 assert_eq!(r1.get_value(0)?, Value::Integer(2_000_000_000));
1351 assert_eq!(r1.get_value(1)?, Value::Text("/gps".to_string()));
1352 assert_eq!(r1.get_value(2)?, Value::Blob(vec![0xBE, 0xEF]));
1353 assert!(rows.next().await?.is_none(), "exactly two rows");
1354
1355 Ok(())
1356 }
1357
1358 #[cfg(feature = "index_experimental")]
1363 #[tokio::test]
1364 async fn test_index_count_matrix_single_inserts() -> Result<()> {
1365 for n_idx in 0..=3usize {
1366 let db = Builder::new_local(":memory:").build().await?;
1367 let conn = db.connect()?;
1368 conn.execute(
1369 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, a INTEGER, b INTEGER, c INTEGER);",
1370 (),
1371 )
1372 .await?;
1373 let cols = ["a", "b", "c"];
1374 for col in cols.iter().take(n_idx) {
1375 conn.execute(&format!("CREATE INDEX idx_{col} ON t ({col});"), ())
1376 .await?;
1377 }
1378 conn.execute("INSERT INTO t (a, b, c) VALUES (1, 1, 1);", ())
1379 .await?;
1380 conn.execute("INSERT INTO t (a, b, c) VALUES (2, 2, 2);", ())
1381 .await?;
1382
1383 assert_eq!(
1384 query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?,
1385 2,
1386 "n_idx={n_idx}: count(*)"
1387 );
1388 assert_eq!(
1389 query_row_count(&conn, "SELECT a FROM t;").await?,
1390 2,
1391 "n_idx={n_idx}: table scan"
1392 );
1393 assert_eq!(
1394 query_row_count(&conn, "SELECT a FROM t ORDER BY a;").await?,
1395 2,
1396 "n_idx={n_idx}: ORDER BY a (index scan)"
1397 );
1398 }
1399 Ok(())
1400 }
1401
1402 #[cfg(feature = "index_experimental")]
1405 #[tokio::test]
1406 async fn test_multi_row_insert_two_indexes() -> Result<()> {
1407 let db = Builder::new_local(":memory:").build().await?;
1408 let conn = db.connect()?;
1409 conn.execute(
1410 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, a INTEGER, b INTEGER);",
1411 (),
1412 )
1413 .await?;
1414 conn.execute("CREATE INDEX idx_a ON t (a);", ()).await?;
1415 conn.execute("CREATE INDEX idx_b ON t (b);", ()).await?;
1416 conn.execute("INSERT INTO t (a, b) VALUES (1, 10), (2, 20), (3, 30);", ())
1417 .await?;
1418
1419 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 3);
1420 assert_eq!(query_row_count(&conn, "SELECT a FROM t;").await?, 3);
1421 assert_eq!(
1422 query_row_count(&conn, "SELECT a FROM t ORDER BY a;").await?,
1423 3
1424 );
1425 assert_eq!(
1426 query_row_count(&conn, "SELECT b FROM t ORDER BY b;").await?,
1427 3
1428 );
1429 Ok(())
1430 }
1431
1432 #[cfg(feature = "index_experimental")]
1436 #[tokio::test]
1437 async fn test_insert_or_ignore_two_non_unique_indexes() -> Result<()> {
1438 let db = Builder::new_local(":memory:").build().await?;
1439 let conn = db.connect()?;
1440 conn.execute(
1441 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, a INTEGER, b INTEGER);",
1442 (),
1443 )
1444 .await?;
1445 conn.execute("CREATE INDEX idx_a ON t (a);", ()).await?;
1446 conn.execute("CREATE INDEX idx_b ON t (b);", ()).await?;
1447
1448 conn.execute("INSERT OR IGNORE INTO t (a, b) VALUES (1, 10);", ())
1451 .await?;
1452 conn.execute("INSERT OR IGNORE INTO t (a, b) VALUES (1, 10);", ())
1453 .await?;
1454
1455 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 2);
1456 assert_eq!(
1457 query_row_count(&conn, "SELECT a FROM t ORDER BY a;").await?,
1458 2
1459 );
1460 Ok(())
1461 }
1462
1463 #[cfg(feature = "index_experimental")]
1468 #[tokio::test]
1469 async fn test_insert_or_replace_unique_plus_non_unique_index() -> Result<()> {
1470 let db = Builder::new_local(":memory:").build().await?;
1471 let conn = db.connect()?;
1472 conn.execute(
1473 "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT, tag INTEGER);",
1474 (),
1475 )
1476 .await?;
1477 conn.execute("CREATE UNIQUE INDEX idx_email ON t (email);", ())
1478 .await?;
1479 conn.execute("CREATE INDEX idx_tag ON t (tag);", ()).await?;
1480 conn.execute(
1481 "INSERT INTO t (id, email, tag) VALUES (1, 'a@example.com', 7);",
1482 (),
1483 )
1484 .await?;
1485
1486 conn.execute(
1488 "INSERT OR REPLACE INTO t (id, email, tag) VALUES (2, 'a@example.com', 9);",
1489 (),
1490 )
1491 .await?;
1492
1493 assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1494 assert_eq!(
1497 query_row_count(&conn, "SELECT id FROM t ORDER BY tag;").await?,
1498 1
1499 );
1500 assert_eq!(
1501 query_scalar_i64(&conn, "SELECT id FROM t WHERE email = 'a@example.com';").await?,
1502 2
1503 );
1504 assert_eq!(
1505 query_scalar_i64(&conn, "SELECT id FROM t WHERE tag = 9;").await?,
1506 2
1507 );
1508 assert_eq!(
1510 query_row_count(&conn, "SELECT id FROM t WHERE tag = 7;").await?,
1511 0
1512 );
1513 Ok(())
1514 }
1515}