use rudb::Database;
use rudb_common::Value;
const CLOSE_ENOUGH: f64 = 1e-9;
const ROWS: &str = "INSERT INTO t SELECT (i % 100)::DECIMAL(15,2) / 100, \
(i % 9701)::DECIMAL(15,2) / 100, (i % 31) + 1000, (i % 17) + 1000 \
FROM range(20000) tbl(i)";
struct Pair {
memory: Database,
file: Database,
path: std::path::PathBuf,
}
impl Pair {
fn new(tag: &str) -> Self {
let path =
std::env::temp_dir().join(format!("rudb-encoded-{tag}-{}.rudb", std::process::id()));
let _ = std::fs::remove_file(&path);
let create = "CREATE TABLE t(cents DECIMAL(15,2), price DECIMAL(15,2), \
low BIGINT, high BIGINT)";
let memory = Database::new();
memory.execute(create).expect("the memory table is created");
memory.execute(ROWS).expect("the memory table is filled");
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(ROWS).expect("the file table is filled");
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) -> Value {
let wanted = self.memory.value(query).expect("the memory table answers");
let got = self.file.value(query).expect("the file answers");
match (&got, &wanted) {
(Value::Double(got), Value::Double(wanted)) => {
assert!(
(got - wanted).abs() <= wanted.abs() * CLOSE_ENOUGH,
"the file says {got} and memory says {wanted} for {query}"
);
}
_ => assert_eq!(got, wanted, "the file and memory disagree about {query}"),
}
got
}
fn number(&self, query: &str) -> f64 {
match self.agree(query) {
Value::Double(number) => number,
other => panic!("{query} answered with {other:?} rather than a double"),
}
}
}
impl Drop for Pair {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[track_caller]
fn about(got: f64, wanted: f64, what: &str) {
assert!((got - wanted).abs() <= wanted.abs() * CLOSE_ENOUGH, "{what} came back as {got}");
}
#[test]
fn an_aggregate_over_a_stored_column_of_numbers_is_the_answer_the_same_rows_give_in_memory() {
let pair = Pair::new("aggregate");
assert_eq!(pair.agree("SELECT count(*) FROM t"), Value::BigInt(20000));
assert_eq!(pair.agree("SELECT count(cents) FROM t"), Value::BigInt(20000));
match pair.agree("SELECT sum(cents) FROM t") {
Value::Decimal { unscaled, scale, .. } => {
assert_eq!((unscaled, scale), (990_000, 2), "the total of a stored decimal column");
}
other => panic!("a total came back as {other:?}"),
}
assert_eq!(
pair.agree("SELECT min(cents) FROM t"),
Value::Decimal { unscaled: 0, width: 15, scale: 2 }
);
assert_eq!(
pair.agree("SELECT max(cents) FROM t"),
Value::Decimal { unscaled: 99, width: 15, scale: 2 }
);
about(pair.number("SELECT avg(cents) FROM t"), 0.495, "the mean of a stored decimal column");
about(pair.number("SELECT avg(price) FROM t"), 47.139_101_5, "the mean of a stored price");
pair.agree("SELECT sum(price) FROM t");
pair.agree("SELECT min(price) FROM t");
pair.agree("SELECT max(price) FROM t");
pair.agree("SELECT sum(low) FROM t");
pair.agree("SELECT min(low) FROM t");
pair.agree("SELECT max(low) FROM t");
}
#[test]
fn the_mean_of_a_stored_decimal_column_is_its_total_divided_by_its_rows() {
let pair = Pair::new("mean");
for column in ["cents", "price"] {
let mean = pair.number(&format!("SELECT avg({column}) FROM t"));
let total = pair.number(&format!("SELECT sum({column})::DOUBLE FROM t"));
let rows = match pair.agree(&format!("SELECT count({column}) FROM t")) {
Value::BigInt(rows) => rows as f64,
other => panic!("a count came back as {other:?}"),
};
about(mean, total / rows, &format!("the mean of {column}"));
}
}
#[test]
fn a_filter_over_stored_columns_keeps_the_rows_the_same_filter_keeps_in_memory() {
let pair = Pair::new("filter");
pair.agree("SELECT count(*) FROM t WHERE cents < 0.50");
pair.agree("SELECT count(*) FROM t WHERE cents >= 0.50");
pair.agree("SELECT count(*) FROM t WHERE low < high");
pair.agree("SELECT count(*) FROM t WHERE low = high");
pair.agree("SELECT sum(cents) FROM t WHERE low < high");
pair.agree("SELECT sum(price) FROM t WHERE cents > 0.90 AND low < high");
}
#[test]
fn arithmetic_over_a_stored_decimal_column_is_what_the_same_arithmetic_gives_in_memory() {
let pair = Pair::new("arithmetic");
pair.agree("SELECT sum(1 - cents) FROM t");
pair.agree("SELECT sum(cents * 2) FROM t");
pair.agree("SELECT sum(price * (1 - cents)) FROM t");
pair.agree("SELECT sum(cents::DOUBLE) FROM t");
about(
pair.number("SELECT avg(price * (1 - cents)) FROM t"),
pair.number("SELECT sum(price * (1 - cents))::DOUBLE / count(*) FROM t"),
"the mean of a product of two stored decimal columns",
);
}
#[test]
fn a_grouped_aggregate_over_stored_columns_adds_up_to_what_it_adds_up_to_in_memory() {
let pair = Pair::new("grouped");
pair.agree("SELECT count(*) FROM (SELECT low FROM t GROUP BY low) g");
pair.agree("SELECT sum(total) FROM (SELECT low, sum(cents) AS total FROM t GROUP BY low) g");
pair.agree("SELECT sum(rows) FROM (SELECT low, count(*) AS rows FROM t GROUP BY low) g");
about(
pair.number("SELECT sum(mean) FROM (SELECT low, avg(cents) AS mean FROM t GROUP BY low) g"),
pair.number(
"SELECT sum(mean) FROM (SELECT low, sum(cents)::DOUBLE / count(*) AS mean \
FROM t GROUP BY low) g",
),
"the means of the groups of a stored decimal column",
);
}