use rudb_plan::{BuildSide, Node, Plan};
use rudb_common::Result;
use crate::estimate::{self, Statistics};
use crate::pass::{Context, Pass, top_down};
#[derive(Debug)]
pub struct BuildSideProbeSide;
impl Pass for BuildSideProbeSide {
fn name(&self) -> &'static str {
"build_side_probe_side"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
choose(plan, context.statistics());
Ok(())
}
}
#[must_use]
fn prefers(left: u64, right: u64) -> Option<BuildSide> {
match left.cmp(&right) {
std::cmp::Ordering::Greater => Some(BuildSide::Left),
std::cmp::Ordering::Less => Some(BuildSide::Right),
std::cmp::Ordering::Equal => None,
}
}
fn choose(plan: &mut Plan, stats: &Statistics) {
for node in top_down(plan) {
let Node::Join { left, right, kind, .. } = *plan.node(node) else {
continue;
};
if kind.mirrored().is_none() {
continue;
}
let (Some(left), Some(right)) =
(estimate::rows(plan, left, stats), estimate::rows(plan, right, stats))
else {
continue;
};
let Some(wanted) = prefers(left, right) else {
continue;
};
if let Node::Join { build, .. } = plan.node_mut(node) {
*build = wanted;
}
}
}
#[cfg(test)]
mod tests {
use rudb_plan::{BuildSide, JoinKind, Node, Plan};
use super::{BuildSideProbeSide, prefers};
use crate::estimate::Statistics;
use crate::pass::{Context, Pass};
fn joined(kind: &str, left: u64, right: u64) -> (Plan, Context) {
let text = format!(
"Join {kind} on=[]\n Get memory.main.l AS l #0 [a::BIGINT]\n Get memory.main.r AS r #1 [b::BIGINT]\n"
);
let plan =
Plan::parse(&text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
let mut statistics = Statistics::new();
statistics.record("memory", "main", "l", left);
statistics.record("memory", "main", "r", right);
let mut context = Context::new();
context.measure(statistics);
(plan, context)
}
fn chosen(kind: &str, left: u64, right: u64) -> BuildSide {
let (mut plan, context) = joined(kind, left, right);
BuildSideProbeSide.run(&mut plan, &context).expect("the pass does not fail");
let Node::Join { build, .. } = *plan.node(plan.root()) else {
panic!("the root stopped being a join");
};
build
}
#[test]
fn the_larger_side_is_the_one_gathered_because_the_nested_loop_walks_it_a_chunk_at_a_time() {
assert_eq!(prefers(100_000, 4), Some(BuildSide::Left));
assert_eq!(prefers(4, 100_000), Some(BuildSide::Right));
}
#[test]
fn two_sides_of_the_same_size_are_not_a_preference() {
assert_eq!(prefers(500, 500), None);
}
#[test]
fn a_big_left_and_a_small_right_gathers_the_left() {
assert_eq!(chosen("INNER", 400_000, 4), BuildSide::Left);
}
#[test]
fn a_small_left_and_a_big_right_keeps_the_side_the_binder_emitted() {
assert_eq!(chosen("INNER", 4, 400_000), BuildSide::Right);
}
#[test]
fn an_outer_join_whose_kind_has_a_mirror_still_gets_the_larger_side() {
assert_eq!(chosen("LEFT", 400_000, 4), BuildSide::Left);
assert_eq!(chosen("FULL", 400_000, 4), BuildSide::Left);
}
#[test]
fn a_kind_that_names_its_left_input_as_the_subject_is_left_alone() {
assert_eq!(chosen("SEMI", 400_000, 4), BuildSide::Right);
assert_eq!(chosen("ANTI", 400_000, 4), BuildSide::Right);
assert_eq!(chosen("MARK", 400_000, 4), BuildSide::Right);
assert_eq!(chosen("POSITIONAL", 400_000, 4), BuildSide::Right);
}
#[test]
fn a_table_nobody_measured_leaves_the_join_as_it_was() {
let text = "Join INNER on=[]\n Get memory.main.l AS l #0 [a::BIGINT]\n Get memory.main.r AS r #1 [b::BIGINT]\n";
let mut plan = Plan::parse(text).expect("the plan parses");
let mut statistics = Statistics::new();
statistics.record("memory", "main", "l", 400_000);
let mut context = Context::new();
context.measure(statistics);
BuildSideProbeSide.run(&mut plan, &context).expect("the pass does not fail");
let Node::Join { build, .. } = *plan.node(plan.root()) else {
panic!("the root stopped being a join");
};
assert_eq!(build, BuildSide::Right, "one estimate is not two estimates");
}
#[test]
fn running_it_twice_writes_what_is_already_there() {
let (mut plan, context) = joined("INNER", 400_000, 4);
BuildSideProbeSide.run(&mut plan, &context).expect("the pass does not fail");
let once = plan.to_string();
BuildSideProbeSide.run(&mut plan, &context).expect("the pass does not fail");
assert_eq!(plan.to_string(), once, "the pass did not settle");
}
#[test]
fn the_kinds_with_no_mirror_are_the_ones_this_pass_refuses_to_touch() {
for kind in
[JoinKind::Semi, JoinKind::Anti, JoinKind::Single, JoinKind::Mark, JoinKind::Positional]
{
assert_eq!(kind.mirrored(), None, "{kind:?} grew a mirror and this test did not");
}
for kind in [JoinKind::Inner, JoinKind::Left, JoinKind::Right, JoinKind::Full] {
assert!(kind.mirrored().is_some(), "{kind:?} lost its mirror");
}
}
}