From 0b08f006d32cd45d87b797ce7fdc599ba2e5383a Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Mon, 27 Oct 2025 16:00:26 +0200 Subject: [PATCH] Add subquery fuzz test --- tests/fuzz/mod.rs | 681 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 681 insertions(+) diff --git a/tests/fuzz/mod.rs b/tests/fuzz/mod.rs index 9f06f20fc..9cfb06ea7 100644 --- a/tests/fuzz/mod.rs +++ b/tests/fuzz/mod.rs @@ -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 (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 (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}", + ); + } + } + } }