mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-08 09:44:21 +01:00
tests: Add rowid alias fuzz test case
This adds a new fuzz test case to verify that any query returns the same results with and without a rowid alias.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod grammar_generator;
|
||||
pub mod rowid_alias;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
210
tests/integration/fuzz/rowid_alias.rs
Normal file
210
tests/integration/fuzz/rowid_alias.rs
Normal file
@@ -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::<u64>().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<Table>,
|
||||
}
|
||||
|
||||
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<Table> {
|
||||
&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::<usize>().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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user