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-cluster-{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 explain(&self, query: &str) -> String {
let result = self.file.query(&format!("EXPLAIN {query}")).expect("the plan prints");
(0..result.len())
.flat_map(|row| (0..result.width()).map(move |column| (row, column)))
.map(|(row, column)| format!("{:?}", result.value_at(row, column)))
.collect::<Vec<_>>()
.join("\n")
}
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_cluster'")
.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}");
}
self.file.execute("SET disabled_optimizers = ''").expect("clears the disabled list");
}
}
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()
}
const CLOSED: &str = "groups closed in key order";
#[test]
fn the_pass_is_one_the_database_can_name() {
assert!(rudb::optimizers().contains(&"aggregate_cluster"));
}
#[test]
fn a_sorted_key_groups_the_same_as_it_did_without_the_order() {
let pair = Pair::new(
"sorted",
"SELECT i // 4 AS k, i AS v, i % 7 AS w, 'x' || (i % 13) AS s FROM range(0, 300000) AS r(i)",
);
let query = "SELECT k, COUNT(*), SUM(v), MIN(w), MAX(s), AVG(v) FROM t GROUP BY k ORDER BY k";
assert!(pair.explain(query).contains(CLOSED), "the pass did not fire on a sorted key");
pair.the_same_either_way(query);
}
#[test]
fn filters_before_and_after_leave_the_answer_alone() {
let pair =
Pair::new("filtered", "SELECT i // 5 AS k, i % 11 AS v FROM range(0, 300000) AS r(i)");
pair.the_same_either_way(
"SELECT k FROM t WHERE v <> 3 GROUP BY k HAVING SUM(v) > 30 ORDER BY k",
);
pair.the_same_either_way("SELECT k, SUM(v) FROM t WHERE v > 8 GROUP BY k ORDER BY k");
}
#[test]
fn a_unique_key_is_every_group_closed() {
let pair = Pair::new("unique", "SELECT i AS k, i * 3 AS v FROM range(0, 200000) AS r(i)");
pair.the_same_either_way("SELECT k, SUM(v) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn an_unsorted_key_is_left_alone() {
let pair = Pair::new("unsorted", "SELECT i % 1000 AS k, i AS v FROM range(0, 100000) AS r(i)");
let query = "SELECT k, SUM(v) FROM t GROUP BY k ORDER BY k";
assert!(!pair.explain(query).contains(CLOSED), "the pass fired on an unsorted key");
pair.the_same_either_way(query);
}