From a090fb927a9bcd3439e3cb3d63a148b41f6126fc Mon Sep 17 00:00:00 2001 From: sonhmai <> Date: Mon, 20 Jan 2025 18:09:16 +0700 Subject: [PATCH] centralize Rust integration and regression tests --- Cargo.lock | 5 +- Cargo.toml | 3 +- cli/Cargo.toml | 5 - test/README.md | 5 - test/src/lib.rs | 703 ------------------ {test => tests}/Cargo.toml | 14 +- tests/README.md | 11 + tests/integration/common.rs | 69 ++ tests/integration/functions/mod.rs | 1 + .../functions/test_function_rowid.rs | 83 +++ tests/integration/mod.rs | 4 + tests/integration/pragma/mod.rs | 1 + .../integration/pragma/test_pragma_stmts.rs | 3 +- tests/integration/query_processing/mod.rs | 2 + .../query_processing/test_read_path.rs | 92 +++ .../query_processing/test_write_path.rs | 459 ++++++++++++ tests/lib.rs | 1 + 17 files changed, 737 insertions(+), 724 deletions(-) delete mode 100644 test/README.md delete mode 100644 test/src/lib.rs rename {test => tests}/Cargo.toml (65%) create mode 100644 tests/README.md create mode 100644 tests/integration/common.rs create mode 100644 tests/integration/functions/mod.rs create mode 100644 tests/integration/functions/test_function_rowid.rs create mode 100644 tests/integration/mod.rs create mode 100644 tests/integration/pragma/mod.rs rename cli/tests/test_journal.rs => tests/integration/pragma/test_pragma_stmts.rs (95%) create mode 100644 tests/integration/query_processing/mod.rs create mode 100644 tests/integration/query_processing/test_read_path.rs create mode 100644 tests/integration/query_processing/test_write_path.rs create mode 100644 tests/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a61e2659a..540fb77d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,12 +454,13 @@ name = "core_tester" version = "0.0.13" dependencies = [ "anyhow", + "assert_cmd", "clap", "dirs", "env_logger 0.10.2", "limbo_core", "log", - "rstest", + "rexpect", "rusqlite", "rustyline", "tempfile", @@ -1227,7 +1228,6 @@ name = "limbo" version = "0.0.13" dependencies = [ "anyhow", - "assert_cmd", "clap", "cli-table", "csv", @@ -1236,7 +1236,6 @@ dependencies = [ "env_logger 0.10.2", "limbo_core", "miette", - "rexpect", "rustyline", ] diff --git a/Cargo.toml b/Cargo.toml index bc188deee..5e243c98b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ members = [ "macros", "simulator", "sqlite3", - "test", "extensions/percentile", + "tests", + "extensions/percentile", ] exclude = ["perf/latency/limbo"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3a5ce67ef..1886627e4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -32,8 +32,3 @@ miette = { version = "7.4.0", features = ["fancy"] } [features] io_uring = ["limbo_core/io_uring"] - -# not testing the cli on windows as rexpect does not support it. -[target.'cfg(not(windows))'.dev-dependencies] -assert_cmd = "^2" -rexpect = "0.6.0" diff --git a/test/README.md b/test/README.md deleted file mode 100644 index a90866dd4..000000000 --- a/test/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Currently the best way to run these tests are like this due to long running tests: - -```bash -cargo test test_sequential_write -- --nocapture -``` diff --git a/test/src/lib.rs b/test/src/lib.rs deleted file mode 100644 index a0d370602..000000000 --- a/test/src/lib.rs +++ /dev/null @@ -1,703 +0,0 @@ -use limbo_core::Database; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use tempfile::TempDir; - -#[allow(dead_code)] -struct TempDatabase { - pub path: PathBuf, - pub io: Arc, -} - -#[allow(dead_code, clippy::arc_with_non_send_sync)] -impl TempDatabase { - pub fn new(table_sql: &str) -> Self { - let mut path = TempDir::new().unwrap().into_path(); - path.push("test.db"); - { - let connection = rusqlite::Connection::open(&path).unwrap(); - connection - .pragma_update(None, "journal_mode", "wal") - .unwrap(); - connection.execute(table_sql, ()).unwrap(); - } - let io: Arc = Arc::new(limbo_core::PlatformIO::new().unwrap()); - - Self { path, io } - } - - pub fn connect_limbo(&self) -> Rc { - log::debug!("conneting to limbo"); - let db = Database::open_file(self.io.clone(), self.path.to_str().unwrap()).unwrap(); - - let conn = db.connect(); - log::debug!("connected to limbo"); - conn - } -} - -#[cfg(test)] -mod tests { - use super::*; - use limbo_core::{CheckpointStatus, Connection, StepResult, Value}; - use log::debug; - - #[ignore] - #[test] - fn test_sequential_write() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - - let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY);"); - let conn = tmp_db.connect_limbo(); - - let list_query = "SELECT * FROM test"; - let max_iterations = 10000; - for i in 0..max_iterations { - debug!("inserting {} ", i); - if (i % 100) == 0 { - let progress = (i as f64 / max_iterations as f64) * 100.0; - println!("progress {:.1}%", progress); - } - let insert_query = format!("INSERT INTO test VALUES ({})", i); - match conn.query(insert_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - }; - - let mut current_read_index = 0; - match conn.query(list_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::Row(row) => { - let first_value = row.values.first().expect("missing id"); - let id = match first_value { - Value::Integer(i) => *i as i32, - Value::Float(f) => *f as i32, - _ => unreachable!(), - }; - assert_eq!(current_read_index, id); - current_read_index += 1; - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => { - panic!("Database is busy"); - } - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - } - do_flush(&conn, &tmp_db)?; - } - Ok(()) - } - - #[test] - /// There was a regression with inserting multiple rows with a column containing an unary operator :) - /// https://github.com/tursodatabase/limbo/pull/679 - fn test_regression_multi_row_insert() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("CREATE TABLE test (x REAL);"); - let conn = tmp_db.connect_limbo(); - - let insert_query = "INSERT INTO test VALUES (-2), (-3), (-1)"; - let list_query = "SELECT * FROM test"; - - match conn.query(insert_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - }; - - do_flush(&conn, &tmp_db)?; - - let mut current_read_index = 1; - let expected_ids = vec![-3, -2, -1]; - let mut actual_ids = Vec::new(); - match conn.query(list_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::Row(row) => { - let first_value = row.values.first().expect("missing id"); - let id = match first_value { - Value::Float(f) => *f as i32, - _ => panic!("expected float"), - }; - actual_ids.push(id); - current_read_index += 1; - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => { - panic!("Database is busy"); - } - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - } - - assert_eq!(current_read_index, 4); // Verify we read all rows - // sort ids - actual_ids.sort(); - assert_eq!(actual_ids, expected_ids); - Ok(()) - } - - #[test] - fn test_simple_overflow_page() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY, t TEXT);"); - let conn = tmp_db.connect_limbo(); - - let mut huge_text = String::new(); - for i in 0..8192 { - huge_text.push((b'A' + (i % 24) as u8) as char); - } - - let list_query = "SELECT * FROM test LIMIT 1"; - let insert_query = format!("INSERT INTO test VALUES (1, '{}')", huge_text.as_str()); - - match conn.query(insert_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - }; - - // this flush helped to review hex of test.db - do_flush(&conn, &tmp_db)?; - - match conn.query(list_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::Row(row) => { - let first_value = &row.values[0]; - let text = &row.values[1]; - let id = match first_value { - Value::Integer(i) => *i as i32, - Value::Float(f) => *f as i32, - _ => unreachable!(), - }; - let text = match text { - Value::Text(t) => *t, - _ => unreachable!(), - }; - assert_eq!(1, id); - compare_string(&huge_text, text); - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - } - do_flush(&conn, &tmp_db)?; - Ok(()) - } - - #[test] - fn test_sequential_overflow_page() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY, t TEXT);"); - let conn = tmp_db.connect_limbo(); - let iterations = 10_usize; - - let mut huge_texts = Vec::new(); - for i in 0..iterations { - let mut huge_text = String::new(); - for _j in 0..8192 { - huge_text.push((b'A' + i as u8) as char); - } - huge_texts.push(huge_text); - } - - for i in 0..iterations { - let huge_text = &huge_texts[i]; - let insert_query = format!("INSERT INTO test VALUES ({}, '{}')", i, huge_text.as_str()); - match conn.query(insert_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - }; - } - - let list_query = "SELECT * FROM test LIMIT 1"; - let mut current_index = 0; - match conn.query(list_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::Row(row) => { - let first_value = &row.values[0]; - let text = &row.values[1]; - let id = match first_value { - Value::Integer(i) => *i as i32, - Value::Float(f) => *f as i32, - _ => unreachable!(), - }; - let text = match text { - Value::Text(t) => *t, - _ => unreachable!(), - }; - let huge_text = &huge_texts[current_index]; - assert_eq!(current_index, id as usize); - compare_string(huge_text, text); - current_index += 1; - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - } - do_flush(&conn, &tmp_db)?; - Ok(()) - } - - #[test] - #[ignore] - fn test_wal_checkpoint() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY);"); - // threshold is 1000 by default - let iterations = 1001_usize; - let conn = tmp_db.connect_limbo(); - - for i in 0..iterations { - let insert_query = format!("INSERT INTO test VALUES ({})", i); - do_flush(&conn, &tmp_db)?; - conn.checkpoint()?; - match conn.query(insert_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - }; - } - - do_flush(&conn, &tmp_db)?; - conn.clear_page_cache()?; - let list_query = "SELECT * FROM test LIMIT 1"; - let mut current_index = 0; - match conn.query(list_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::Row(row) => { - let first_value = &row.values[0]; - let id = match first_value { - Value::Integer(i) => *i as i32, - Value::Float(f) => *f as i32, - _ => unreachable!(), - }; - assert_eq!(current_index, id as usize); - current_index += 1; - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - } - do_flush(&conn, &tmp_db)?; - Ok(()) - } - - #[test] - fn test_wal_restart() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY);"); - // threshold is 1000 by default - - fn insert(i: usize, conn: &Rc, tmp_db: &TempDatabase) -> anyhow::Result<()> { - debug!("inserting {}", i); - let insert_query = format!("INSERT INTO test VALUES ({})", i); - match conn.query(insert_query) { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - }; - debug!("inserted {}", i); - tmp_db.io.run_once()?; - Ok(()) - } - - fn count(conn: &Rc, tmp_db: &TempDatabase) -> anyhow::Result { - debug!("counting"); - let list_query = "SELECT count(x) FROM test"; - loop { - if let Some(ref mut rows) = conn.query(list_query)? { - loop { - match rows.next_row()? { - StepResult::Row(row) => { - let first_value = &row.values[0]; - let count = match first_value { - Value::Integer(i) => *i as i32, - _ => unreachable!(), - }; - debug!("counted {}", count); - return Ok(count as usize); - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => panic!("Database is busy"), - } - } - } - } - } - - { - let conn = tmp_db.connect_limbo(); - insert(1, &conn, &tmp_db)?; - assert_eq!(count(&conn, &tmp_db)?, 1); - conn.close()?; - } - { - let conn = tmp_db.connect_limbo(); - assert_eq!( - count(&conn, &tmp_db)?, - 1, - "failed to read from wal from another connection" - ); - conn.close()?; - } - Ok(()) - } - - fn compare_string(a: &String, b: &String) { - assert_eq!(a.len(), b.len(), "Strings are not equal in size!"); - let a = a.as_bytes(); - let b = b.as_bytes(); - - let len = a.len(); - for i in 0..len { - if a[i] != b[i] { - println!( - "Bytes differ \n\t at index: dec -> {} hex -> {:#02x} \n\t values dec -> {}!={} hex -> {:#02x}!={:#02x}", - i, i, a[i], b[i], a[i], b[i] - ); - break; - } - } - } - - fn do_flush(conn: &Rc, tmp_db: &TempDatabase) -> anyhow::Result<()> { - loop { - match conn.cacheflush()? { - CheckpointStatus::Done => { - break; - } - CheckpointStatus::IO => { - tmp_db.io.run_once()?; - } - } - } - Ok(()) - } - - #[test] - fn test_last_insert_rowid_basic() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = - TempDatabase::new("CREATE TABLE test_rowid (id INTEGER PRIMARY KEY, val TEXT);"); - let conn = tmp_db.connect_limbo(); - - // Simple insert - let mut insert_query = - conn.query("INSERT INTO test_rowid (id, val) VALUES (NULL, 'test1')")?; - if let Some(ref mut rows) = insert_query { - loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - } - } - - // Check last_insert_rowid separately - let mut select_query = conn.query("SELECT last_insert_rowid()")?; - if let Some(ref mut rows) = select_query { - loop { - match rows.next_row()? { - StepResult::Row(row) => { - if let Value::Integer(id) = row.values[0] { - assert_eq!(id, 1, "First insert should have rowid 1"); - } - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => panic!("Database is busy"), - } - } - } - - // Test explicit rowid - match conn.query("INSERT INTO test_rowid (id, val) VALUES (5, 'test2')") { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Done => break, - _ => unreachable!(), - } - }, - Ok(None) => {} - Err(err) => eprintln!("{}", err), - }; - - // Check last_insert_rowid after explicit id - let mut last_id = 0; - match conn.query("SELECT last_insert_rowid()") { - Ok(Some(ref mut rows)) => loop { - match rows.next_row()? { - StepResult::Row(row) => { - if let Value::Integer(id) = row.values[0] { - last_id = id; - } - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => panic!("Database is busy"), - } - }, - Ok(None) => {} - Err(err) => eprintln!("{}", err), - }; - assert_eq!(last_id, 5, "Explicit insert should have rowid 5"); - do_flush(&conn, &tmp_db)?; - Ok(()) - } - - #[test] - fn test_statement_reset() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("create table test (i integer);"); - let conn = tmp_db.connect_limbo(); - - conn.execute("insert into test values (1)")?; - conn.execute("insert into test values (2)")?; - - let mut stmt = conn.prepare("select * from test")?; - - loop { - match stmt.step()? { - StepResult::Row(row) => { - assert_eq!(row.values[0], Value::Integer(1)); - break; - } - StepResult::IO => tmp_db.io.run_once()?, - _ => break, - } - } - - stmt.reset(); - - loop { - match stmt.step()? { - StepResult::Row(row) => { - assert_eq!(row.values[0], Value::Integer(1)); - break; - } - StepResult::IO => tmp_db.io.run_once()?, - _ => break, - } - } - - Ok(()) - } - - #[test] - fn test_statement_reset_bind() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("create table test (i integer);"); - let conn = tmp_db.connect_limbo(); - - let mut stmt = conn.prepare("select ?")?; - - stmt.bind_at(1.try_into()?, Value::Integer(1)); - - loop { - match stmt.step()? { - StepResult::Row(row) => { - assert_eq!(row.values[0], Value::Integer(1)); - } - StepResult::IO => tmp_db.io.run_once()?, - _ => break, - } - } - - stmt.reset(); - - stmt.bind_at(1.try_into()?, Value::Integer(2)); - - loop { - match stmt.step()? { - StepResult::Row(row) => { - assert_eq!(row.values[0], Value::Integer(2)); - } - StepResult::IO => tmp_db.io.run_once()?, - _ => break, - } - } - - Ok(()) - } - - #[test] - fn test_statement_bind() -> anyhow::Result<()> { - let _ = env_logger::try_init(); - let tmp_db = TempDatabase::new("create table test (i integer);"); - let conn = tmp_db.connect_limbo(); - - let mut stmt = conn.prepare("select ?, ?1, :named, ?3, ?4")?; - - stmt.bind_at(1.try_into()?, Value::Text(&"hello".to_string())); - - let i = stmt.parameters().index(":named").unwrap(); - stmt.bind_at(i, Value::Integer(42)); - - stmt.bind_at(3.try_into()?, Value::Blob(&vec![0x1, 0x2, 0x3])); - - stmt.bind_at(4.try_into()?, Value::Float(0.5)); - - assert_eq!(stmt.parameters().count(), 4); - - loop { - match stmt.step()? { - StepResult::Row(row) => { - if let Value::Text(s) = row.values[0] { - assert_eq!(s, "hello") - } - - if let Value::Text(s) = row.values[1] { - assert_eq!(s, "hello") - } - - if let Value::Integer(i) = row.values[2] { - assert_eq!(i, 42) - } - - if let Value::Blob(v) = row.values[3] { - assert_eq!(v, &vec![0x1 as u8, 0x2, 0x3]) - } - - if let Value::Float(f) = row.values[4] { - assert_eq!(f, 0.5) - } - } - StepResult::IO => { - tmp_db.io.run_once()?; - } - StepResult::Interrupt => break, - StepResult::Done => break, - StepResult::Busy => panic!("Database is busy"), - }; - } - Ok(()) - } -} diff --git a/test/Cargo.toml b/tests/Cargo.toml similarity index 65% rename from test/Cargo.toml rename to tests/Cargo.toml index 26fd3ad18..473fed1cd 100644 --- a/test/Cargo.toml +++ b/tests/Cargo.toml @@ -5,12 +5,14 @@ authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true -description = "Internal tester of write path" +description = "Integration tests" [lib] -name = "test" -path = "src/lib.rs" +path = "lib.rs" +[[test]] +name = "integration_tests" +path = "integration/mod.rs" [dependencies] anyhow = "1.0.75" @@ -22,6 +24,8 @@ rustyline = "12.0.0" rusqlite = { version = "0.29", features = ["bundled"] } tempfile = "3.0.7" log = "0.4.22" +assert_cmd = "^2" -[dev-dependencies] -rstest = "0.18.2" +# rexpect does not support windows. +[target.'cfg(not(windows))'.dependencies] +rexpect = "0.6.0" \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..9dfe55f6c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,11 @@ + +Integration and regression test suite. + +```bash + +# run all tests +cargo test + +# run individual test +cargo test test_sequential_write -- --nocapture +``` diff --git a/tests/integration/common.rs b/tests/integration/common.rs new file mode 100644 index 000000000..86f4b7b3f --- /dev/null +++ b/tests/integration/common.rs @@ -0,0 +1,69 @@ +use limbo_core::{CheckpointStatus, Connection, Database, IO}; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use tempfile::TempDir; + +#[allow(dead_code)] +pub struct TempDatabase { + pub path: PathBuf, + pub io: Arc, +} + +#[allow(dead_code, clippy::arc_with_non_send_sync)] +impl TempDatabase { + pub fn new(table_sql: &str) -> Self { + let mut path = TempDir::new().unwrap().into_path(); + path.push("test.db"); + { + let connection = rusqlite::Connection::open(&path).unwrap(); + connection + .pragma_update(None, "journal_mode", "wal") + .unwrap(); + connection.execute(table_sql, ()).unwrap(); + } + let io: Arc = Arc::new(limbo_core::PlatformIO::new().unwrap()); + + Self { path, io } + } + + pub fn connect_limbo(&self) -> Rc { + log::debug!("conneting to limbo"); + let db = Database::open_file(self.io.clone(), self.path.to_str().unwrap()).unwrap(); + + let conn = db.connect(); + log::debug!("connected to limbo"); + conn + } +} + +pub(crate) fn do_flush(conn: &Rc, tmp_db: &TempDatabase) -> anyhow::Result<()> { + loop { + match conn.cacheflush()? { + CheckpointStatus::Done => { + break; + } + CheckpointStatus::IO => { + tmp_db.io.run_once()?; + } + } + } + Ok(()) +} + +pub(crate) fn compare_string(a: &String, b: &String) { + assert_eq!(a.len(), b.len(), "Strings are not equal in size!"); + let a = a.as_bytes(); + let b = b.as_bytes(); + + let len = a.len(); + for i in 0..len { + if a[i] != b[i] { + println!( + "Bytes differ \n\t at index: dec -> {} hex -> {:#02x} \n\t values dec -> {}!={} hex -> {:#02x}!={:#02x}", + i, i, a[i], b[i], a[i], b[i] + ); + break; + } + } +} diff --git a/tests/integration/functions/mod.rs b/tests/integration/functions/mod.rs new file mode 100644 index 000000000..66fcb1cb5 --- /dev/null +++ b/tests/integration/functions/mod.rs @@ -0,0 +1 @@ +mod test_function_rowid; diff --git a/tests/integration/functions/test_function_rowid.rs b/tests/integration/functions/test_function_rowid.rs new file mode 100644 index 000000000..6655cee0d --- /dev/null +++ b/tests/integration/functions/test_function_rowid.rs @@ -0,0 +1,83 @@ +use crate::common::{do_flush, TempDatabase}; +use limbo_core::{StepResult, Value}; + +#[test] +fn test_last_insert_rowid_basic() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("CREATE TABLE test_rowid (id INTEGER PRIMARY KEY, val TEXT);"); + let conn = tmp_db.connect_limbo(); + + // Simple insert + let mut insert_query = conn.query("INSERT INTO test_rowid (id, val) VALUES (NULL, 'test1')")?; + if let Some(ref mut rows) = insert_query { + loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + } + } + + // Check last_insert_rowid separately + let mut select_query = conn.query("SELECT last_insert_rowid()")?; + if let Some(ref mut rows) = select_query { + loop { + match rows.next_row()? { + StepResult::Row(row) => { + if let Value::Integer(id) = row.values[0] { + assert_eq!(id, 1, "First insert should have rowid 1"); + } + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => panic!("Database is busy"), + } + } + } + + // Test explicit rowid + match conn.query("INSERT INTO test_rowid (id, val) VALUES (5, 'test2')") { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => eprintln!("{}", err), + }; + + // Check last_insert_rowid after explicit id + let mut last_id = 0; + match conn.query("SELECT last_insert_rowid()") { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::Row(row) => { + if let Value::Integer(id) = row.values[0] { + last_id = id; + } + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => panic!("Database is busy"), + } + }, + Ok(None) => {} + Err(err) => eprintln!("{}", err), + }; + assert_eq!(last_id, 5, "Explicit insert should have rowid 5"); + do_flush(&conn, &tmp_db)?; + Ok(()) +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs new file mode 100644 index 000000000..221ca089f --- /dev/null +++ b/tests/integration/mod.rs @@ -0,0 +1,4 @@ +mod common; +mod functions; +mod pragma; +mod query_processing; diff --git a/tests/integration/pragma/mod.rs b/tests/integration/pragma/mod.rs new file mode 100644 index 000000000..1070d3011 --- /dev/null +++ b/tests/integration/pragma/mod.rs @@ -0,0 +1 @@ +mod test_pragma_stmts; diff --git a/cli/tests/test_journal.rs b/tests/integration/pragma/test_pragma_stmts.rs similarity index 95% rename from cli/tests/test_journal.rs rename to tests/integration/pragma/test_pragma_stmts.rs index 3d34719b1..11a831d37 100644 --- a/cli/tests/test_journal.rs +++ b/tests/integration/pragma/test_pragma_stmts.rs @@ -35,7 +35,6 @@ mod tests { fn run_cli() -> process::Command { let bin_path = cargo_bin("limbo"); - let cmd = process::Command::new(bin_path); - cmd + process::Command::new(bin_path) } } diff --git a/tests/integration/query_processing/mod.rs b/tests/integration/query_processing/mod.rs new file mode 100644 index 000000000..71309fe5a --- /dev/null +++ b/tests/integration/query_processing/mod.rs @@ -0,0 +1,2 @@ +mod test_read_path; +mod test_write_path; diff --git a/tests/integration/query_processing/test_read_path.rs b/tests/integration/query_processing/test_read_path.rs new file mode 100644 index 000000000..55f72c1cc --- /dev/null +++ b/tests/integration/query_processing/test_read_path.rs @@ -0,0 +1,92 @@ +use crate::common::TempDatabase; +use limbo_core::{StepResult, Value}; + +#[test] +fn test_statement_reset_bind() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("create table test (i integer);"); + let conn = tmp_db.connect_limbo(); + + let mut stmt = conn.prepare("select ?")?; + + stmt.bind_at(1.try_into()?, Value::Integer(1)); + + loop { + match stmt.step()? { + StepResult::Row(row) => { + assert_eq!(row.values[0], Value::Integer(1)); + } + StepResult::IO => tmp_db.io.run_once()?, + _ => break, + } + } + + stmt.reset(); + + stmt.bind_at(1.try_into()?, Value::Integer(2)); + + loop { + match stmt.step()? { + StepResult::Row(row) => { + assert_eq!(row.values[0], Value::Integer(2)); + } + StepResult::IO => tmp_db.io.run_once()?, + _ => break, + } + } + + Ok(()) +} + +#[test] +fn test_statement_bind() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("create table test (i integer);"); + let conn = tmp_db.connect_limbo(); + + let mut stmt = conn.prepare("select ?, ?1, :named, ?3, ?4")?; + + stmt.bind_at(1.try_into()?, Value::Text(&"hello".to_string())); + + let i = stmt.parameters().index(":named").unwrap(); + stmt.bind_at(i, Value::Integer(42)); + + stmt.bind_at(3.try_into()?, Value::Blob(&vec![0x1, 0x2, 0x3])); + + stmt.bind_at(4.try_into()?, Value::Float(0.5)); + + assert_eq!(stmt.parameters().count(), 4); + + loop { + match stmt.step()? { + StepResult::Row(row) => { + if let Value::Text(s) = row.values[0] { + assert_eq!(s, "hello") + } + + if let Value::Text(s) = row.values[1] { + assert_eq!(s, "hello") + } + + if let Value::Integer(i) = row.values[2] { + assert_eq!(i, 42) + } + + if let Value::Blob(v) = row.values[3] { + assert_eq!(v, &vec![0x1 as u8, 0x2, 0x3]) + } + + if let Value::Float(f) = row.values[4] { + assert_eq!(f, 0.5) + } + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => panic!("Database is busy"), + }; + } + Ok(()) +} diff --git a/tests/integration/query_processing/test_write_path.rs b/tests/integration/query_processing/test_write_path.rs new file mode 100644 index 000000000..97b68a804 --- /dev/null +++ b/tests/integration/query_processing/test_write_path.rs @@ -0,0 +1,459 @@ +use crate::common; +use crate::common::{compare_string, do_flush, TempDatabase}; +use limbo_core::{Connection, StepResult, Value}; +use log::debug; +use std::rc::Rc; + +#[test] +fn test_simple_overflow_page() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY, t TEXT);"); + let conn = tmp_db.connect_limbo(); + + let mut huge_text = String::new(); + for i in 0..8192 { + huge_text.push((b'A' + (i % 24) as u8) as char); + } + + let list_query = "SELECT * FROM test LIMIT 1"; + let insert_query = format!("INSERT INTO test VALUES (1, '{}')", huge_text.as_str()); + + match conn.query(insert_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + }; + + // this flush helped to review hex of test.db + do_flush(&conn, &tmp_db)?; + + match conn.query(list_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::Row(row) => { + let first_value = &row.values[0]; + let text = &row.values[1]; + let id = match first_value { + Value::Integer(i) => *i as i32, + Value::Float(f) => *f as i32, + _ => unreachable!(), + }; + let text = match text { + Value::Text(t) => *t, + _ => unreachable!(), + }; + assert_eq!(1, id); + compare_string(&huge_text, text); + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + } + do_flush(&conn, &tmp_db)?; + Ok(()) +} + +#[test] +fn test_sequential_overflow_page() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY, t TEXT);"); + let conn = tmp_db.connect_limbo(); + let iterations = 10_usize; + + let mut huge_texts = Vec::new(); + for i in 0..iterations { + let mut huge_text = String::new(); + for _j in 0..8192 { + huge_text.push((b'A' + i as u8) as char); + } + huge_texts.push(huge_text); + } + + for i in 0..iterations { + let huge_text = &huge_texts[i]; + let insert_query = format!("INSERT INTO test VALUES ({}, '{}')", i, huge_text.as_str()); + match conn.query(insert_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + }; + } + + let list_query = "SELECT * FROM test LIMIT 1"; + let mut current_index = 0; + match conn.query(list_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::Row(row) => { + let first_value = &row.values[0]; + let text = &row.values[1]; + let id = match first_value { + Value::Integer(i) => *i as i32, + Value::Float(f) => *f as i32, + _ => unreachable!(), + }; + let text = match text { + Value::Text(t) => *t, + _ => unreachable!(), + }; + let huge_text = &huge_texts[current_index]; + assert_eq!(current_index, id as usize); + compare_string(huge_text, text); + current_index += 1; + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + } + do_flush(&conn, &tmp_db)?; + Ok(()) +} + +#[ignore] +#[test] +fn test_sequential_write() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + + let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY);"); + let conn = tmp_db.connect_limbo(); + + let list_query = "SELECT * FROM test"; + let max_iterations = 10000; + for i in 0..max_iterations { + debug!("inserting {} ", i); + if (i % 100) == 0 { + let progress = (i as f64 / max_iterations as f64) * 100.0; + println!("progress {:.1}%", progress); + } + let insert_query = format!("INSERT INTO test VALUES ({})", i); + match conn.query(insert_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + }; + + let mut current_read_index = 0; + match conn.query(list_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::Row(row) => { + let first_value = row.values.first().expect("missing id"); + let id = match first_value { + Value::Integer(i) => *i as i32, + Value::Float(f) => *f as i32, + _ => unreachable!(), + }; + assert_eq!(current_read_index, id); + current_read_index += 1; + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => { + panic!("Database is busy"); + } + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + } + common::do_flush(&conn, &tmp_db)?; + } + Ok(()) +} + +#[test] +/// There was a regression with inserting multiple rows with a column containing an unary operator :) +/// https://github.com/tursodatabase/limbo/pull/679 +fn test_regression_multi_row_insert() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("CREATE TABLE test (x REAL);"); + let conn = tmp_db.connect_limbo(); + + let insert_query = "INSERT INTO test VALUES (-2), (-3), (-1)"; + let list_query = "SELECT * FROM test"; + + match conn.query(insert_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + }; + + common::do_flush(&conn, &tmp_db)?; + + let mut current_read_index = 1; + let expected_ids = vec![-3, -2, -1]; + let mut actual_ids = Vec::new(); + match conn.query(list_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::Row(row) => { + let first_value = row.values.first().expect("missing id"); + let id = match first_value { + Value::Float(f) => *f as i32, + _ => panic!("expected float"), + }; + actual_ids.push(id); + current_read_index += 1; + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => { + panic!("Database is busy"); + } + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + } + + assert_eq!(current_read_index, 4); // Verify we read all rows + // sort ids + actual_ids.sort(); + assert_eq!(actual_ids, expected_ids); + Ok(()) +} + +#[test] +fn test_statement_reset() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("create table test (i integer);"); + let conn = tmp_db.connect_limbo(); + + conn.execute("insert into test values (1)")?; + conn.execute("insert into test values (2)")?; + + let mut stmt = conn.prepare("select * from test")?; + + loop { + match stmt.step()? { + StepResult::Row(row) => { + assert_eq!(row.values[0], Value::Integer(1)); + break; + } + StepResult::IO => tmp_db.io.run_once()?, + _ => break, + } + } + + stmt.reset(); + + loop { + match stmt.step()? { + StepResult::Row(row) => { + assert_eq!(row.values[0], Value::Integer(1)); + break; + } + StepResult::IO => tmp_db.io.run_once()?, + _ => break, + } + } + + Ok(()) +} + +#[test] +#[ignore] +fn test_wal_checkpoint() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY);"); + // threshold is 1000 by default + let iterations = 1001_usize; + let conn = tmp_db.connect_limbo(); + + for i in 0..iterations { + let insert_query = format!("INSERT INTO test VALUES ({})", i); + do_flush(&conn, &tmp_db)?; + conn.checkpoint()?; + match conn.query(insert_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + }; + } + + do_flush(&conn, &tmp_db)?; + conn.clear_page_cache()?; + let list_query = "SELECT * FROM test LIMIT 1"; + let mut current_index = 0; + match conn.query(list_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::Row(row) => { + let first_value = &row.values[0]; + let id = match first_value { + Value::Integer(i) => *i as i32, + Value::Float(f) => *f as i32, + _ => unreachable!(), + }; + assert_eq!(current_index, id as usize); + current_index += 1; + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + } + do_flush(&conn, &tmp_db)?; + Ok(()) +} + +#[test] +fn test_wal_restart() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let tmp_db = TempDatabase::new("CREATE TABLE test (x INTEGER PRIMARY KEY);"); + // threshold is 1000 by default + + fn insert(i: usize, conn: &Rc, tmp_db: &TempDatabase) -> anyhow::Result<()> { + debug!("inserting {}", i); + let insert_query = format!("INSERT INTO test VALUES ({})", i); + match conn.query(insert_query) { + Ok(Some(ref mut rows)) => loop { + match rows.next_row()? { + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Done => break, + _ => unreachable!(), + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + }; + debug!("inserted {}", i); + tmp_db.io.run_once()?; + Ok(()) + } + + fn count(conn: &Rc, tmp_db: &TempDatabase) -> anyhow::Result { + debug!("counting"); + let list_query = "SELECT count(x) FROM test"; + loop { + if let Some(ref mut rows) = conn.query(list_query)? { + loop { + match rows.next_row()? { + StepResult::Row(row) => { + let first_value = &row.values[0]; + let count = match first_value { + Value::Integer(i) => *i as i32, + _ => unreachable!(), + }; + debug!("counted {}", count); + return Ok(count as usize); + } + StepResult::IO => { + tmp_db.io.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => panic!("Database is busy"), + } + } + } + } + } + + { + let conn = tmp_db.connect_limbo(); + insert(1, &conn, &tmp_db)?; + assert_eq!(count(&conn, &tmp_db)?, 1); + conn.close()?; + } + { + let conn = tmp_db.connect_limbo(); + assert_eq!( + count(&conn, &tmp_db)?, + 1, + "failed to read from wal from another connection" + ); + conn.close()?; + } + Ok(()) +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1 @@ +