mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-09 18:24:20 +01:00
Add subquery fuzz test
This commit is contained in:
@@ -4203,4 +4203,685 @@ mod fuzz_tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Tests for correlated and uncorrelated subqueries occurring in the WHERE clause of a SELECT statement.
|
||||
pub fn table_subquery_fuzz() {
|
||||
let _ = env_logger::try_init();
|
||||
let (mut rng, seed) = rng_from_time_or_env();
|
||||
log::info!("table_subquery_fuzz seed: {seed}");
|
||||
|
||||
// Constants for fuzzing parameters
|
||||
const NUM_FUZZ_ITERATIONS: usize = 2000;
|
||||
const MAX_ROWS_PER_TABLE: usize = 100;
|
||||
const MIN_ROWS_PER_TABLE: usize = 5;
|
||||
const MAX_SUBQUERY_DEPTH: usize = 4;
|
||||
|
||||
let db = TempDatabase::new_empty(true);
|
||||
let limbo_conn = db.connect_limbo();
|
||||
let sqlite_conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
|
||||
let mut debug_ddl_dml_string = String::new();
|
||||
|
||||
// Create 3 simple tables
|
||||
let table_schemas = [
|
||||
"CREATE TABLE t1 (id INT PRIMARY KEY, value1 INTEGER, value2 INTEGER);",
|
||||
"CREATE TABLE t2 (id INT PRIMARY KEY, ref_id INTEGER, data INTEGER);",
|
||||
"CREATE TABLE t3 (id INT PRIMARY KEY, category INTEGER, amount INTEGER);",
|
||||
];
|
||||
|
||||
for schema in &table_schemas {
|
||||
debug_ddl_dml_string.push_str(schema);
|
||||
limbo_exec_rows(&db, &limbo_conn, schema);
|
||||
sqlite_exec_rows(&sqlite_conn, schema);
|
||||
}
|
||||
|
||||
// Populate tables with random data
|
||||
for table_num in 1..=3 {
|
||||
let num_rows = rng.random_range(MIN_ROWS_PER_TABLE..=MAX_ROWS_PER_TABLE);
|
||||
for i in 1..=num_rows {
|
||||
let insert_sql = match table_num {
|
||||
1 => format!(
|
||||
"INSERT INTO t1 VALUES ({}, {}, {});",
|
||||
i,
|
||||
rng.random_range(-10..20),
|
||||
rng.random_range(-5..15)
|
||||
),
|
||||
2 => format!(
|
||||
"INSERT INTO t2 VALUES ({}, {}, {});",
|
||||
i,
|
||||
rng.random_range(1..=num_rows), // ref_id references t1 approximately
|
||||
rng.random_range(-5..10)
|
||||
),
|
||||
3 => format!(
|
||||
"INSERT INTO t3 VALUES ({}, {}, {});",
|
||||
i,
|
||||
rng.random_range(1..5), // category 1-4
|
||||
rng.random_range(0..100)
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
log::debug!("{insert_sql}");
|
||||
debug_ddl_dml_string.push_str(&insert_sql);
|
||||
limbo_exec_rows(&db, &limbo_conn, &insert_sql);
|
||||
sqlite_exec_rows(&sqlite_conn, &insert_sql);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("DDL/DML to reproduce manually:\n{debug_ddl_dml_string}");
|
||||
|
||||
// Helper function to generate random simple WHERE condition
|
||||
let gen_simple_where = |rng: &mut ChaCha8Rng, table: &str| -> String {
|
||||
let conditions = match table {
|
||||
"t1" => vec![
|
||||
format!("value1 > {}", rng.random_range(-5..15)),
|
||||
format!("value2 < {}", rng.random_range(-5..15)),
|
||||
format!("id <= {}", rng.random_range(1..20)),
|
||||
"value1 IS NOT NULL".to_string(),
|
||||
],
|
||||
"t2" => vec![
|
||||
format!("data > {}", rng.random_range(-3..8)),
|
||||
format!("ref_id = {}", rng.random_range(1..15)),
|
||||
format!("id < {}", rng.random_range(5..25)),
|
||||
"data IS NOT NULL".to_string(),
|
||||
],
|
||||
"t3" => vec![
|
||||
format!("category = {}", rng.random_range(1..5)),
|
||||
format!("amount > {}", rng.random_range(0..50)),
|
||||
format!("id <= {}", rng.random_range(1..20)),
|
||||
"amount IS NOT NULL".to_string(),
|
||||
],
|
||||
_ => vec!["1=1".to_string()],
|
||||
};
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
};
|
||||
|
||||
// Helper function to generate simple subquery
|
||||
fn gen_subquery(rng: &mut ChaCha8Rng, depth: usize, outer_table: Option<&str>) -> String {
|
||||
if depth > MAX_SUBQUERY_DEPTH {
|
||||
return "SELECT 1".to_string();
|
||||
}
|
||||
|
||||
let gen_simple_where_inner = |rng: &mut ChaCha8Rng, table: &str| -> String {
|
||||
let conditions = match table {
|
||||
"t1" => vec![
|
||||
format!("value1 > {}", rng.random_range(-5..15)),
|
||||
format!("value2 < {}", rng.random_range(-5..15)),
|
||||
format!("id <= {}", rng.random_range(1..20)),
|
||||
"value1 IS NOT NULL".to_string(),
|
||||
],
|
||||
"t2" => vec![
|
||||
format!("data > {}", rng.random_range(-3..8)),
|
||||
format!("ref_id = {}", rng.random_range(1..15)),
|
||||
format!("id < {}", rng.random_range(5..25)),
|
||||
"data IS NOT NULL".to_string(),
|
||||
],
|
||||
"t3" => vec![
|
||||
format!("category = {}", rng.random_range(1..5)),
|
||||
format!("amount > {}", rng.random_range(0..50)),
|
||||
format!("id <= {}", rng.random_range(1..20)),
|
||||
"amount IS NOT NULL".to_string(),
|
||||
],
|
||||
_ => vec!["1=1".to_string()],
|
||||
};
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
};
|
||||
|
||||
// Helper function to generate correlated WHERE conditions
|
||||
let gen_correlated_where =
|
||||
|rng: &mut ChaCha8Rng, inner_table: &str, outer_table: &str| -> String {
|
||||
match (outer_table, inner_table) {
|
||||
("t1", "t2") => {
|
||||
// t2.ref_id relates to t1.id
|
||||
let conditions = [
|
||||
format!("{inner_table}.ref_id = {outer_table}.id"),
|
||||
format!("{inner_table}.id < {outer_table}.value1"),
|
||||
format!("{inner_table}.data > {outer_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t1", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.category < {outer_table}.value1"),
|
||||
format!("{inner_table}.amount > {outer_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.ref_id"),
|
||||
format!("{inner_table}.value1 > {outer_table}.data"),
|
||||
format!("{inner_table}.value2 < {outer_table}.id"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.category = {outer_table}.ref_id"),
|
||||
format!("{inner_table}.amount > {outer_table}.data"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.value1 > {outer_table}.category"),
|
||||
format!("{inner_table}.value2 < {outer_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t2") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.ref_id = {outer_table}.category"),
|
||||
format!("{inner_table}.data < {outer_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
_ => "1=1".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let subquery_types = [
|
||||
// Simple scalar subqueries - single column only for safe nesting
|
||||
"SELECT MAX(amount) FROM t3".to_string(),
|
||||
"SELECT MIN(value1) FROM t1".to_string(),
|
||||
"SELECT COUNT(*) FROM t2".to_string(),
|
||||
"SELECT AVG(amount) FROM t3".to_string(),
|
||||
"SELECT id FROM t1".to_string(),
|
||||
"SELECT ref_id FROM t2".to_string(),
|
||||
"SELECT category FROM t3".to_string(),
|
||||
// Subqueries with WHERE - single column only
|
||||
format!(
|
||||
"SELECT MAX(amount) FROM t3 WHERE {}",
|
||||
gen_simple_where_inner(rng, "t3")
|
||||
),
|
||||
format!(
|
||||
"SELECT value1 FROM t1 WHERE {}",
|
||||
gen_simple_where_inner(rng, "t1")
|
||||
),
|
||||
format!(
|
||||
"SELECT ref_id FROM t2 WHERE {}",
|
||||
gen_simple_where_inner(rng, "t2")
|
||||
),
|
||||
];
|
||||
|
||||
let base_query = &subquery_types[rng.random_range(0..subquery_types.len())];
|
||||
|
||||
// Add correlated conditions if outer_table is provided and sometimes
|
||||
let final_query = if let Some(outer_table) = outer_table {
|
||||
if rng.random_bool(0.4) {
|
||||
// 40% chance for correlation
|
||||
// Extract the inner table from the base query
|
||||
let inner_table = if base_query.contains("FROM t1") {
|
||||
"t1"
|
||||
} else if base_query.contains("FROM t2") {
|
||||
"t2"
|
||||
} else if base_query.contains("FROM t3") {
|
||||
"t3"
|
||||
} else {
|
||||
return base_query.clone(); // fallback
|
||||
};
|
||||
|
||||
let correlated_condition = gen_correlated_where(rng, inner_table, outer_table);
|
||||
|
||||
if base_query.contains("WHERE") {
|
||||
format!("{base_query} AND {correlated_condition}")
|
||||
} else {
|
||||
format!("{base_query} WHERE {correlated_condition}")
|
||||
}
|
||||
} else {
|
||||
base_query.clone()
|
||||
}
|
||||
} else {
|
||||
base_query.clone()
|
||||
};
|
||||
|
||||
// Sometimes add nesting - but use scalar subquery for nesting to avoid column count issues
|
||||
if depth < 1 && rng.random_bool(0.2) {
|
||||
// Reduced probability and depth
|
||||
let nested = gen_scalar_subquery(rng, 0, outer_table);
|
||||
if final_query.contains("WHERE") {
|
||||
format!("{final_query} AND id IN ({nested})")
|
||||
} else {
|
||||
format!("{final_query} WHERE id IN ({nested})")
|
||||
}
|
||||
} else {
|
||||
final_query
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to generate scalar subquery (single column only)
|
||||
fn gen_scalar_subquery(
|
||||
rng: &mut ChaCha8Rng,
|
||||
depth: usize,
|
||||
outer_table: Option<&str>,
|
||||
) -> String {
|
||||
if depth > MAX_SUBQUERY_DEPTH {
|
||||
// Reduced nesting depth
|
||||
return "SELECT 1".to_string();
|
||||
}
|
||||
|
||||
let gen_simple_where_inner = |rng: &mut ChaCha8Rng, table: &str| -> String {
|
||||
let conditions = match table {
|
||||
"t1" => vec![
|
||||
format!("value1 > {}", rng.random_range(-5..15)),
|
||||
format!("value2 < {}", rng.random_range(-5..15)),
|
||||
format!("id <= {}", rng.random_range(1..20)),
|
||||
"value1 IS NOT NULL".to_string(),
|
||||
],
|
||||
"t2" => vec![
|
||||
format!("data > {}", rng.random_range(-3..8)),
|
||||
format!("ref_id = {}", rng.random_range(1..15)),
|
||||
format!("id < {}", rng.random_range(5..25)),
|
||||
"data IS NOT NULL".to_string(),
|
||||
],
|
||||
"t3" => vec![
|
||||
format!("category = {}", rng.random_range(1..5)),
|
||||
format!("amount > {}", rng.random_range(0..50)),
|
||||
format!("id <= {}", rng.random_range(1..20)),
|
||||
"amount IS NOT NULL".to_string(),
|
||||
],
|
||||
_ => vec!["1=1".to_string()],
|
||||
};
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
};
|
||||
|
||||
// Helper function to generate correlated WHERE conditions
|
||||
let gen_correlated_where =
|
||||
|rng: &mut ChaCha8Rng, inner_table: &str, outer_table: &str| -> String {
|
||||
match (outer_table, inner_table) {
|
||||
("t1", "t2") => {
|
||||
// t2.ref_id relates to t1.id
|
||||
let conditions = [
|
||||
format!("{inner_table}.ref_id = {outer_table}.id"),
|
||||
format!("{inner_table}.id < {outer_table}.value1"),
|
||||
format!("{inner_table}.data > {outer_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t1", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.category < {outer_table}.value1"),
|
||||
format!("{inner_table}.amount > {outer_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.ref_id"),
|
||||
format!("{inner_table}.value1 > {outer_table}.data"),
|
||||
format!("{inner_table}.value2 < {outer_table}.id"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.category = {outer_table}.ref_id"),
|
||||
format!("{inner_table}.amount > {outer_table}.data"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.value1 > {outer_table}.category"),
|
||||
format!("{inner_table}.value2 < {outer_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t2") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {outer_table}.id"),
|
||||
format!("{inner_table}.ref_id = {outer_table}.category"),
|
||||
format!("{inner_table}.data < {outer_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
_ => "1=1".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let scalar_subquery_types = [
|
||||
// Only scalar subqueries - single column only
|
||||
"SELECT MAX(amount) FROM t3".to_string(),
|
||||
"SELECT MIN(value1) FROM t1".to_string(),
|
||||
"SELECT COUNT(*) FROM t2".to_string(),
|
||||
"SELECT AVG(amount) FROM t3".to_string(),
|
||||
"SELECT id FROM t1".to_string(),
|
||||
"SELECT ref_id FROM t2".to_string(),
|
||||
"SELECT category FROM t3".to_string(),
|
||||
// Scalar subqueries with WHERE
|
||||
format!(
|
||||
"SELECT MAX(amount) FROM t3 WHERE {}",
|
||||
gen_simple_where_inner(rng, "t3")
|
||||
),
|
||||
format!(
|
||||
"SELECT value1 FROM t1 WHERE {}",
|
||||
gen_simple_where_inner(rng, "t1")
|
||||
),
|
||||
format!(
|
||||
"SELECT ref_id FROM t2 WHERE {}",
|
||||
gen_simple_where_inner(rng, "t2")
|
||||
),
|
||||
];
|
||||
|
||||
let base_query =
|
||||
&scalar_subquery_types[rng.random_range(0..scalar_subquery_types.len())];
|
||||
|
||||
// Add correlated conditions if outer_table is provided and sometimes
|
||||
let final_query = if let Some(outer_table) = outer_table {
|
||||
if rng.random_bool(0.4) {
|
||||
// 40% chance for correlation
|
||||
// Extract the inner table from the base query
|
||||
let inner_table = if base_query.contains("FROM t1") {
|
||||
"t1"
|
||||
} else if base_query.contains("FROM t2") {
|
||||
"t2"
|
||||
} else if base_query.contains("FROM t3") {
|
||||
"t3"
|
||||
} else {
|
||||
return base_query.clone(); // fallback
|
||||
};
|
||||
|
||||
let correlated_condition = gen_correlated_where(rng, inner_table, outer_table);
|
||||
|
||||
if base_query.contains("WHERE") {
|
||||
format!("{base_query} AND {correlated_condition}")
|
||||
} else {
|
||||
format!("{base_query} WHERE {correlated_condition}")
|
||||
}
|
||||
} else {
|
||||
base_query.clone()
|
||||
}
|
||||
} else {
|
||||
base_query.clone()
|
||||
};
|
||||
|
||||
// Sometimes add nesting
|
||||
if depth < 1 && rng.random_bool(0.2) {
|
||||
// Reduced probability and depth
|
||||
let nested = gen_scalar_subquery(rng, depth + 1, outer_table);
|
||||
if final_query.contains("WHERE") {
|
||||
format!("{final_query} AND id IN ({nested})")
|
||||
} else {
|
||||
format!("{final_query} WHERE id IN ({nested})")
|
||||
}
|
||||
} else {
|
||||
final_query
|
||||
}
|
||||
}
|
||||
|
||||
for iter_num in 0..NUM_FUZZ_ITERATIONS {
|
||||
let main_table = ["t1", "t2", "t3"][rng.random_range(0..3)];
|
||||
|
||||
let query_type = rng.random_range(0..6); // Increased from 4 to 6 for new correlated query types
|
||||
let query = match query_type {
|
||||
0 => {
|
||||
// Comparison subquery: WHERE column <op> (SELECT ...)
|
||||
let column = match main_table {
|
||||
"t1" => ["value1", "value2", "id"][rng.random_range(0..3)],
|
||||
"t2" => ["data", "ref_id", "id"][rng.random_range(0..3)],
|
||||
"t3" => ["amount", "category", "id"][rng.random_range(0..3)],
|
||||
_ => "id",
|
||||
};
|
||||
let op = [">", "<", ">=", "<=", "=", "<>"][rng.random_range(0..6)];
|
||||
let subquery = gen_scalar_subquery(&mut rng, 0, Some(main_table));
|
||||
format!("SELECT * FROM {main_table} WHERE {column} {op} ({subquery})",)
|
||||
}
|
||||
1 => {
|
||||
// EXISTS subquery: WHERE [NOT] EXISTS (SELECT ...)
|
||||
let not_exists = if rng.random_bool(0.3) { "NOT " } else { "" };
|
||||
let subquery = gen_subquery(&mut rng, 0, Some(main_table));
|
||||
format!("SELECT * FROM {main_table} WHERE {not_exists}EXISTS ({subquery})",)
|
||||
}
|
||||
2 => {
|
||||
// IN subquery with single column: WHERE column [NOT] IN (SELECT ...)
|
||||
let not_in = if rng.random_bool(0.3) { "NOT " } else { "" };
|
||||
let column = match main_table {
|
||||
"t1" => ["value1", "value2", "id"][rng.random_range(0..3)],
|
||||
"t2" => ["data", "ref_id", "id"][rng.random_range(0..3)],
|
||||
"t3" => ["amount", "category", "id"][rng.random_range(0..3)],
|
||||
_ => "id",
|
||||
};
|
||||
let subquery = gen_scalar_subquery(&mut rng, 0, Some(main_table));
|
||||
format!("SELECT * FROM {main_table} WHERE {column} {not_in}IN ({subquery})",)
|
||||
}
|
||||
3 => {
|
||||
// IN subquery with tuple: WHERE (col1, col2) [NOT] IN (SELECT col1, col2 ...)
|
||||
let not_in = if rng.random_bool(0.3) { "NOT " } else { "" };
|
||||
let (columns, sub_columns) = match main_table {
|
||||
"t1" => {
|
||||
if rng.random_bool(0.5) {
|
||||
("(id, value1)", "SELECT id, value1 FROM t1")
|
||||
} else {
|
||||
("id", "SELECT id FROM t1")
|
||||
}
|
||||
}
|
||||
"t2" => {
|
||||
if rng.random_bool(0.5) {
|
||||
("(ref_id, data)", "SELECT ref_id, data FROM t2")
|
||||
} else {
|
||||
("ref_id", "SELECT ref_id FROM t2")
|
||||
}
|
||||
}
|
||||
"t3" => {
|
||||
if rng.random_bool(0.5) {
|
||||
("(id, category)", "SELECT id, category FROM t3")
|
||||
} else {
|
||||
("id", "SELECT id FROM t3")
|
||||
}
|
||||
}
|
||||
_ => ("id", "SELECT id FROM t1"),
|
||||
};
|
||||
let subquery = if rng.random_bool(0.5) {
|
||||
sub_columns.to_string()
|
||||
} else {
|
||||
let base = sub_columns;
|
||||
let table_for_where = base.split("FROM ").nth(1).unwrap_or("t1");
|
||||
format!(
|
||||
"{} WHERE {}",
|
||||
base,
|
||||
gen_simple_where(&mut rng, table_for_where)
|
||||
)
|
||||
};
|
||||
format!("SELECT * FROM {main_table} WHERE {columns} {not_in}IN ({subquery})",)
|
||||
}
|
||||
4 => {
|
||||
// Correlated EXISTS subquery: WHERE [NOT] EXISTS (SELECT ... WHERE correlation)
|
||||
let not_exists = if rng.random_bool(0.3) { "NOT " } else { "" };
|
||||
|
||||
// Choose a different table for the subquery to ensure correlation is meaningful
|
||||
let inner_tables = match main_table {
|
||||
"t1" => ["t2", "t3"],
|
||||
"t2" => ["t1", "t3"],
|
||||
"t3" => ["t1", "t2"],
|
||||
_ => ["t1", "t2"],
|
||||
};
|
||||
let inner_table = inner_tables[rng.random_range(0..inner_tables.len())];
|
||||
|
||||
// Generate correlated condition
|
||||
let correlated_condition = match (main_table, inner_table) {
|
||||
("t1", "t2") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.ref_id = {main_table}.id"),
|
||||
format!("{inner_table}.id < {main_table}.value1"),
|
||||
format!("{inner_table}.data > {main_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t1", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.category < {main_table}.value1"),
|
||||
format!("{inner_table}.amount > {main_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.ref_id"),
|
||||
format!("{inner_table}.value1 > {main_table}.data"),
|
||||
format!("{inner_table}.value2 < {main_table}.id"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.category = {main_table}.ref_id"),
|
||||
format!("{inner_table}.amount > {main_table}.data"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.value1 > {main_table}.category"),
|
||||
format!("{inner_table}.value2 < {main_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t2") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.ref_id = {main_table}.category"),
|
||||
format!("{inner_table}.data < {main_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
_ => "1=1".to_string(),
|
||||
};
|
||||
|
||||
format!(
|
||||
"SELECT * FROM {main_table} WHERE {not_exists}EXISTS (SELECT 1 FROM {inner_table} WHERE {correlated_condition})",
|
||||
)
|
||||
}
|
||||
5 => {
|
||||
// Correlated comparison subquery: WHERE column <op> (SELECT ... WHERE correlation)
|
||||
let column = match main_table {
|
||||
"t1" => ["value1", "value2", "id"][rng.random_range(0..3)],
|
||||
"t2" => ["data", "ref_id", "id"][rng.random_range(0..3)],
|
||||
"t3" => ["amount", "category", "id"][rng.random_range(0..3)],
|
||||
_ => "id",
|
||||
};
|
||||
let op = [">", "<", ">=", "<=", "=", "<>"][rng.random_range(0..6)];
|
||||
|
||||
// Choose a different table for the subquery
|
||||
let inner_tables = match main_table {
|
||||
"t1" => ["t2", "t3"],
|
||||
"t2" => ["t1", "t3"],
|
||||
"t3" => ["t1", "t2"],
|
||||
_ => ["t1", "t2"],
|
||||
};
|
||||
let inner_table = inner_tables[rng.random_range(0..inner_tables.len())];
|
||||
|
||||
// Choose what to select from inner table
|
||||
let select_column = match inner_table {
|
||||
"t1" => ["value1", "value2", "id"][rng.random_range(0..3)],
|
||||
"t2" => ["data", "ref_id", "id"][rng.random_range(0..3)],
|
||||
"t3" => ["amount", "category", "id"][rng.random_range(0..3)],
|
||||
_ => "id",
|
||||
};
|
||||
|
||||
// Generate correlated condition
|
||||
let correlated_condition = match (main_table, inner_table) {
|
||||
("t1", "t2") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.ref_id = {main_table}.id"),
|
||||
format!("{inner_table}.id < {main_table}.value1"),
|
||||
format!("{inner_table}.data > {main_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t1", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.category < {main_table}.value1"),
|
||||
format!("{inner_table}.amount > {main_table}.value2"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.ref_id"),
|
||||
format!("{inner_table}.value1 > {main_table}.data"),
|
||||
format!("{inner_table}.value2 < {main_table}.id"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t2", "t3") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.category = {main_table}.ref_id"),
|
||||
format!("{inner_table}.amount > {main_table}.data"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t1") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.value1 > {main_table}.category"),
|
||||
format!("{inner_table}.value2 < {main_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
("t3", "t2") => {
|
||||
let conditions = [
|
||||
format!("{inner_table}.id = {main_table}.id"),
|
||||
format!("{inner_table}.ref_id = {main_table}.category"),
|
||||
format!("{inner_table}.data < {main_table}.amount"),
|
||||
];
|
||||
conditions[rng.random_range(0..conditions.len())].clone()
|
||||
}
|
||||
_ => "1=1".to_string(),
|
||||
};
|
||||
|
||||
format!(
|
||||
"SELECT * FROM {main_table} WHERE {column} {op} (SELECT {select_column} FROM {inner_table} WHERE {correlated_condition})",
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"Iteration {}/{NUM_FUZZ_ITERATIONS}: Query: {query}",
|
||||
iter_num + 1,
|
||||
);
|
||||
|
||||
let limbo_results = limbo_exec_rows(&db, &limbo_conn, &query);
|
||||
let sqlite_results = sqlite_exec_rows(&sqlite_conn, &query);
|
||||
|
||||
// Check if results match
|
||||
if limbo_results.len() != sqlite_results.len() {
|
||||
panic!(
|
||||
"Row count mismatch for query: {}\nLimbo: {} rows, SQLite: {} rows\nLimbo: {:?}\nSQLite: {:?}\nSeed: {}\n\n DDL/DML to reproduce manually:\n{}",
|
||||
query, limbo_results.len(), sqlite_results.len(), limbo_results, sqlite_results, seed, debug_ddl_dml_string
|
||||
);
|
||||
}
|
||||
|
||||
// Check if all rows match (order might be different)
|
||||
// Since Value doesn't implement Ord, we'll check containment both ways
|
||||
let all_limbo_in_sqlite = limbo_results.iter().all(|limbo_row| {
|
||||
sqlite_results
|
||||
.iter()
|
||||
.any(|sqlite_row| limbo_row == sqlite_row)
|
||||
});
|
||||
let all_sqlite_in_limbo = sqlite_results.iter().all(|sqlite_row| {
|
||||
limbo_results
|
||||
.iter()
|
||||
.any(|limbo_row| sqlite_row == limbo_row)
|
||||
});
|
||||
|
||||
if !all_limbo_in_sqlite || !all_sqlite_in_limbo {
|
||||
panic!(
|
||||
"Results mismatch for query: {query}\nLimbo: {limbo_results:?}\nSQLite: {sqlite_results:?}\nSeed: {seed}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user