use nedb_engine::sqljoin::JoinExec;
use nedb_engine::sqlplan::Stage;
use nedb_engine::sqlselect::{execute_opts, parse, Opts, Relation};
use serde_json::{json, Value};
use std::cell::Cell;
use std::rc::Rc;
struct Counting {
rows: Vec<Value>,
at: usize,
pulled: Rc<Cell<usize>>,
hint: Option<usize>,
}
impl Relation for Counting {
fn next_row(&mut self) -> anyhow::Result<Option<Value>> {
if self.at >= self.rows.len() {
return Ok(None);
}
let v = self.rows[self.at].clone();
self.at += 1;
self.pulled.set(self.pulled.get() + 1);
Ok(Some(v))
}
fn size_hint(&self) -> Option<usize> {
self.hint
}
}
const N_LEFT: usize = 8_000;
const N_RIGHT: usize = 1_500;
fn left_rows() -> Vec<Value> {
(0..N_LEFT)
.map(|i| json!({"k": i % 1_500, "v": i, "amount": (i * 7) % 1000}))
.collect()
}
fn right_rows() -> Vec<Value> {
(0..N_RIGHT).map(|i| json!({"k": i, "w": i * 10})).collect()
}
struct Counters {
left: Rc<Cell<usize>>,
right: Rc<Cell<usize>>,
}
impl Counters {
fn new() -> Self {
Counters { left: Rc::new(Cell::new(0)), right: Rc::new(Cell::new(0)) }
}
}
fn run(sql: &str, exec: JoinExec) -> (usize, usize, usize, nedb_engine::sqlplan::Plan) {
let c = Counters::new();
let (lc, rc) = (Rc::clone(&c.left), Rc::clone(&c.right));
let resolve = move |name: &str| -> anyhow::Result<Option<Box<dyn Relation>>> {
Ok(match name {
"l" => Some(Box::new(Counting {
rows: left_rows(),
at: 0,
pulled: Rc::clone(&lc),
hint: Some(N_LEFT),
}) as Box<dyn Relation>),
"r" => Some(Box::new(Counting {
rows: right_rows(),
at: 0,
pulled: Rc::clone(&rc),
hint: Some(N_RIGHT),
}) as Box<dyn Relation>),
_ => None,
})
};
let sel = parse(sql).unwrap_or_else(|e| panic!("{sql}: {e:#}"));
let (_, rows, plan) =
execute_opts(&sel, &resolve, Opts::exec(exec)).unwrap_or_else(|e| panic!("{sql}: {e:#}"));
(c.left.get(), c.right.get(), rows.len(), plan)
}
#[test]
fn a_limit_over_a_join_pulls_exactly_the_rows_it_needs() {
for exec in [JoinExec::NestedLoop, JoinExec::Hash] {
let (left, right, out, plan) =
run("SELECT l.v, r.w FROM l JOIN r ON l.k = r.k LIMIT 20", exec);
assert_eq!(out, 20, "{exec:?}");
assert_eq!(
left, 20,
"{exec:?}: pulled {left} of {N_LEFT} left rows to return 20\n{}",
plan.render().join("\n")
);
assert_eq!(right, N_RIGHT, "{exec:?}: the inner side is materialised");
}
}
#[test]
fn an_outer_join_with_a_limit_is_equally_tight() {
for exec in [JoinExec::NestedLoop, JoinExec::Hash] {
let (left, _, out, _) =
run("SELECT l.v, r.w FROM l LEFT JOIN r ON l.k = r.k LIMIT 20", exec);
assert_eq!(out, 20, "{exec:?}");
assert_eq!(left, 20, "{exec:?}");
}
}
#[test]
fn a_filtered_limit_over_a_join_also_stops_early() {
for exec in [JoinExec::NestedLoop, JoinExec::Hash] {
let (left, _, out, plan) = run(
"SELECT l.v, r.w FROM l JOIN r ON l.k = r.k WHERE l.amount > 500 LIMIT 20",
exec,
);
assert_eq!(out, 20, "{exec:?}");
assert_eq!(
left, 92,
"{exec:?}: pulled {left} of {N_LEFT} with a filter + LIMIT 20\n{}",
plan.render().join("\n")
);
}
}
#[test]
fn an_offset_is_added_to_the_budget_rather_than_ignored() {
let (left, _, out, _) =
run("SELECT l.v FROM l JOIN r ON l.k = r.k LIMIT 5 OFFSET 40", JoinExec::Hash);
assert_eq!(out, 5);
assert_eq!(left, 45, "pulled {left}, expected offset + limit");
}
#[test]
fn without_a_limit_the_whole_source_is_read() {
let (left, right, out, _) =
run("SELECT l.v, r.w FROM l JOIN r ON l.k = r.k", JoinExec::Hash);
assert_eq!(left, N_LEFT, "no LIMIT means every left row is needed");
assert_eq!(right, N_RIGHT);
assert_eq!(out, N_LEFT, "one match per left row");
}
#[test]
fn an_order_by_still_reads_everything_because_it_must() {
let (left, _, out, plan) = run(
"SELECT l.v FROM l JOIN r ON l.k = r.k ORDER BY l.v DESC LIMIT 20",
JoinExec::Hash,
);
assert_eq!(out, 20);
assert_eq!(left, N_LEFT, "sorting needs every row first");
assert_eq!(plan.budget, None);
}
#[test]
fn distinct_also_reads_everything_because_it_must() {
let (left, _, _, plan) = run(
"SELECT DISTINCT l.k FROM l JOIN r ON l.k = r.k LIMIT 20",
JoinExec::Hash,
);
assert_eq!(left, N_LEFT, "dedup can shrink the count, so nothing may be skipped");
assert_eq!(plan.budget, None);
}
#[test]
fn the_inner_side_is_pulled_whole_on_purpose() {
for exec in [JoinExec::NestedLoop, JoinExec::Hash] {
let (_, right, _, _) =
run("SELECT l.v FROM l JOIN r ON l.k = r.k LIMIT 1", exec);
assert_eq!(right, N_RIGHT, "{exec:?}");
}
}
#[test]
fn the_plan_reports_rows_pulled_not_rows_available() {
let (left, _, _, plan) =
run("SELECT l.v, r.w FROM l JOIN r ON l.k = r.k LIMIT 20", JoinExec::Hash);
let scan_l = plan
.stages
.iter()
.find_map(|s| match s {
Stage::Scan { binding, rows, .. } if binding == "l" => Some(*rows),
_ => None,
})
.expect("a scan of l");
assert_eq!(scan_l, left, "the plan must report what was pulled");
assert!(scan_l < 100, "and that must be the small number: {scan_l}");
}
#[test]
fn a_prefilter_on_a_streamed_relation_reports_honest_counts() {
let (left, _, _, plan) = run(
"SELECT l.v, r.w FROM l JOIN r ON l.k = r.k WHERE l.amount > 500 LIMIT 20",
JoinExec::Hash,
);
let pre = plan.stages.iter().find_map(|s| match s {
Stage::Prefilter { binding, in_rows, out_rows, .. } if binding == "l" => {
Some((*in_rows, *out_rows))
}
_ => None,
});
let (in_rows, out_rows) = pre.expect("a prefilter on l");
assert_eq!(in_rows, left, "in_rows is what the source delivered");
assert!(out_rows <= in_rows);
assert!(out_rows >= 20, "at least the budget survived the filter");
}
#[test]
fn the_early_stopped_answer_is_the_prefix_of_the_full_one() {
let sel_all = parse("SELECT l.v, r.w FROM l JOIN r ON l.k = r.k").unwrap();
let sel_lim = parse("SELECT l.v, r.w FROM l JOIN r ON l.k = r.k LIMIT 20").unwrap();
let resolve = |name: &str| -> anyhow::Result<Option<Box<dyn Relation>>> {
Ok(match name {
"l" => Some(nedb_engine::sqlselect::from_vec(left_rows())),
"r" => Some(nedb_engine::sqlselect::from_vec(right_rows())),
_ => None,
})
};
for exec in [JoinExec::NestedLoop, JoinExec::Hash] {
let (_, all, _) = execute_opts(&sel_all, &resolve, Opts::exec(exec)).unwrap();
let (_, lim, _) = execute_opts(&sel_lim, &resolve, Opts::exec(exec)).unwrap();
assert_eq!(lim, all[..20].to_vec(), "{exec:?}");
}
}
#[test]
fn a_source_that_does_not_know_its_size_still_works() {
let resolve = |name: &str| -> anyhow::Result<Option<Box<dyn Relation>>> {
Ok(match name {
"l" => Some(Box::new(Counting {
rows: left_rows(),
at: 0,
pulled: Rc::new(Cell::new(0)),
hint: None,
}) as Box<dyn Relation>),
"r" => Some(nedb_engine::sqlselect::from_vec(right_rows())),
_ => None,
})
};
let sel = parse("SELECT l.v, r.w FROM l JOIN r ON l.k = r.k LIMIT 20").unwrap();
let (_, rows, plan) = execute_opts(&sel, &resolve, Opts::default()).unwrap();
assert_eq!(rows.len(), 20);
assert_eq!(
plan.join_strategy(0),
Some(nedb_engine::sqljoin::Strategy::Hash),
"{}",
plan.render().join("\n")
);
}