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-dense-{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 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 the_same_either_way(&self, query: &str) {
for threads in [1, 8] {
let set = format!("SET threads = {threads}");
self.memory.execute(&set).expect("sets the thread count");
self.file.execute(&set).expect("sets the thread count");
self.file.execute("SET disabled_optimizers = ''").expect("clears the disabled list");
let with = rows(&self.file, query);
assert!(!with.is_empty(), "the query has to produce rows to be worth comparing");
let wanted = rows(&self.memory, query);
assert_eq!(with, wanted, "the file and memory disagree about {query} at {threads}");
self.file
.execute("SET disabled_optimizers = 'aggregate_dense'")
.expect("the pass answers to its name");
let without = rows(&self.file, query);
assert_eq!(with, without, "the pass changed the answer to {query} at {threads}");
}
}
}
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()
}
#[test]
fn the_pass_is_one_the_database_can_name() {
assert!(rudb::optimizers().contains(&"aggregate_dense"));
}
#[test]
fn a_dense_key_groups_the_same_as_it_did_without_a_range() {
let pair = Pair::new("dense", "SELECT i % 300 AS k, i AS v FROM range(0, 40000) AS r(i)");
pair.the_same_either_way("SELECT k, COUNT(*), SUM(v) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_null_key_is_still_a_group_of_its_own() {
let pair = Pair::new(
"nulls",
"SELECT CASE WHEN i % 37 = 0 THEN NULL ELSE i % 300 END AS k, i AS v
FROM range(0, 40000) AS r(i)",
);
pair.the_same_either_way("SELECT k, COUNT(*), SUM(v) FROM t GROUP BY k ORDER BY k");
let groups = rows(&pair.file, "SELECT COUNT(*) FROM (SELECT k FROM t GROUP BY k)");
assert_eq!(groups, vec![vec![Value::BigInt(301)]], "the null key and the three hundred values");
}
#[test]
fn a_key_that_runs_below_zero_groups_the_same() {
let pair =
Pair::new("signed", "SELECT (i % 601) - 300 AS k, i AS v FROM range(0, 40000) AS r(i)");
pair.the_same_either_way("SELECT k, COUNT(*), SUM(v) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_key_of_one_value_groups_the_same() {
let pair = Pair::new("single", "SELECT 7 AS k, i AS v FROM range(0, 10000) AS r(i)");
pair.the_same_either_way("SELECT k, COUNT(*), SUM(v) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_filter_under_the_aggregate_changes_nothing() {
let pair = Pair::new("filtered", "SELECT i % 300 AS k, i AS v FROM range(0, 40000) AS r(i)");
pair.the_same_either_way("SELECT k, COUNT(*) FROM t WHERE k > 200 GROUP BY k ORDER BY k");
}
#[test]
fn a_key_wide_enough_to_partition_groups_the_same() {
let pair = Pair::new("wide", "SELECT i % 20000 AS k, i AS v FROM range(0, 200000) AS r(i)");
pair.the_same_either_way("SELECT k, COUNT(*), SUM(v) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_key_of_two_columns_groups_the_same() {
let pair = Pair::new("pair", "SELECT i % 300 AS k, i AS v FROM range(0, 40000) AS r(i)");
pair.the_same_either_way("SELECT k, v % 7, COUNT(*) FROM t GROUP BY k, v % 7 ORDER BY k, 2");
}