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], threads: usize) -> Self {
let path =
std::env::temp_dir().join(format!("rudb-topcoded-{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(&format!("SET threads = {threads}")).expect("sets the thread count");
}
Self { memory, file, path }
}
fn agree(&self, query: &str) {
assert!(
self.file.plan(query).expect("binds").contains("TopN"),
"{query} is not a top N, so it proves nothing"
);
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()
}
const SCRAMBLED: &str = "CAST(1000000 + (i * 7919) % 999983 AS VARCHAR)";
fn ordered(name: &str, rows: i64, keys: i64) -> String {
format!(
"CREATE TABLE {name} AS SELECT i % {keys} AS k, {SCRAMBLED} AS s, i AS n \
FROM range(0, {rows}) AS r(i)"
)
}
fn holed(name: &str, rows: i64, keys: i64) -> String {
format!(
"CREATE TABLE {name} AS SELECT i % {keys} AS k, \
CASE WHEN i % 7 = 0 THEN NULL ELSE {SCRAMBLED} END AS s, i AS n \
FROM range(0, {rows}) AS r(i)"
)
}
#[test]
fn one_coded_key_gives_the_rows_the_strings_give() {
let pair = Pair::new("plain", &[ordered("t", 20_000, 97)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY s LIMIT 10");
}
#[test]
fn a_coded_key_read_backwards_gives_the_rows_the_strings_give() {
let pair = Pair::new("desc", &[ordered("t", 20_000, 97)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY s DESC LIMIT 10");
}
#[test]
fn a_coded_key_agrees_however_many_workers_split_the_rows() {
for threads in [1, 2, 8] {
let pair = Pair::new(&format!("threads{threads}"), &[ordered("t", 20_000, 401)], threads);
pair.agree("SELECT k, s, n FROM t ORDER BY s LIMIT 10");
}
}
#[test]
fn an_offset_past_the_sorted_bound_takes_the_batched_path() {
let pair = Pair::new("offset", &[ordered("t", 20_000, 97)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY s LIMIT 10 OFFSET 1000");
}
#[test]
fn a_coded_key_after_one_that_almost_every_row_ties() {
let pair = Pair::new("second", &[ordered("t", 20_000, 3)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY k, s LIMIT 10");
}
#[test]
fn a_coded_key_in_front_of_another_one() {
let pair = Pair::new("first", &[ordered("t", 20_000, 97)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY s, k LIMIT 10");
}
#[test]
fn nulls_in_a_coded_key_go_where_the_query_puts_them() {
let pair = Pair::new("nulls", &[holed("t", 20_000, 97)], 4);
for placement in ["NULLS FIRST", "NULLS LAST"] {
for direction in ["ASC", "DESC"] {
pair.agree(&format!(
"SELECT k, s, n FROM t ORDER BY s {direction} {placement} LIMIT 10"
));
}
}
}
#[test]
fn nulls_in_a_coded_key_over_the_batched_path_too() {
let pair = Pair::new("nullsoffset", &[holed("t", 20_000, 97)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY s NULLS FIRST LIMIT 10 OFFSET 1000");
}
#[test]
fn rows_from_two_tables_under_one_top_n_come_out_in_one_order() {
let tables = [ordered("t", 20_000, 97), ordered("u", 9_000, 31)];
let pair = Pair::new("two", &tables, 4);
let union = "SELECT s, n FROM t UNION ALL SELECT s, n FROM u";
pair.agree(&format!("SELECT s, n FROM ({union}) ORDER BY s LIMIT 10"));
pair.agree(&format!("SELECT s, n FROM ({union}) ORDER BY s DESC LIMIT 10"));
}
#[test]
fn a_coded_key_under_a_filter_that_the_candidates_tighten() {
let pair = Pair::new("cutoff", &[ordered("t", 20_000, 97)], 4);
pair.agree("SELECT s FROM t WHERE s > '1500000' ORDER BY s LIMIT 10");
pair.agree("SELECT s FROM t WHERE s < '1900000' ORDER BY s DESC LIMIT 10");
}
#[test]
fn a_key_that_is_an_expression_over_a_coded_column() {
let pair = Pair::new("expr", &[ordered("t", 20_000, 97)], 4);
pair.agree("SELECT k, s, n FROM t ORDER BY SUBSTRING(s, 3), n LIMIT 10");
}