#![cfg(feature = "testing")]
use powdb_query::executor::Engine;
use powdb_query::result::QueryResult;
use powdb_storage::types::Value;
const UUID_TEXT: &str = "3f2504e0-4f89-41d3-9a0c-0305e82c3301";
struct Column {
name: &'static str,
declaration: &'static str,
value: &'static str,
}
const COLUMNS: &[Column] = &[
Column {
name: "c_int",
declaration: "c_int: int",
value: "1",
},
Column {
name: "c_float",
declaration: "c_float: float",
value: "1.0",
},
Column {
name: "c_str",
declaration: "c_str: str",
value: "\"1\"",
},
Column {
name: "c_bool",
declaration: "c_bool: bool",
value: "true",
},
Column {
name: "c_dt",
declaration: "c_dt: datetime",
value: "1",
},
Column {
name: "c_uuid",
declaration: "c_uuid: uuid",
value: "\"UUID\"",
},
Column {
name: "c_bytes",
declaration: "c_bytes: bytes",
value: "\"\\\\x01\"",
},
Column {
name: "c_json",
declaration: "c_json: json",
value: "\"1\"",
},
];
struct Literal {
label: &'static str,
text: &'static str,
}
const LITERALS: &[Literal] = &[
Literal {
label: "int_0",
text: "0",
},
Literal {
label: "int_1",
text: "1",
},
Literal {
label: "int_2",
text: "2",
},
Literal {
label: "int_neg",
text: "-1",
},
Literal {
label: "float_1_0",
text: "1.0",
},
Literal {
label: "float_1_5",
text: "1.5",
},
Literal {
label: "str_1",
text: "\"1\"",
},
Literal {
label: "str_a",
text: "\"a\"",
},
Literal {
label: "str_empty",
text: "\"\"",
},
Literal {
label: "bool_true",
text: "true",
},
Literal {
label: "bool_false",
text: "false",
},
Literal {
label: "null",
text: "null",
},
Literal {
label: "uuid_lit",
text: "uuid(\"UUID\")",
},
Literal {
label: "bytes_lit",
text: "bytes(\"\\\\x01\")",
},
];
const OPERATORS: &[&str] = &["=", "!=", "<", "<=", ">", ">="];
fn schema() -> String {
let columns: Vec<&str> = COLUMNS.iter().map(|column| column.declaration).collect();
format!(
"type X {{ required unique id: int, {} }}",
columns.join(", ")
)
}
fn populated_row() -> String {
let assignments: Vec<String> = COLUMNS
.iter()
.map(|column| {
format!(
"{} := {}",
column.name,
column.value.replace("UUID", UUID_TEXT)
)
})
.collect();
format!("insert X {{ id := 1, {} }}", assignments.join(", "))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Index {
None,
Btree,
Unique,
}
impl Index {
fn verb(self) -> Option<&'static str> {
match self {
Index::None => None,
Index::Btree => Some("index"),
Index::Unique => Some("unique"),
}
}
}
const INDEXED: [Index; 2] = [Index::Btree, Index::Unique];
fn build(force_generic: bool) -> (Engine, tempfile::TempDir) {
let (engine, dir, _) = build_indexed(force_generic, Index::None);
(engine, dir)
}
fn build_indexed(force_generic: bool, index: Index) -> (Engine, tempfile::TempDir, Vec<String>) {
let dir = tempfile::tempdir().expect("temp dir for the matrix engine");
let mut engine = Engine::new(dir.path()).expect("engine opens over a fresh temp dir");
for statement in [
schema(),
populated_row(),
"insert X { id := 2 }".to_string(),
] {
engine
.execute_powql(&statement)
.unwrap_or_else(|err| panic!("fixture statement `{statement}` failed: {err}"));
}
let mut indexed = Vec::new();
if let Some(verb) = index.verb() {
for column in COLUMNS {
let statement = format!("alter X add {verb} .{}", column.name);
if engine.execute_powql(&statement).is_ok() {
indexed.push(column.name.to_string());
}
}
}
engine.set_force_generic_path(force_generic);
(engine, dir, indexed)
}
fn cell(engine: &mut Engine, query: &str) -> String {
match engine.execute_powql(query) {
Ok(QueryResult::Scalar(Value::Int(n))) => n.to_string(),
Ok(other) => format!("UNEXPECTED({other:?})"),
Err(err) => format!("ERR({err})"),
}
}
struct Sweep {
text: String,
path_divergences: Vec<String>,
index_divergences: Vec<String>,
}
fn render() -> Sweep {
let (mut fast, _fast_dir) = build(false);
let (mut generic, _generic_dir) = build(true);
let mut indexed: Vec<(Index, bool, Engine, tempfile::TempDir)> = Vec::new();
for index in INDEXED {
for force_generic in [false, true] {
let (engine, dir, _) = build_indexed(force_generic, index);
indexed.push((index, force_generic, engine, dir));
}
}
let mut out = String::new();
out.push_str(
"# PowDB cross-type comparison matrix.\n\
#\n\
# Generated by crates/query/tests/cross_type_matrix.rs. Do not hand-edit:\n\
# UPDATE_EXPECT=1 cargo test -p powdb-query --test cross_type_matrix\n\
#\n\
# Fixture: one row with every column populated (each holding \"one\" in its\n\
# own type) and one row that is null in every column. Each cell is the row\n\
# count returned by `count(X filter .<column> <op> <literal>)`, so 1 means\n\
# the populated row matched, 0 means nothing matched, and ERR(...) means the\n\
# comparison was refused. The null row must never match, including under\n\
# `!=`: that is PowDB's documented two-valued NULL rule.\n\
#\n\
# Every cell is evaluated with the executor fast paths on and again with\n\
# them forced off. They must agree; a disagreement is written as\n\
# DIVERGED(...) and fails the test.\n\
#\n\
# Every cell is also evaluated with a plain and with a unique index on\n\
# the column, at each path setting. An index must never change the\n\
# answer; one that does is written as INDEX_DIVERGED(...).\n\
#\n\
# column op literal result\n",
);
let mut divergences = Vec::new();
let mut index_divergences = Vec::new();
for column in COLUMNS {
out.push('\n');
for operator in OPERATORS {
for literal in LITERALS {
let query = format!(
"count(X filter .{} {operator} {})",
column.name,
literal.text.replace("UUID", UUID_TEXT)
);
let fast_cell = cell(&mut fast, &query);
let generic_cell = cell(&mut generic, &query);
let mut rendered = if fast_cell == generic_cell {
fast_cell.clone()
} else {
divergences.push(format!(
"{} {operator} {}: fast={fast_cell} generic={generic_cell}",
column.name, literal.label
));
format!("DIVERGED(fast={fast_cell}, generic={generic_cell})")
};
for (index, force_generic, engine, _) in indexed.iter_mut() {
let indexed_cell = cell(engine, &query);
let reference = if *force_generic {
&generic_cell
} else {
&fast_cell
};
if &indexed_cell != reference {
let paths = if *force_generic { "generic" } else { "fast" };
index_divergences.push(format!(
"{} {operator} {} [{index:?}/{paths}]: none={reference} \
index={indexed_cell}",
column.name, literal.label
));
rendered.push_str(&format!(
" INDEX_DIVERGED({index:?}/{paths}: none={reference}, \
index={indexed_cell})"
));
}
}
out.push_str(&format!(
"{:<9} {:<4} {:<14} {}\n",
column.name, operator, literal.label, rendered
));
}
}
}
Sweep {
text: out,
path_divergences: divergences,
index_divergences,
}
}
fn expected_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("expected")
.join("cross_type_matrix.txt")
}
#[test]
fn cross_type_comparison_matrix_matches_the_checked_in_snapshot() {
let sweep = render();
let rendered = sweep.text;
let divergences = sweep.path_divergences;
let path = expected_path();
if std::env::var_os("UPDATE_EXPECT").is_some() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("expected/ directory is creatable");
}
std::fs::write(&path, &rendered).expect("snapshot is writable");
}
let expected = std::fs::read_to_string(&path).unwrap_or_else(|err| {
panic!(
"cannot read {}: {err}. Regenerate with \
`UPDATE_EXPECT=1 cargo test -p powdb-query --test cross_type_matrix`",
path.display()
)
});
if expected != rendered {
let diff = first_difference(&expected, &rendered);
panic!(
"the cross-type comparison matrix changed. If the change is intended, \
regenerate it with `UPDATE_EXPECT=1 cargo test -p powdb-query --test \
cross_type_matrix` and review the diff.\n{diff}"
);
}
let mut observed = divergences;
observed.sort();
let mut known: Vec<String> = KNOWN_PATH_DIVERGENCES
.iter()
.map(|s| s.to_string())
.collect();
known.sort();
assert_eq!(
observed, known,
"the set of fast-path/generic coercion divergences changed. A new entry is a \
new bug; a missing entry means one was fixed and KNOWN_PATH_DIVERGENCES should \
shrink."
);
let mut observed_index = sweep.index_divergences;
observed_index.sort();
observed_index.dedup();
let mut known_index: Vec<String> = KNOWN_INDEX_DIVERGENCES
.iter()
.map(|s| s.to_string())
.collect();
known_index.sort();
assert_eq!(
observed_index, known_index,
"the set of cells where an index changes the answer changed. A new entry is a \
new bug; a missing entry means one was fixed and KNOWN_INDEX_DIVERGENCES \
should shrink."
);
}
const KNOWN_INDEX_DIVERGENCES: &[&str] = &[];
#[test]
fn the_index_axis_reaches_the_columns_it_claims_to() {
for index in INDEXED {
let (_engine, _dir, indexed) = build_indexed(false, index);
let all: Vec<String> = COLUMNS
.iter()
.map(|column| column.name.to_string())
.collect();
assert_eq!(
indexed, all,
"the {index:?} axis only reached {indexed:?} of {all:?}. If a type \
genuinely cannot be indexed, list it here deliberately; do not let the \
axis quietly skip it."
);
}
}
const KNOWN_PATH_DIVERGENCES: &[&str] = &[];
#[test]
fn int_and_float_comparison_is_a_consistent_total_order() {
let (mut engine, _dir) = build(false);
let column_first: Vec<String> = ["=", "!=", "<", "<=", ">", ">="]
.iter()
.map(|operator| {
cell(
&mut engine,
&format!("count(X filter .c_int {operator} 1.0)"),
)
})
.collect();
assert_eq!(
column_first,
vec!["1", "0", "0", "1", "0", "1"],
"an int column and an equal float literal must compare equal under `=` and \
under both non-strict inequalities, and unequal under nothing"
);
let literal_first: Vec<String> = ["=", "!=", "<", "<=", ">", ">="]
.iter()
.map(|operator| {
cell(
&mut engine,
&format!("count(X filter 1.0 {operator} .c_int)"),
)
})
.collect();
assert_eq!(literal_first, column_first, "the order must be symmetric");
let float_column: Vec<String> = ["=", "!=", "<", "<=", ">", ">="]
.iter()
.map(|operator| {
cell(
&mut engine,
&format!("count(X filter .c_float {operator} 1)"),
)
})
.collect();
assert_eq!(
float_column, column_first,
"a float column against an equal int literal must answer the same table as \
an int column against an equal float literal"
);
}
fn first_difference(expected: &str, actual: &str) -> String {
let mut report = String::new();
let expected_lines: Vec<&str> = expected.lines().collect();
let actual_lines: Vec<&str> = actual.lines().collect();
let mut shown = 0;
for index in 0..expected_lines.len().max(actual_lines.len()) {
let before = expected_lines.get(index).copied().unwrap_or("<missing>");
let after = actual_lines.get(index).copied().unwrap_or("<missing>");
if before != after {
report.push_str(&format!("line {}:\n -{before}\n +{after}\n", index + 1));
shown += 1;
if shown == 20 {
report.push_str(" ... further differences suppressed\n");
break;
}
}
}
report
}