Add TCL/differential fuzz tests for verifying index scan behavior

This commit is contained in:
Jussi Saurio 2025-04-08 10:15:06 +03:00
parent ed929d2a0a
commit 431ef2fa6a
2 changed files with 215 additions and 2 deletions

View file

@ -141,3 +141,62 @@ Collin|15}
do_execsql_test case-insensitive-alias {
select u.first_name as fF, count(1) > 0 as cC from users u where fF = 'Jamie' group by fF order by cC;
} {Jamie|1}
do_execsql_test age_idx_order_desc {
select first_name from users order by age desc limit 3;
} {Robert
Sydney
Matthew}
do_execsql_test rowid_or_integer_pk_desc {
select first_name from users order by id desc limit 3;
} {Nicole
Gina
Dorothy}
# These two following tests may seem dumb but they verify that index scanning by age_idx doesn't drop any rows due to BTree bugs
do_execsql_test orderby_asc_verify_rows {
select count(1) from (select * from users order by age desc)
} {10000}
do_execsql_test orderby_desc_verify_rows {
select count(1) from (select * from users order by age desc)
} {10000}
do_execsql_test orderby_desc_with_offset {
select first_name, age from users order by age desc limit 3 offset 666;
} {Francis|94
Matthew|94
Theresa|94}
do_execsql_test orderby_desc_with_filter {
select first_name, age from users where age <= 50 order by age desc limit 5;
} {Gerald|50
Nicole|50
Tammy|50
Marissa|50
Daniel|50}
do_execsql_test orderby_asc_with_filter_range {
select first_name, age from users where age <= 50 and age >= 49 order by age asc limit 5;
} {William|49
Jennifer|49
Robert|49
David|49
Stephanie|49}
do_execsql_test orderby_desc_with_filter_id_lt {
select id from users where id < 6666 order by id desc limit 5;
} {6665
6664
6663
6662
6661}
do_execsql_test orderby_desc_with_filter_id_le {
select id from users where id <= 6666 order by id desc limit 5;
} {6666
6665
6664
6663
6662}

View file

@ -2,9 +2,9 @@ pub mod grammar_generator;
#[cfg(test)]
mod tests {
use std::rc::Rc;
use std::{collections::HashSet, rc::Rc};
use rand::SeedableRng;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use rusqlite::params;
@ -107,6 +107,160 @@ mod tests {
}
}
#[test]
pub fn index_scan_fuzz() {
let db = TempDatabase::new_with_rusqlite("CREATE TABLE t(x PRIMARY KEY)");
let sqlite_conn = rusqlite::Connection::open(db.path.clone()).unwrap();
let insert = format!(
"INSERT INTO t VALUES {}",
(0..10000)
.map(|x| format!("({})", x))
.collect::<Vec<_>>()
.join(", ")
);
sqlite_conn.execute(&insert, params![]).unwrap();
sqlite_conn.close().unwrap();
let sqlite_conn = rusqlite::Connection::open(db.path.clone()).unwrap();
let limbo_conn = db.connect_limbo();
const COMPARISONS: [&str; 5] = ["=", "<", "<=", ">", ">="];
const ORDER_BY: [Option<&str>; 4] = [
None,
Some("ORDER BY x"),
Some("ORDER BY x DESC"),
Some("ORDER BY x ASC"),
];
for comp in COMPARISONS.iter() {
for order_by in ORDER_BY.iter() {
for max in 0..=10000 {
let query = format!(
"SELECT * FROM t WHERE x {} {} {} LIMIT 3",
comp,
max,
order_by.unwrap_or(""),
);
let limbo = limbo_exec_rows(&db, &limbo_conn, &query);
let sqlite = sqlite_exec_rows(&sqlite_conn, &query);
assert_eq!(
limbo, sqlite,
"query: {}, limbo: {:?}, sqlite: {:?}",
query, limbo, sqlite
);
}
}
}
}
#[test]
pub fn index_scan_compound_key_fuzz() {
let (mut rng, seed) = if std::env::var("SEED").is_ok() {
let seed = std::env::var("SEED").unwrap().parse::<u64>().unwrap();
(ChaCha8Rng::seed_from_u64(seed), seed)
} else {
rng_from_time()
};
let db = TempDatabase::new_with_rusqlite("CREATE TABLE t(x, y, z, PRIMARY KEY (x, y))");
let sqlite_conn = rusqlite::Connection::open(db.path.clone()).unwrap();
let mut pk_tuples = HashSet::new();
while pk_tuples.len() < 100000 {
pk_tuples.insert((rng.random_range(0..3000), rng.random_range(0..3000)));
}
let mut tuples = Vec::new();
for pk_tuple in pk_tuples {
tuples.push(format!(
"({}, {}, {})",
pk_tuple.0,
pk_tuple.1,
rng.random_range(0..2000)
));
}
let insert = format!("INSERT INTO t VALUES {}", tuples.join(", "));
sqlite_conn.execute(&insert, params![]).unwrap();
sqlite_conn.close().unwrap();
let sqlite_conn = rusqlite::Connection::open(db.path.clone()).unwrap();
let limbo_conn = db.connect_limbo();
const COMPARISONS: [&str; 5] = ["=", "<", "<=", ">", ">="];
const ORDER_BY: [Option<&str>; 4] = [
None,
Some("ORDER BY x"),
Some("ORDER BY x DESC"),
Some("ORDER BY x ASC"),
];
let print_dump_on_fail = |insert: &str, seed: u64| {
let comment = format!("-- seed: {}; dump for manual debugging:", seed);
let pragma_journal_mode = "PRAGMA journal_mode = wal;";
let create_table = "CREATE TABLE t(x, y, z, PRIMARY KEY (x, y));";
let dump = format!(
"{}\n{}\n{}\n{}\n{}",
comment, pragma_journal_mode, create_table, comment, insert
);
println!("{}", dump);
};
for comp in COMPARISONS.iter() {
for order_by in ORDER_BY.iter() {
for max in 0..=3000 {
// see comment below about ordering and the '=' comparison operator; omitting LIMIT for that reason
// we mainly have LIMIT here for performance reasons but for = we want to get all the rows to ensure
// correctness in the = case
let limit = if *comp == "=" { "" } else { "LIMIT 5" };
let query = format!(
"SELECT * FROM t WHERE x {} {} {} {}",
comp,
max,
order_by.unwrap_or(""),
limit
);
log::trace!("query: {}", query);
let limbo = limbo_exec_rows(&db, &limbo_conn, &query);
let sqlite = sqlite_exec_rows(&sqlite_conn, &query);
let is_equal = limbo == sqlite;
if !is_equal {
// if the condition is = and the same rows are present but in different order, then we accept that
// e.g. sqlite doesn't bother iterating in reverse order if "WHERE X = 3 ORDER BY X DESC", but we currently do.
if *comp == "=" {
let limbo_row_count = limbo.len();
let sqlite_row_count = sqlite.len();
if limbo_row_count == sqlite_row_count {
for limbo_row in limbo.iter() {
if !sqlite.contains(limbo_row) {
// save insert to file and print the filename for debugging
let error_msg = format!("row not found in sqlite: query: {}, limbo: {:?}, sqlite: {:?}, seed: {}", query, limbo, sqlite, seed);
print_dump_on_fail(&insert, seed);
panic!("{}", error_msg);
}
}
for sqlite_row in sqlite.iter() {
if !limbo.contains(sqlite_row) {
let error_msg = format!("row not found in limbo: query: {}, limbo: {:?}, sqlite: {:?}, seed: {}", query, limbo, sqlite, seed);
print_dump_on_fail(&insert, seed);
panic!("{}", error_msg);
}
}
continue;
} else {
print_dump_on_fail(&insert, seed);
let error_msg = format!("row count mismatch (limbo: {}, sqlite: {}): query: {}, limbo: {:?}, sqlite: {:?}, seed: {}", limbo_row_count, sqlite_row_count, query, limbo, sqlite, seed);
panic!("{}", error_msg);
}
}
print_dump_on_fail(&insert, seed);
panic!(
"query: {}, limbo: {:?}, sqlite: {:?}, seed: {}",
query, limbo, sqlite, seed
);
}
}
}
}
}
#[test]
pub fn arithmetic_expression_fuzz() {
let _ = env_logger::try_init();