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, threads: usize) -> Self {
let path =
std::env::temp_dir().join(format!("rudb-ranked-{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");
for database in [&memory, &file] {
database.execute(&format!("SET threads = {threads}")).expect("sets the thread count");
}
Self { memory, file, path }
}
fn agree(&self, query: &str) {
let wanted = rows(&self.memory, query);
let got = rows(&self.file, query);
assert_eq!(got, wanted, "the file and memory disagree about {query}");
assert!(!wanted.is_empty(), "{query} answered nothing, so it proved nothing");
}
}
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 ordered(rows: i64, keys: i64) -> String {
format!("SELECT i % {keys} AS k, {SCRAMBLED} AS s FROM range(0, {rows}) AS r(i)")
}
const SCRAMBLED: &str = "CAST(1000000 + (i * 7919) % 999983 AS VARCHAR)";
#[test]
fn a_grouped_extreme_over_a_sorted_dictionary_is_the_one_the_bytes_give() {
let pair = Pair::new("plain", &ordered(20_000, 97), 4);
pair.agree("SELECT k, MIN(s), MAX(s) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_grouped_extreme_agrees_however_many_workers_split_the_rows() {
for threads in [1, 2, 8] {
let pair = Pair::new(&format!("threads{threads}"), &ordered(20_000, 401), threads);
pair.agree("SELECT k, MIN(s), MAX(s) FROM t GROUP BY k ORDER BY k");
}
}
#[test]
fn a_grouped_extreme_skips_the_nulls_the_way_memory_does() {
let select = "SELECT i % 53 AS k, \
CASE WHEN i % 7 = 0 THEN NULL \
ELSE CAST(1000000 + (i * 7919) % 999983 AS VARCHAR) END AS s \
FROM range(0, 20000) AS r(i)";
let pair = Pair::new("nulls", select, 4);
pair.agree("SELECT k, MIN(s), MAX(s) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_group_whose_rows_are_all_null_answers_null_rather_than_a_string() {
let select = "SELECT i % 7 AS k, \
CASE WHEN i % 7 = 0 THEN NULL \
ELSE CAST(1000000 + (i * 7919) % 999983 AS VARCHAR) END AS s \
FROM range(0, 20000) AS r(i)";
let pair = Pair::new("allnull", select, 4);
pair.agree("SELECT k, MIN(s), MAX(s) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn a_grouped_extreme_beside_the_other_aggregates_leaves_them_alone() {
let pair = Pair::new("mixed", &ordered(20_000, 97), 4);
pair.agree("SELECT k, COUNT(*), MIN(s), MAX(s), MIN(STRLEN(s)) FROM t GROUP BY k ORDER BY k");
}
#[test]
fn an_ungrouped_extreme_over_the_whole_column_is_the_one_the_bytes_give() {
let pair = Pair::new("whole", &ordered(20_000, 97), 4);
pair.agree("SELECT MIN(s), MAX(s) FROM t");
}
#[test]
fn an_ungrouped_extreme_over_a_filtered_column_is_the_one_the_bytes_give() {
let pair = Pair::new("wholefiltered", &ordered(20_000, 97), 4);
pair.agree("SELECT MIN(s), MAX(s) FROM t WHERE s > '1500000' AND s < '1900000'");
}
#[test]
fn a_grouped_extreme_over_a_filtered_column_is_the_one_the_bytes_give() {
let pair = Pair::new("filtered", &ordered(20_000, 97), 4);
pair.agree("SELECT k, MIN(s), MAX(s) FROM t WHERE s > '1500000' GROUP BY k ORDER BY k");
}