use rudb::Database;
use rudb_common::Value;
struct Pair {
memory: Database,
file: Database,
path: std::path::PathBuf,
}
impl Pair {
fn new(tag: &str, select: &str) -> Self {
let path =
std::env::temp_dir().join(format!("rudb-pruned-{tag}-{}.rudb", std::process::id()));
let _ = std::fs::remove_file(&path);
let create = format!("CREATE TABLE t AS {select}");
let memory = Database::new();
memory.execute(&create).expect("the memory table is created");
let off = rudb::optimizers().join(",");
memory
.execute(&format!("SET disabled_optimizers = '{off}'"))
.expect("every pass answers to its name");
let name = path.to_str().expect("a UTF-8 temporary path");
{
let writing = Database::open(name).expect("a file name starts a native database");
writing.execute(&create).expect("the file table is created");
writing.execute("CHECKPOINT").expect("the file table is committed");
}
let file = Database::open(name).expect("the written file opens again");
Self { memory, file, path }
}
fn agree(&self, query: &str) {
for threads in [1, 4] {
let set = format!("SET threads = {threads}");
self.memory.execute(&set).expect("sets the thread count");
self.file.execute(&set).expect("sets the thread count");
let wanted = rows(&self.memory, query);
let got = rows(&self.file, query);
assert_eq!(got, wanted, "the file and memory disagree about {query} at {threads}");
}
}
fn gives(&self, query: &str, wanted: &[i64]) {
self.agree(query);
let got: Vec<i64> = rows(&self.file, query)
.into_iter()
.map(|row| match row.first() {
Some(&Value::BigInt(value)) => value,
other => panic!("{query} answered {other:?} where a BIGINT was wanted"),
})
.collect();
assert_eq!(got, wanted, "{query}");
}
}
impl Drop for Pair {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn rows(database: &Database, query: &str) -> Vec<Vec<Value>> {
let result = database.query(query).expect("the query ran");
(0..result.len())
.map(|row| (0..result.width()).map(|column| result.value_at(row, column)).collect())
.collect()
}
fn climbing(rows: i64) -> String {
let ends = format!("CASE WHEN i < 2 OR i >= {} THEN 1 ELSE 0 END", rows - 2);
format!("SELECT i, (i * 7919) % 1000003 AS j, {ends} AS k FROM range(0, {rows}) AS r(i)")
}
const ROWS: i64 = 300_000;
#[test]
fn a_filter_that_rules_out_nothing_still_reads_every_row() {
let pair = Pair::new("all", &climbing(ROWS));
pair.agree("SELECT COUNT(*), MIN(i), MAX(i), SUM(i) FROM t WHERE i >= 0");
}
#[test]
fn a_filter_that_rules_out_every_part_answers_nothing() {
let pair = Pair::new("none", &climbing(ROWS));
pair.gives("SELECT i FROM t WHERE i = -1", &[]);
pair.agree("SELECT COUNT(*) FROM t WHERE i = -1");
}
#[test]
fn survivors_at_the_very_front_are_not_walked_past() {
let pair = Pair::new("front", &climbing(ROWS));
pair.gives("SELECT i FROM t WHERE i < 3 ORDER BY i", &[0, 1, 2]);
}
#[test]
fn survivors_at_the_very_back_are_reached() {
let pair = Pair::new("back", &climbing(ROWS));
let last = ROWS - 1;
pair.gives(&format!("SELECT i FROM t WHERE i > {} ORDER BY i", last - 2), &[last - 1, last]);
}
#[test]
fn survivors_in_the_middle_are_reached_with_ruled_out_parts_on_both_sides() {
let middle = ROWS / 2;
let pair = Pair::new("middle", &climbing(ROWS));
pair.gives(
&format!("SELECT i FROM t WHERE i >= {middle} AND i < {} ORDER BY i", middle + 3),
&[middle, middle + 1, middle + 2],
);
}
#[test]
fn survivors_scattered_through_nearly_every_part_are_all_found() {
let pair = Pair::new("scattered", &climbing(ROWS));
pair.agree("SELECT COUNT(*), MIN(i), MAX(i), SUM(i) FROM t WHERE j < 40");
}
#[test]
fn two_ranges_far_apart_are_both_found() {
let pair = Pair::new("two", &climbing(ROWS));
let far = ROWS - 2;
pair.gives("SELECT i FROM t WHERE k = 1 ORDER BY i", &[0, 1, far, far + 1]);
}
#[test]
fn a_single_row_deep_inside_a_morsel_is_reached_by_walking_to_it() {
let pair = Pair::new("deep", &climbing(ROWS));
let row = 200_000;
let wanted = (row * 7919) % 1_000_003;
pair.gives(&format!("SELECT i FROM t WHERE j = {wanted}"), &[row]);
}
#[test]
fn an_answer_over_ruled_out_parts_agrees_however_many_workers_split_them() {
let pair = Pair::new("workers", &climbing(ROWS));
let from = ROWS / 2;
let query = format!(
"SELECT COUNT(*), MIN(i), MAX(i) FROM t WHERE i >= {from} AND i < {}",
from + 50_000
);
let wanted = rows(&pair.memory, &query);
for threads in [1, 2, 4, 8] {
pair.file.execute(&format!("SET threads = {threads}")).expect("sets the thread count");
assert_eq!(rows(&pair.file, &query), wanted, "at {threads} workers");
}
}