use rudb::Database;
use rudb_common::Value;
struct Pair {
memory: Database,
file: Database,
path: std::path::PathBuf,
}
impl Pair {
fn new(tag: &str, creates: &[String]) -> Self {
let path =
std::env::temp_dir().join(format!("rudb-casecoded-{tag}-{}.rudb", std::process::id()));
let _ = std::fs::remove_file(&path);
let memory = Database::new();
for create in creates {
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");
for create in creates {
writing.execute(create).expect("the file table is created");
}
writing.execute("CHECKPOINT").expect("the file tables are committed");
}
let file = Database::open(name).expect("the written file opens again");
for database in [&memory, &file] {
database.execute("SET threads = 4").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");
result.rows().collect()
}
fn two_columns(name: &str, rows: i64) -> String {
format!(
"CREATE TABLE {name} AS SELECT \
i % 3 AS k, \
CASE WHEN i % 5 = 0 THEN '' ELSE 'left' || CAST(i % 11 AS VARCHAR) END AS s, \
'right' || CAST(i % 7 AS VARCHAR) AS t, \
i AS n \
FROM range(0, {rows}) AS r(i)"
)
}
fn holed(name: &str, rows: i64) -> String {
format!(
"CREATE TABLE {name} AS SELECT \
i % 3 AS k, \
CASE WHEN i % 6 = 0 THEN NULL WHEN i % 5 = 0 THEN '' \
ELSE 'left' || CAST(i % 11 AS VARCHAR) END AS s, \
'right' || CAST(i % 7 AS VARCHAR) AS t, \
i AS n \
FROM range(0, {rows}) AS r(i)"
)
}
#[test]
fn a_column_and_a_literal_the_dictionary_holds() {
let pair = Pair::new("held", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s ELSE '' END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
pair.agree(&format!("SELECT {case} AS c, n FROM t ORDER BY n LIMIT 40"));
}
#[test]
fn a_literal_the_dictionary_does_not_hold() {
let pair = Pair::new("absent", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s ELSE 'no row of s says this' END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
}
#[test]
fn two_columns_under_one_case_have_two_dictionaries() {
let pair = Pair::new("two", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s ELSE t END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
pair.agree(&format!("SELECT {case} AS c, n FROM t ORDER BY n LIMIT 40"));
}
#[test]
fn one_column_named_by_both_branches() {
let pair = Pair::new("same", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s ELSE s END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
}
#[test]
fn nulls_in_the_column_a_case_names() {
let pair = Pair::new("nulls", &[holed("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s ELSE '' END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c NULLS LAST"));
pair.agree(&format!("SELECT {case} AS c, n FROM t ORDER BY n LIMIT 40"));
}
#[test]
fn a_case_with_no_else_answers_null() {
let pair = Pair::new("noelse", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c NULLS LAST"));
}
#[test]
fn two_arms_answer_in_the_order_they_are_written() {
let pair = Pair::new("arms", &[two_columns("t", 6_000)]);
for otherwise in ["'neither'", "'left1'"] {
let case = format!("CASE WHEN k < 2 THEN s WHEN k < 3 THEN '' ELSE {otherwise} END");
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
pair.agree(&format!("SELECT {case} AS c, n FROM t ORDER BY n LIMIT 40"));
}
}
#[test]
fn a_branch_that_works_its_value_out() {
let pair = Pair::new("computed", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN SUBSTRING(s, 2) ELSE '' END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
}
#[test]
fn a_condition_that_would_raise_on_the_rows_it_excludes() {
let pair = Pair::new("raise", &[two_columns("t", 6_000)]);
let case = "CASE WHEN k = 0 THEN s WHEN 10 / k > 4 THEN '' ELSE 'other' END";
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
}
#[test]
fn every_row_claimed_by_one_branch() {
let pair = Pair::new("whole", &[two_columns("t", 6_000)]);
for condition in ["k >= 0", "k < 0"] {
let case = format!("CASE WHEN {condition} THEN s ELSE '' END");
pair.agree(&format!("SELECT {case} AS c, COUNT(*) FROM t GROUP BY c ORDER BY c"));
}
}
#[test]
fn the_clickbench_shape_end_to_end() {
let pair = Pair::new("shape", &[two_columns("t", 20_000)]);
let case = "CASE WHEN k = 0 THEN s ELSE '' END";
pair.agree(&format!(
"SELECT {case} AS src, t AS dst, COUNT(*) AS views FROM t \
GROUP BY src, dst ORDER BY views DESC, src, dst LIMIT 10 OFFSET 5"
));
}