Skip to main content

limbo/
lib.rs

1// UPSTREAM: vendored Limbo fork — allow upstream style
2//! Top-level facade of the C-free **oxisqlite** engine, a Pure-Rust fork of
3//! limbo 0.0.22 and entry point for the `oxisql-sqlite-compat` backend.
4//!
5//! Re-exports `Connection`, `Statement`, `params`/`params_from_iter`, and
6//! `Value` as a thin, ergonomic wrapper; bytecode execution, storage, and
7//! SQL processing all live in `oxisqlite-core`.
8#![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    /// The database schema changed after this statement was compiled (SQLITE_SCHEMA).
41    /// Re-prepare the statement and retry.
42    #[error("database schema has changed")]
43    SchemaChanged,
44}
45
46impl Error {
47    /// Returns `true` if this error signals that the database schema changed after
48    /// the statement was compiled.  Callers should re-prepare the statement
49    /// against the refreshed schema and retry.
50    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    /// Open an in-memory database preloaded from an existing SQLite database
120    /// image `bytes` (e.g. the output of `include_bytes!`, `VACUUM INTO`, or
121    /// `sqlite3_serialize()`).
122    ///
123    /// Mirrors rusqlite's `Connection::deserialize` / SQLite's
124    /// `sqlite3_deserialize()`. Unlike [`Builder::build`], this is synchronous
125    /// because no real I/O occurs — the bytes are copied into an in-memory
126    /// page store. The returned [`Database`] can be [`connect`]ed multiple
127    /// times, and all connections share the same preloaded image.
128    ///
129    /// [`connect`]: Database::connect
130    ///
131    /// # Errors
132    ///
133    /// Returns [`Error::SqlExecutionFailure`] if `bytes` is not a valid SQLite
134    /// database image (too short, wrong magic, or an invalid page size in the
135    /// header). Never panics on malformed input.
136    #[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    /// Return the number of rows changed by the most recent DML statement on
185    /// this connection.  Mirrors `sqlite3_changes()` semantics: DDL statements
186    /// and `BEGIN`/`COMMIT`/`ROLLBACK` return 0.
187    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            // Reset the statement before executing
263            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                    // unexpected row during execution, error out.
292                    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                    //return Ok(1);
300                }
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    /// Bind a set of named parameters (`:name`, `@name`, `$name`, or
315    /// `#name` placeholders — prefix character included, exactly as it
316    /// appears in the compiled SQL) against this prepared statement.
317    ///
318    /// Each `name` is resolved to its 1-based bind index through
319    /// `limbo_core::Statement::parameters()` /
320    /// `limbo_core::parameters::Parameters::index`, then bound via the exact
321    /// same `limbo_core::Statement::bind_at` path already used by the
322    /// sibling [`params::Params::Positional`] arm. Shared by
323    /// [`Statement::query`] and [`Statement::execute`], the two call sites
324    /// that accept [`params::Params::Named`].
325    ///
326    /// # Errors
327    ///
328    /// Returns [`Error::SqlExecutionFailure`] if `name` does not match any
329    /// placeholder recorded in the prepared statement (e.g. a typo, or a
330    /// name for a placeholder absent from the compiled SQL) — the bind is
331    /// rejected outright rather than silently skipped.
332    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        // First, create the database, a table, and insert some data
495        {
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        } // db and conn are dropped here, simulating closing
508
509        // Now, re-open the database and check if the data is still there
510        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; // 1KB
535
536        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        // First, create the database, a table, and insert many large strings
545        {
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        } // db and conn are dropped here, simulating closing
562
563        // Now, re-open the database and check if the data is still there
564        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        // Delete the WAL file only and try to re-open and query
590        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        // Re-open the database after deleting the WAL and assert the data is still
596        // fully intact. The clean close above (dropping the connection) triggers a
597        // checkpoint-on-close, which writes all WAL frames into the main `.db` file
598        // and truncates the WAL. As a result the `-wal` file is no longer
599        // load-bearing after a clean close: deleting it must NOT lose any data.
600        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    // ------------------------------------------------------------------
662    // A1: PRAGMA application_id
663    // ------------------------------------------------------------------
664
665    /// Read a single scalar integer value produced by a query (e.g. a PRAGMA).
666    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        // Default is 0.
684        assert_eq!(query_scalar_i64(&conn, "PRAGMA application_id;").await?, 0);
685
686        // GPKG magic (0x47504B47 = 1196444487), a large positive identifier.
687        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        // Overwrite with another value.
695        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        // SQLite presents application_id as a SIGNED 32-bit integer, so -1 must
704        // round-trip as -1 (not 4294967295).
705        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    /// Build a unique, file-backed database path under the OS temp directory.
722    ///
723    /// Uses [`std::env::temp_dir`] plus the process id and an atomically
724    /// incrementing counter so concurrently-running tests never collide, and
725    /// cleans up the database file together with its `-wal` sidecar on drop.
726    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            // Ensure a clean slate even if a previous run left files behind.
743            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    /// `application_id` survives a real close/reopen cycle for a file-backed
763    /// database (regression test for the header-cookie durability bug: the
764    /// in-memory header was previously re-read straight from the main DB file
765    /// at open time, bypassing the WAL, so a cookie that lived only in the WAL
766    /// reset to 0 on reopen).
767    #[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            // Within the same open database the value (and its sign) is retained.
780            assert_eq!(
781                query_scalar_i64(&conn, "PRAGMA application_id;").await?,
782                -12345
783            );
784        } // connection + database dropped here, simulating a close.
785
786        // Reopen and assert the value is durably restored from the WAL.
787        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    /// `application_id` set to a large positive identifier round-trips across a
799    /// close/reopen for a file-backed database.
800    #[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    /// `user_version` (the canonical cookie mirror of `application_id`) survives
827    /// a close/reopen identically.
828    #[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    /// A negative `application_id` (e.g. -1) is stored on disk as 0xFFFFFFFF but
855    /// must read back as the signed value -1 after a durable close/reopen, just
856    /// like SQLite.
857    #[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        // And the on-disk bytes (after a checkpoint flushes the WAL into the
878        // main file) must be the 32-bit two's-complement big-endian 0xFFFFFFFF.
879        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    /// Byte-level GeoPackage check: writing the GPKG magic via
895    /// `PRAGMA application_id = 1196444487` (0x47504B47) and checkpointing must
896    /// land the big-endian magic at file offset 68, and a `user_version` write
897    /// must land at offset 60 — the exact layout GeoPackage requires.
898    #[tokio::test]
899    async fn test_application_id_byte_level_on_disk() -> Result<()> {
900        const GPKG_MAGIC: u32 = 1196444487; // 0x47504B47, "GPKG".
901        const USER_VERSION: i32 = 10201; // arbitrary GeoPackage-style version.
902
903        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            // A table forces real page allocation so the file is a valid db.
910            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            // Checkpoint so the WAL's page-1 frame is copied into the main
917            // database file: in WAL mode the header bytes only reach the main
918            // file after a checkpoint (this is the same requirement SQLite has
919            // for a byte-valid GeoPackage on disk).
920            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        // application_id at offset [68..72], big-endian == 0x47504B47.
930        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        // user_version at offset [60..64], big-endian.
942        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        // The value is also readable through PRAGMA after reopen.
949        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    // ------------------------------------------------------------------
964    // A2: INSERT OR IGNORE
965    // ------------------------------------------------------------------
966
967    #[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        // Conflicting rowid is silently ignored, not an error.
980        conn.execute("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'Bob');", ())
981            .await?;
982
983        // Original row is untouched.
984        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        // Multi-row INSERT OR IGNORE: row id=2 conflicts and is skipped, but ids
1005        // 1 and 3 must still land.
1006        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        // The conflicting row keeps its original value.
1014        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        // The non-conflicting rows are present.
1020        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        // Different rowid but conflicting unique-index value -> skipped, no error
1050        // and crucially no partial index/table state.
1051        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        // Row id=2 must NOT exist.
1059        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    // ------------------------------------------------------------------
1070    // A3: INSERT OR REPLACE
1071    // ------------------------------------------------------------------
1072
1073    #[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        // Same rowid -> old row replaced by new one.
1086        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        // Row N (id=1, 'Second') conflicts with just-inserted row N-1 (id=1,
1110        // 'First') within the same multi-row statement -> the later one wins.
1111        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        // New rowid (2) but conflicting unique-index value -> the victim (id=1)
1140        // is deleted and replaced by the new row (id=2).
1141        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        // Only id=2 remains, and the unique index still resolves it.
1149        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        // SQLite OR REPLACE semantics: a new row that conflicts on MULTIPLE
1165        // unique indexes pointing at DIFFERENT existing rows must delete EVERY
1166        // victim, leaving exactly the new row.
1167        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        // Two distinct existing rows; the new row collides with row 1 on column a
1180        // and with row 2 on column b.
1181        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        // Both victims (id=1 and id=2) are gone; exactly the new row remains.
1193        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        // Indexes resolve only the surviving row.
1202        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    // ------------------------------------------------------------------
1215    // Regression: plain INSERT conflict must still error (no Halt regression).
1216    // ------------------------------------------------------------------
1217
1218    #[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        // A plain INSERT (no OR clause) on a duplicate rowid must still fail.
1231        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        // The original row is intact and no second row was written.
1240        assert_eq!(query_scalar_i64(&conn, "SELECT count(*) FROM t;").await?, 1);
1241
1242        Ok(())
1243    }
1244
1245    // ------------------------------------------------------------------
1246    // Regression: orphaned-WAL row duplication.
1247    //
1248    // A previous session leaves a populated `-wal` behind; the main `.db`
1249    // file is then deleted (or otherwise recreated empty) while the `-wal`
1250    // survives. On reopen the engine must NOT replay that orphaned WAL — doing
1251    // so resurrects the previous session's committed pages on top of the fresh
1252    // database, so every row count grows by the stale content on each reopen.
1253    //
1254    // This mirrors the downstream `oxiaero-ros2` rosbag2 roundtrip failure:
1255    // AUTOINCREMENT PRIMARY KEY + two NON-UNIQUE secondary indexes + a BLOB
1256    // column, two single-row INSERTs, then a `SELECT ... ORDER BY <indexed col>`
1257    // read-back that returned 4 (then 6, 8, ...) rows instead of 2 because the
1258    // index-driven scan walked the resurrected + new index entries.
1259    //
1260    // Index maintenance only runs under `index_experimental` (a plain INSERT
1261    // into an indexed table is rejected without it — exactly the feature the
1262    // `oxisql-sqlite-compat` consumer enables), so these tests are gated on it.
1263    // ------------------------------------------------------------------
1264
1265    /// Count rows returned by `sql` by draining the cursor (works for both
1266    /// table scans and index-driven scans like `ORDER BY <indexed column>`).
1267    #[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    /// Apply the exact consumer schema (idempotent — `IF NOT EXISTS`) to `conn`.
1278    #[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    /// Insert one message row via positional parameters (mirrors the consumer's
1299    /// `INSERT INTO messages (topic, timestamp, data) VALUES (?, ?, ?)`).
1300    #[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    /// The full downstream consumer reproduction: write 2 rows to a file-backed
1320    /// DB, delete ONLY the main `.db` (leaving the populated `-wal`, exactly what
1321    /// the consumer's test harness does between runs), recreate + write 2 rows
1322    /// again, then read back. Must be exactly 2 rows — both via a plain table
1323    /// scan AND via the consumer's `ORDER BY timestamp` (index-driven) read — and
1324    /// must not accumulate across repeated cycles.
1325    #[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        // One write-then-readback cycle that recreates the DB while leaving any
1339        // pre-existing `-wal` in place.
1340        async fn cycle(p: &str) -> Result<(i64, i64)> {
1341            // Recreate the main DB file but keep a stale `-wal` if present.
1342            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?; // consumer re-runs schema on open
1353            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        // First cycle starts clean.
1364        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        // Subsequent cycles each find a populated stale `-wal`; the orphaned WAL
1369        // must be discarded, so counts stay at 2 (pre-fix they were 4, 6, ...).
1370        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    /// Consumer-equivalent roundtrip on a clean DB: AUTOINCREMENT + 2 non-unique
1381    /// indexes + BLOB, 2 writes -> exactly 2 rows, with correct column values via
1382    /// the index-driven `ORDER BY timestamp` read.
1383    #[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    /// Index-count matrix: 0/1/2/3 NON-UNIQUE secondary indexes, two single-row
1417    /// INSERTs each -> exactly 2 rows, verified by BOTH a plain table scan and an
1418    /// index-driven `ORDER BY a` scan (which is what surfaces duplicate index
1419    /// entries).
1420    #[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    /// Multi-row `INSERT ... VALUES (..),(..),(..)` into a table with two
1461    /// non-unique indexes -> exactly 3 rows (table scan and index scan agree).
1462    #[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    /// `INSERT OR IGNORE` into a table with two non-unique indexes: a plain
1491    /// (non-unique) secondary index never causes a conflict, so all rows land
1492    /// exactly once.
1493    #[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        // Duplicate (a,b) values are fine for non-unique indexes; nothing is
1507        // ignored and nothing is duplicated.
1508        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    /// `INSERT OR REPLACE` into a table that has both a UNIQUE index and a
1522    /// secondary NON-UNIQUE index: replacing on the unique-index conflict must
1523    /// delete the victim's entry from EVERY index, leaving exactly one row and
1524    /// no duplicate index entries.
1525    #[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        // New rowid, same unique email -> victim id=1 replaced by id=2.
1545        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        // The non-unique secondary index must resolve only the surviving row
1553        // (no orphaned victim entry left behind).
1554        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        // The old tag value must no longer resolve any row.
1567        assert_eq!(
1568            query_row_count(&conn, "SELECT id FROM t WHERE tag = 7;").await?,
1569            0
1570        );
1571        Ok(())
1572    }
1573
1574    // ------------------------------------------------------------------
1575    // Named parameters (`:name` / `@name` / `$name` / `#name`).
1576    // ------------------------------------------------------------------
1577
1578    #[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        // Heterogeneous named params via tuple syntax.
1589        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    /// The same named placeholder used twice in one statement resolves to
1684    /// the same bind index (SQLite semantics): binding it once must satisfy
1685    /// both occurrences.
1686    #[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    /// Homogeneous const-array named-parameter syntax (`[(":a", v), ...]`)
1702    /// exercises the zero-allocation `&'static str` key path (the
1703    /// `[(&'static str, T); N]` `IntoParams` impl) with more than one pair.
1704    #[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    /// Owned `String` keys (the non-`'static` path, e.g. built at runtime)
1728    /// still bind correctly through the `Vec<(String, T)>` `IntoParams` impl
1729    /// — the `Cow::Owned` branch of `Params::Named`.
1730    #[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    /// Binding a name that doesn't match any placeholder in the prepared
1747    /// statement must be a clear, catchable error — never a panic, and never
1748    /// a silent no-op that leaves the placeholder unbound.
1749    #[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    /// A named parameter that IS declared in the SQL but never bound must
1775    /// still error at the unknown-name check for any name the caller tries
1776    /// to bind that the statement doesn't recognize (typo protection), while
1777    /// a `query`-side (as opposed to `execute`-side) unknown name must also
1778    /// error rather than silently succeed with a bogus/absent binding.
1779    #[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    //! Tests for [`Database::open_from_bytes`] at the engine-wrapper layer,
1802    //! including multi-connection shared visibility over a single preloaded
1803    //! in-memory image.
1804    use super::*;
1805    use tempfile::NamedTempFile;
1806
1807    /// Produce a populated database image by writing through the engine into a
1808    /// temp file, checkpointing, and reading the bytes back.
1809    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    /// Multiple connections opened from the same preloaded [`Database`] share
1843    /// the underlying in-memory image. A write committed on one connection is
1844    /// visible to a *newly opened* connection, mirroring the shared-storage
1845    /// behavior of the `":memory:"` path. A connection that already
1846    /// established a read snapshot keeps that snapshot (SQLite-style read
1847    /// isolation) — this behavior is pinned explicitly below.
1848    #[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        // Both connections start from the same 2-row image.
1856        assert_eq!(count(&reader).await?, 2);
1857
1858        writer
1859            .execute("INSERT INTO t (id, v) VALUES (3, 'three');", ())
1860            .await?;
1861
1862        // The writer observes its own committed write.
1863        assert_eq!(count(&writer).await?, 3);
1864
1865        // The reader that already took a read snapshot retains it.
1866        assert_eq!(
1867            count(&reader).await?,
1868            2,
1869            "an existing read snapshot is isolated from a later commit"
1870        );
1871
1872        // A freshly opened connection sees the committed write, proving the
1873        // image is genuinely shared (not a private per-connection copy).
1874        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}