use nedb_engine::sqljoin::JoinExec;
use nedb_engine::sqlplan::Stage;
use nedb_engine::sqlselect::{execute_with, parse};
use serde_json::{json, Value};
fn relation(name: &str) -> Option<Vec<Value>> {
Some(match name {
"l" => vec![
json!({"k": 1, "v": 10, "tag": "a"}),
json!({"k": 2, "v": 20, "tag": "b"}),
json!({"k": 3, "v": 30, "tag": null}),
json!({"k": null, "v": 40, "tag": "c"}),
json!({"k": 1, "v": 50, "tag": "a"}),
],
"r" => vec![
json!({"k": 1, "w": 100, "label": "x"}),
json!({"k": 2, "w": 200, "label": null}),
json!({"k": 9, "w": 300, "label": "orphan"}),
json!({"k": null, "w": 400, "label": "limbo"}),
],
"m" => vec![
json!({"k": 1, "z": 7}),
json!({"k": 2, "z": 8}),
],
_ => return None,
})
}
fn resolve(name: &str) -> anyhow::Result<Option<Box<dyn nedb_engine::sqlselect::Relation>>> {
Ok(relation(name).map(nedb_engine::sqlselect::from_vec))
}
#[track_caller]
fn agree(sql: &str) -> usize {
let sel = parse(sql).unwrap_or_else(|e| panic!("{sql}\n failed to parse: {e:#}"));
let mut baseline: Option<(Vec<String>, Vec<Value>)> = None;
let mut prefiltered = 0usize;
for exec in [JoinExec::NestedLoop, JoinExec::Hash] {
for pushdown in [false, true] {
let (cols, rows, plan) = execute_with(&sel, &resolve, exec, pushdown)
.unwrap_or_else(|e| {
panic!("{sql}\n failed with exec={exec:?} pushdown={pushdown}: {e:#}")
});
let names: Vec<String> = cols.into_iter().map(|c| c.name).collect();
if plan.stages.iter().any(|s| matches!(s, Stage::Prefilter { .. })) {
prefiltered += 1;
assert!(pushdown, "{sql}\n pre-filtered with pushdown DISABLED");
}
match &baseline {
None => baseline = Some((names, rows)),
Some((bn, br)) => {
assert_eq!(bn, &names, "{sql}\n columns differ (exec={exec:?} pushdown={pushdown})");
assert_eq!(
br, &rows,
"{sql}\n ROWS DIFFER with exec={exec:?} pushdown={pushdown}\n \
baseline = {}\n got = {}\n plan:\n{}",
json!(br),
json!(rows),
plan.render().join("\n")
);
}
}
}
}
prefiltered
}
const PREDICATES: &[&str] = &[
"l.v > 15",
"r.w > 150",
"l.tag = 'a'",
"r.label = 'x'",
"l.tag IS NULL",
"r.label IS NULL",
"l.k IS NULL",
"r.k IS NULL",
"NOT (r.label = 'x')",
"r.label NOT IN ('x')",
"r.w NOT BETWEEN 150 AND 250",
"coalesce(r.label, 'none') = 'none'",
"l.v > r.w",
"l.v > 15 AND r.w > 150",
"l.tag IS NULL AND r.label IS NULL",
"l.v > 15 OR r.w > 150",
];
const KINDS: &[&str] = &["JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"];
#[test]
fn pushdown_never_changes_an_answer() {
let mut total_prefiltered = 0;
for kind in KINDS {
for pred in PREDICATES {
for order in ["", " ORDER BY 1, 2"] {
let sql = format!(
"SELECT l.v, r.w FROM l {kind} r ON l.k = r.k WHERE {pred}{order}"
);
total_prefiltered += agree(&sql);
}
}
}
assert!(
total_prefiltered > 40,
"only {total_prefiltered} runs pre-filtered — the suite is not exercising pushdown"
);
}
#[test]
fn pushdown_never_changes_an_answer_in_a_three_relation_query() {
for second in ["JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"] {
for pred in [
"l.v > 15",
"r.w > 150",
"m.z > 7",
"l.tag IS NULL",
"r.label IS NULL",
"l.v > 15 AND m.z > 7",
] {
let sql = format!(
"SELECT l.v, r.w, m.z FROM l JOIN r ON l.k = r.k \
{second} m ON r.k = m.k WHERE {pred} ORDER BY 1, 2, 3"
);
agree(&sql);
}
}
}
#[test]
fn the_case_that_disproved_the_first_safety_argument() {
let sql = "SELECT l.v, r.w FROM l LEFT JOIN r ON l.k = r.k \
WHERE r.label IS NULL ORDER BY 1";
let prefiltered = agree(sql);
assert_eq!(prefiltered, 0, "r is nullable here and must not be pre-filtered");
let sel = parse(sql).unwrap();
let (_, rows, plan) = execute_with(&sel, &resolve, JoinExec::Auto, true).unwrap();
assert_eq!(rows.len(), 3, "{}", json!(rows));
assert!(
plan.refusals.iter().any(|r| r.contains("nullable side")),
"the refusal must be recorded, not silent: {:?}",
plan.refusals
);
}
#[test]
fn an_inner_join_pushes_and_says_so_in_the_plan() {
let sel = parse(
"SELECT l.v, r.w FROM l JOIN r ON l.k = r.k WHERE l.v > 15 AND r.w > 150",
)
.unwrap();
let (_, _, plan) = execute_with(&sel, &resolve, JoinExec::Auto, true).unwrap();
let pre: Vec<&Stage> = plan
.stages
.iter()
.filter(|s| matches!(s, Stage::Prefilter { .. }))
.collect();
assert_eq!(pre.len(), 2, "one per relation: {:?}", plan.stages);
assert!(plan.refusals.is_empty(), "{:?}", plan.refusals);
for s in pre {
if let Stage::Prefilter { in_rows, out_rows, .. } = s {
assert!(out_rows < in_rows, "{s:?} removed nothing");
}
}
}
#[test]
fn a_predicate_spanning_both_relations_is_refused_with_a_reason() {
let sel = parse("SELECT l.v FROM l JOIN r ON l.k = r.k WHERE l.v > r.w").unwrap();
let (_, _, plan) = execute_with(&sel, &resolve, JoinExec::Auto, true).unwrap();
assert!(!plan.stages.iter().any(|s| matches!(s, Stage::Prefilter { .. })));
assert!(
plan.refusals.iter().any(|r| r.contains("spans more than one relation")),
"{:?}",
plan.refusals
);
}
#[test]
fn the_plan_tree_survives_a_prefilter_above_a_scan() {
let sel = parse(
"SELECT l.v, r.w FROM l JOIN r ON l.k = r.k WHERE l.v > 15 AND r.w > 150",
)
.unwrap();
let (_, _, plan) = execute_with(&sel, &resolve, JoinExec::Auto, true).unwrap();
let t = plan.tree().expect("a tree");
let mut node = &t;
while node.children().len() == 1 {
node = node.children()[0];
}
assert_eq!(node.children().len(), 2, "the join still has two inputs:\n{:#?}", t);
for c in node.children() {
assert!(matches!(c.stage(), Stage::Prefilter { .. }), "{:?}", c.stage());
assert_eq!(c.children().len(), 1);
assert!(matches!(c.children()[0].stage(), Stage::Scan { .. }));
}
}