diff --git a/Cargo.lock b/Cargo.lock index 69d059d0e..f3d6ced1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,6 +831,7 @@ dependencies = [ "rand 0.9.2", "rand_chacha 0.9.0", "rusqlite", + "sql_generation", "tempfile", "test-log", "tokio", @@ -838,6 +839,7 @@ dependencies = [ "tracing-subscriber", "turso", "turso_core", + "turso_parser", "twox-hash", "zerocopy 0.8.26", ] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 837635850..416d26768 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -29,6 +29,8 @@ rand = { workspace = true } zerocopy = "0.8.26" ctor = "0.5.0" twox-hash = "2.1.1" +sql_generation = { path = "../sql_generation" } +turso_parser = { workspace = true } [dev-dependencies] test-log = { version = "0.2.17", features = ["trace"] } diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index 0e09c1c2c..caffe5582 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -1,4 +1,5 @@ pub mod grammar_generator; +pub mod rowid_alias; #[cfg(test)] mod tests { diff --git a/tests/integration/fuzz/rowid_alias.rs b/tests/integration/fuzz/rowid_alias.rs new file mode 100644 index 000000000..e9de059cc --- /dev/null +++ b/tests/integration/fuzz/rowid_alias.rs @@ -0,0 +1,210 @@ +use crate::common::{limbo_exec_rows, TempDatabase}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use sql_generation::{ + generation::{Arbitrary, GenerationContext, Opts}, + model::{ + query::{Create, Insert, Select}, + table::{Column, ColumnType, Table}, + }, +}; +use turso_parser::ast::ColumnConstraint; + +fn rng_from_time_or_env() -> (ChaCha8Rng, u64) { + let seed = if let Ok(seed_str) = std::env::var("FUZZ_SEED") { + seed_str.parse::().expect("Invalid FUZZ_SEED value") + } else { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + }; + let rng = ChaCha8Rng::seed_from_u64(seed); + (rng, seed) +} + +// Our test context that implements GenerationContext +#[derive(Debug, Clone)] +struct FuzzTestContext { + opts: Opts, + tables: Vec, +} + +impl FuzzTestContext { + fn new() -> Self { + Self { + opts: Opts::default(), + tables: Vec::new(), + } + } + + fn add_table(&mut self, table: Table) { + self.tables.push(table); + } +} + +impl GenerationContext for FuzzTestContext { + fn tables(&self) -> &Vec
{ + &self.tables + } + + fn opts(&self) -> &Opts { + &self.opts + } +} + +// Convert a table's CREATE statement to use INTEGER PRIMARY KEY (rowid alias) +fn convert_to_rowid_alias(create_sql: &str) -> String { + // Since we always generate INTEGER PRIMARY KEY, just return as-is + create_sql.to_string() +} + +// Convert a table's CREATE statement to NOT use rowid alias +fn convert_to_no_rowid_alias(create_sql: &str) -> String { + // Replace INTEGER PRIMARY KEY with INT PRIMARY KEY to disable rowid alias + create_sql.replace("INTEGER PRIMARY KEY", "INT PRIMARY KEY") +} + +#[test] +pub fn rowid_alias_differential_fuzz() { + let (mut rng, seed) = rng_from_time_or_env(); + tracing::info!("rowid_alias_differential_fuzz seed: {}", seed); + + // Number of queries to test + let num_queries = if let Ok(num) = std::env::var("FUZZ_NUM_QUERIES") { + num.parse::().unwrap_or(1000) + } else { + 1000 + }; + + // Create two Limbo databases with indexes enabled + let db_with_alias = TempDatabase::new_empty(true); + let db_without_alias = TempDatabase::new_empty(true); + + // Connect to both databases + let conn_with_alias = db_with_alias.connect_limbo(); + let conn_without_alias = db_without_alias.connect_limbo(); + + // Create our test context + let mut context = FuzzTestContext::new(); + + let mut successful_queries = 0; + let mut skipped_queries = 0; + + for iteration in 0..num_queries { + // Decide whether to create a new table, insert data, or generate a query + let action = + if context.tables.is_empty() || (context.tables.len() < 5 && rng.random_bool(0.1)) { + 0 // Create a new table + } else if rng.random_bool(0.3) { + 1 // Insert data + } else { + 2 // Generate a SELECT query + }; + + match action { + 0 => { + // Generate a new table with an integer primary key + let primary_key = Column { + name: "id".to_string(), + column_type: ColumnType::Integer, + constraints: vec![ColumnConstraint::PrimaryKey { + order: None, + conflict_clause: None, + auto_increment: false, + }], + }; + let table_name = format!("table_{}", context.tables.len()); + let table = Table::arbitrary_with_columns( + &mut rng, + &context, + table_name, + vec![primary_key], + ); + let create = Create { + table: table.clone(), + }; + + // Create table with rowid alias in first database + let create_with_alias = convert_to_rowid_alias(&create.to_string()); + let _ = limbo_exec_rows(&db_with_alias, &conn_with_alias, &create_with_alias); + + // Create table without rowid alias in second database + let create_without_alias = convert_to_no_rowid_alias(&create.to_string()); + let _ = limbo_exec_rows( + &db_without_alias, + &conn_without_alias, + &create_without_alias, + ); + + // Add table to context for future query generation + context.add_table(table); + + skipped_queries += 1; + continue; + } + 1 => { + // Generate and execute an INSERT statement + let insert = Insert::arbitrary(&mut rng, &context); + let insert_str = insert.to_string(); + + // Execute the insert in both databases + let _ = limbo_exec_rows(&db_with_alias, &conn_with_alias, &insert_str); + let _ = limbo_exec_rows(&db_without_alias, &conn_without_alias, &insert_str); + + // Update the table's rows in the context so predicate generation knows about the data + if let Insert::Values { + table: table_name, + values, + } = &insert + { + for table in &mut context.tables { + if table.name == *table_name { + table.rows.extend(values.clone()); + break; + } + } + } + + skipped_queries += 1; + continue; + } + _ => { + // Continue to generate SELECT query below + } + } + + let select = Select::arbitrary(&mut rng, &context); + let query_str = select.to_string(); + + tracing::debug!("Comparing query {}: {}", iteration, query_str); + + let with_alias_results = limbo_exec_rows(&db_with_alias, &conn_with_alias, &query_str); + let without_alias_results = + limbo_exec_rows(&db_without_alias, &conn_without_alias, &query_str); + + let mut sorted_with_alias = with_alias_results; + let mut sorted_without_alias = without_alias_results; + + // Sort results to handle different row ordering + sorted_with_alias.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}"))); + sorted_without_alias.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}"))); + + assert_eq!( + sorted_with_alias, sorted_without_alias, + "Query produced different results with and without rowid alias!\n\ + Query: {query_str}\n\ + With rowid alias: {sorted_with_alias:?}\n\ + Without rowid alias: {sorted_without_alias:?}\n\ + Seed: {seed}" + ); + + successful_queries += 1; + } + + tracing::info!( + "Rowid alias differential fuzz test completed: {} queries tested successfully, {} queries skipped", + successful_queries, + skipped_queries + ); +}