use rudb_common::{Field, LogicalType, Result};
use rudb_functions::FILE_ROW_NUMBER;
use rudb_plan::{
Carried, ColumnBinding, CompareOp, Expr, JoinKind, Node, NodeRef, Plan, Slice, rids_of,
};
use crate::estimate;
use crate::pass::{Context, Pass};
pub const CACHE_BYTES: u64 = 8 * 1024 * 1024;
pub const NARROW_BYTES: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sizes {
pub cache_bytes: u64,
pub narrow_bytes: usize,
}
impl Default for Sizes {
fn default() -> Self {
Self { cache_bytes: CACHE_BYTES, narrow_bytes: NARROW_BYTES }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Linked {
pub child: String,
pub child_column: String,
pub parent: String,
pub parent_column: String,
pub built: bool,
pub total: bool,
}
impl Linked {
pub fn built(
child: impl Into<String>,
child_column: impl Into<String>,
parent: impl Into<String>,
parent_column: impl Into<String>,
) -> Self {
Self { built: true, ..Self::declared(child, child_column, parent, parent_column) }
}
pub fn verified(
child: impl Into<String>,
child_column: impl Into<String>,
parent: impl Into<String>,
parent_column: impl Into<String>,
) -> Self {
Self { total: true, ..Self::built(child, child_column, parent, parent_column) }
}
pub fn declared(
child: impl Into<String>,
child_column: impl Into<String>,
parent: impl Into<String>,
parent_column: impl Into<String>,
) -> Self {
Self {
child: child.into(),
child_column: child_column.into(),
parent: parent.into(),
parent_column: parent_column.into(),
built: false,
total: false,
}
}
#[must_use]
pub fn exactly_one(&self) -> bool {
self.built && self.total
}
#[must_use]
pub(crate) fn between(&self, child: (&str, &str), parent: (&str, &str)) -> bool {
self.child.eq_ignore_ascii_case(child.0)
&& self.child_column.eq_ignore_ascii_case(child.1)
&& self.parent.eq_ignore_ascii_case(parent.0)
&& self.parent_column.eq_ignore_ascii_case(parent.1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Why {
Kind,
Key,
None,
NotBuilt,
ParentNotStored,
ChildNotStored,
RowIdGone,
ColumnWouldShow,
Fits {
rows: u64,
bytes: u64,
},
Wide {
width: usize,
narrow: usize,
},
Narrow {
width: usize,
narrow: usize,
},
NeverRead,
}
impl Why {
#[must_use]
pub const fn chosen(self) -> bool {
matches!(self, Self::Narrow { .. } | Self::NeverRead)
}
const fn rank(self) -> u8 {
match self {
Self::Kind | Self::Key | Self::None => 0,
Self::ParentNotStored | Self::ChildNotStored => 1,
Self::NotBuilt => 2,
Self::RowIdGone => 3,
Self::ColumnWouldShow => 4,
Self::Fits { .. } | Self::Wide { .. } => 5,
Self::Narrow { .. } | Self::NeverRead => 6,
}
}
fn or(self, other: Self) -> Self {
if other.rank() > self.rank() { other } else { self }
}
}
impl std::fmt::Display for Why {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Kind => write!(out, "a forward link does not answer a right or a full join"),
Self::Key => write!(out, "the join is not one equality over two columns"),
Self::None => write!(out, "no relationship is declared between those two columns"),
Self::NotBuilt => {
write!(out, "the relationship is declared and its link is not in the file")
}
Self::ParentNotStored => {
write!(out, "the parent side is not a stored table read whole")
}
Self::ChildNotStored => write!(out, "the child side is not a stored table"),
Self::RowIdGone => write!(
out,
"the link is in the file and the rows here are no longer rows of the child table"
),
Self::ColumnWouldShow => {
write!(out, "the row id would reach an operator that counts its input's columns")
}
Self::Fits { rows, bytes } => write!(
out,
"the parent is {rows} rows and {bytes} bytes projected, which fits in cache"
),
Self::Wide { width, narrow } => write!(
out,
"the parent does not fit in cache and its projection is {width} bytes, \
which is not under {narrow}"
),
Self::Narrow { width, narrow } => write!(
out,
"the parent does not fit in cache and its projection is {width} bytes, \
which is under {narrow}"
),
Self::NeverRead => write!(out, "a semi or an anti join never reads the parent"),
}
}
}
#[must_use]
pub fn why(plan: &Plan, at: NodeRef, context: &Context) -> Option<Why> {
match *plan.node(at) {
Node::Join { .. } => {
let carried = rids_of(plan);
let consumers = consumers(plan);
Some(decided(plan, at, &carried, &consumers, context).0)
}
Node::LinkJoin { parent, kind, .. } => Some(match kind {
JoinKind::Semi | JoinKind::Anti => Why::NeverRead,
_ => match *plan.node(parent) {
Node::Get { columns, .. } => {
let sizes = context.sizes();
Why::Narrow { width: width(plan, columns), narrow: sizes.narrow_bytes }
}
_ => Why::ParentNotStored,
},
}),
_ => None,
}
}
#[derive(Debug)]
pub struct LinkJoinRewrite;
impl Pass for LinkJoinRewrite {
fn name(&self) -> &'static str {
"link_join"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
if context.links().is_empty() {
return Ok(());
}
let carried = rids_of(plan);
let consumers = consumers(plan);
let reachable = reachable(plan);
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
if reachable.get(node as usize).copied().unwrap_or(false) {
rewrite(plan, node, &carried, &consumers, context);
}
}
Ok(())
}
}
fn rewrite(
plan: &mut Plan,
at: NodeRef,
carried: &[Carried],
consumers: &[Option<NodeRef>],
context: &Context,
) {
let (why, taken) = decided(plan, at, carried, consumers, context);
if !why.chosen() {
return;
}
let Some(taken) = taken else { return };
let Some(binding) = number(plan, taken.scan) else {
return;
};
let rid = plan.add_expr(Expr::Column(binding), LogicalType::BigInt);
let Node::Join { kind, conditions, .. } = *plan.node(at) else { return };
*plan.node_mut(at) =
Node::LinkJoin { child: taken.child, parent: taken.parent, kind, conditions, rid };
}
#[derive(Debug, Clone, Copy)]
struct Taken {
child: NodeRef,
parent: NodeRef,
scan: NodeRef,
}
fn decided(
plan: &Plan,
at: NodeRef,
carried: &[Carried],
consumers: &[Option<NodeRef>],
context: &Context,
) -> (Why, Option<Taken>) {
let Node::Join { left, right, kind, conditions, .. } = *plan.node(at) else {
return (Why::Key, None);
};
let sides: &[(NodeRef, NodeRef)] = match kind {
JoinKind::Inner => &[(left, right), (right, left)],
JoinKind::Left | JoinKind::Semi | JoinKind::Anti => &[(left, right)],
_ => return (Why::Kind, None),
};
let Some(keys) = equated_pair(plan, conditions) else {
return (Why::Key, None);
};
let mut worst = Why::None;
for &(child, parent) in sides {
let found = match matched(plan, child, parent, keys, carried, context) {
Ok(found) => found,
Err(why) => {
worst = worst.or(why);
continue;
}
};
let why = worth_it(plan, parent, kind, &found, context);
let why =
if why.chosen() && !absorbed(plan, consumers, at) { Why::ColumnWouldShow } else { why };
worst = worst.or(why);
if why.chosen() {
return (why, Some(Taken { child, parent, scan: found.scan }));
}
}
(worst, None)
}
fn reachable(plan: &Plan) -> Vec<bool> {
let mut seen = vec![false; plan.node_count()];
let mut stack = vec![plan.root()];
while let Some(at) = stack.pop() {
let Some(slot) = seen.get_mut(at as usize) else {
continue;
};
if *slot {
continue;
}
*slot = true;
stack.extend(plan.node(at).children().into_iter().flatten());
}
seen
}
pub(crate) fn consumers(plan: &Plan) -> Vec<Option<NodeRef>> {
let mut consumers = vec![None; plan.node_count()];
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
for child in plan.node(node).children().into_iter().flatten() {
if let Some(slot) = consumers.get_mut(child as usize) {
*slot = Some(node);
}
}
}
consumers
}
pub(crate) fn absorbed(plan: &Plan, consumers: &[Option<NodeRef>], at: NodeRef) -> bool {
let mut at = at;
for _ in 0..plan.node_count() {
let Some(above) = consumers.get(at as usize).copied().flatten() else {
return false;
};
match plan.node(above) {
Node::Project { .. } | Node::Aggregate { .. } => return true,
Node::Filter { .. }
| Node::Sort { .. }
| Node::Limit { .. }
| Node::LimitPercent { .. }
| Node::TopN { .. }
| Node::Window { .. }
| Node::Join { .. }
| Node::LinkJoin { .. }
| Node::CrossProduct { .. }
| Node::DependentJoin { .. } => at = above,
_ => return false,
}
}
false
}
struct Match {
scan: NodeRef,
projected: Slice,
}
fn matched(
plan: &Plan,
child: NodeRef,
parent: NodeRef,
keys: [ColumnBinding; 2],
carried: &[Carried],
context: &Context,
) -> std::result::Result<Match, Why> {
let Node::Get { table: parent_name, index: parent_index, columns: projected, .. } =
*plan.node(parent)
else {
return Err(Why::ParentNotStored);
};
let [child_key, parent_key] =
match (keys[0].table == parent_index, keys[1].table == parent_index) {
(false, true) => [keys[0], keys[1]],
(true, false) => [keys[1], keys[0]],
_ => return Err(Why::None),
};
let scan = scan_under(plan, child, child_key.table).ok_or(Why::ChildNotStored)?;
let Node::Get { table: child_name, columns: child_columns, .. } = *plan.node(scan) else {
return Err(Why::ChildNotStored);
};
let child_column =
plan.field_list(child_columns).get(child_key.column as usize).ok_or(Why::None)?;
let parent_column =
plan.field_list(projected).get(parent_key.column as usize).ok_or(Why::None)?;
let relationship = (
(plan.string(child_name), child_column.name.as_str()),
(plan.string(parent_name), parent_column.name.as_str()),
);
let declared = context
.links()
.iter()
.find(|link| link.between(relationship.0, relationship.1))
.ok_or(Why::None)?;
if !declared.built {
return Err(Why::NotBuilt);
}
if !carried.get(child as usize).is_some_and(|rids| rids.has(child_key.table)) {
return Err(Why::RowIdGone);
}
Ok(Match { scan, projected })
}
fn worth_it(plan: &Plan, parent: NodeRef, kind: JoinKind, found: &Match, context: &Context) -> Why {
if matches!(kind, JoinKind::Semi | JoinKind::Anti) {
return Why::NeverRead;
}
let width = width(plan, found.projected);
let sizes = context.sizes();
let bytes = |rows: u64| rows.saturating_mul(u64::try_from(width).unwrap_or(u64::MAX));
if let Some(rows) = estimate::rows(plan, parent, context.facts()) {
if bytes(rows) <= sizes.cache_bytes {
return Why::Fits { rows, bytes: bytes(rows) };
}
}
if width < sizes.narrow_bytes {
Why::Narrow { width, narrow: sizes.narrow_bytes }
} else {
Why::Wide { width, narrow: sizes.narrow_bytes }
}
}
fn width(plan: &Plan, columns: Slice) -> usize {
plan.field_list(columns).iter().map(|field| field.ty.physical().size()).sum()
}
fn equated_pair(plan: &Plan, conditions: Slice) -> Option<[ColumnBinding; 2]> {
let [condition] = plan.expr_list(conditions) else {
return None;
};
let Expr::Compare { op: CompareOp::Equal, left, right } = *plan.expr(*condition) else {
return None;
};
match (plan.expr(left), plan.expr(right)) {
(&Expr::Column(left), &Expr::Column(right)) => Some([left, right]),
_ => None,
}
}
fn scan_under(plan: &Plan, at: NodeRef, index: u32) -> Option<NodeRef> {
match *plan.node(at) {
Node::Get { index: found, .. } if found == index => Some(at),
Node::Filter { input, .. } => scan_under(plan, input, index),
_ => None,
}
}
fn number(plan: &mut Plan, scan: NodeRef) -> Option<ColumnBinding> {
let Node::Get { index, columns, .. } = *plan.node(scan) else {
return None;
};
let mut fields = plan.field_list(columns).to_vec();
if let Some(at) = fields.iter().position(|field| field.name == FILE_ROW_NUMBER) {
return Some(ColumnBinding::new(index, u32::try_from(at).ok()?));
}
let at = u32::try_from(fields.len()).ok()?;
fields.push(Field::required(FILE_ROW_NUMBER.to_string(), LogicalType::BigInt));
let widened = plan.add_fields(&fields);
match plan.node_mut(scan) {
Node::Get { columns, .. } => *columns = widened,
_ => return None,
}
Some(ColumnBinding::new(index, at))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rudb_plan::{Node, Plan};
use super::{LinkJoinRewrite, Linked, Why};
use crate::estimate::Facts;
use crate::pass::{Context, Pass};
fn declared() -> Arc<Vec<Linked>> {
Arc::new(vec![Linked::built("lineitem", "l_orderkey", "orders", "o_orderkey")])
}
fn context(parent_rows: u64) -> Context {
let mut facts = Facts::new();
facts.record("memory", "main", "lineitem", 6_000_000);
facts.record("memory", "main", "orders", parent_rows);
let mut context = Context::new();
context.measure(Arc::new(facts));
context.relate(declared());
context
}
fn joined(kind: &str) -> Plan {
let text = format!(
"Project #2 [#0.0::BIGINT AS k]\n \
Join {kind} on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n \
Get memory.main.lineitem AS lineitem #0 [l_orderkey::BIGINT]\n \
Get memory.main.orders AS orders #1 [o_orderkey::BIGINT]\n"
);
Plan::parse(&text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"))
}
fn rewritten(plan: &mut Plan, context: &Context) -> String {
LinkJoinRewrite.run(plan, context).expect("the pass does not fail");
plan.to_string()
}
#[test]
fn a_join_over_a_declared_relationship_with_a_parent_too_large_to_cache_reads_the_link() {
let mut plan = joined("INNER");
let text = rewritten(&mut plan, &context(1_500_000));
assert!(text.contains("LinkJoin"), "the join was not rewritten:\n{text}");
assert!(text.contains("rid="), "the rewrite left no row id behind:\n{text}");
assert!(
text.contains("file_row_number"),
"the child scan was not asked for a row id:\n{text}"
);
}
#[test]
fn a_parent_small_enough_to_sit_in_cache_keeps_its_hash_join() {
let mut plan = joined("INNER");
let text = rewritten(&mut plan, &context(25));
assert!(!text.contains("LinkJoin"), "twenty five rows were worth a link:\n{text}");
}
#[test]
fn a_semi_join_reads_the_link_however_small_the_parent_is() {
let mut plan = joined("SEMI");
let text = rewritten(&mut plan, &context(25));
assert!(text.contains("LinkJoin"), "a semi join sized its parent:\n{text}");
}
#[test]
fn an_anti_join_reads_the_link_too() {
let mut plan = joined("ANTI");
let text = rewritten(&mut plan, &context(25));
assert!(text.contains("LinkJoin"), "an anti join sized its parent:\n{text}");
}
#[test]
fn a_relationship_nobody_declared_is_left_alone() {
let mut plan = joined("INNER");
let mut context = context(1_500_000);
context.relate(Arc::default());
let text = rewritten(&mut plan, &context);
assert!(!text.contains("LinkJoin"), "an undeclared join was rewritten:\n{text}");
}
#[test]
fn the_relationship_has_to_be_the_way_round_it_was_declared() {
let mut plan = joined("INNER");
let mut context = context(1_500_000);
context.relate(Arc::new(vec![Linked::built(
"orders",
"o_orderkey",
"lineitem",
"l_orderkey",
)]));
let text = rewritten(&mut plan, &context);
assert!(text.contains("LinkJoin"), "the pass refused to swap the sides:\n{text}");
let found = (0..u32::try_from(plan.node_count()).expect("a small plan"))
.find_map(|node| match *plan.node(node) {
Node::LinkJoin { child, .. } => Some(child),
_ => None,
})
.expect("the join is still a join of some kind");
let Node::Get { table, .. } = *plan.node(found) else {
panic!("the child is not a scan");
};
assert_eq!(
plan.string(table),
"orders",
"the child is not the table the relationship names as the child"
);
}
#[test]
fn a_left_join_may_only_read_the_link_in_the_direction_that_keeps_its_rows() {
let mut plan = joined("LEFT");
let mut context = context(1_500_000);
context.relate(Arc::new(vec![Linked::built(
"orders",
"o_orderkey",
"lineitem",
"l_orderkey",
)]));
let text = rewritten(&mut plan, &context);
assert!(
!text.contains("LinkJoin"),
"a left join swapped the side whose rows it keeps:\n{text}"
);
}
#[test]
fn a_join_at_the_root_is_left_alone_because_the_row_id_would_be_in_the_answer() {
let text = "Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n \
Get memory.main.lineitem AS lineitem #0 [l_orderkey::BIGINT]\n \
Get memory.main.orders AS orders #1 [o_orderkey::BIGINT]\n";
let mut plan = Plan::parse(text).expect("the plan parses");
let text = rewritten(&mut plan, &context(1_500_000));
assert!(!text.contains("LinkJoin"), "the answer grew a column:\n{text}");
}
#[test]
fn a_full_or_right_join_is_not_something_a_forward_link_answers() {
for kind in ["FULL", "RIGHT"] {
let mut plan = joined(kind);
let text = rewritten(&mut plan, &context(1_500_000));
assert!(!text.contains("LinkJoin"), "a {kind} join was rewritten:\n{text}");
}
}
#[test]
fn a_join_no_longer_in_the_plan_does_not_widen_a_scan_that_still_is() {
let mut plan = joined("INNER");
let Node::Project { index, exprs, names, .. } = *plan.node(plan.root()) else {
panic!("the plan is a projection over a join");
};
let scan = (0..u32::try_from(plan.node_count()).expect("a small plan"))
.find(|&node| matches!(*plan.node(node), Node::Get { index: 0, .. }))
.expect("the child scan is in the plan");
let kept = plan.add_node(Node::Project { input: scan, index, exprs, names });
plan.set_root(kept);
let text = rewritten(&mut plan, &context(1_500_000));
assert!(!text.contains("LinkJoin"), "a join nobody reads was rewritten:\n{text}");
assert!(
!text.contains("file_row_number"),
"a dead join widened a scan the answer reads:\n{text}"
);
}
#[test]
fn running_the_pass_twice_gives_the_same_plan() {
let mut plan = joined("INNER");
let context = context(1_500_000);
let once = rewritten(&mut plan, &context);
let twice = rewritten(&mut plan, &context);
assert_eq!(once, twice, "the pass does not settle");
}
fn about(plan: &Plan, context: &Context) -> Why {
(0..u32::try_from(plan.node_count()).expect("a small plan"))
.find_map(|node| super::why(plan, node, context))
.expect("the plan has a join in it")
}
#[test]
fn a_join_that_read_the_link_says_which_bullet_chose_it() {
let mut plan = joined("INNER");
let context = context(1_500_000);
rewritten(&mut plan, &context);
let why = about(&plan, &context);
assert!(why.chosen(), "{why}");
assert_eq!(
why.to_string(),
"the parent does not fit in cache and its projection is 8 bytes, which is under 32"
);
}
#[test]
fn a_semi_join_says_it_never_read_the_parent_rather_than_quoting_a_width() {
let mut plan = joined("SEMI");
let context = context(25);
rewritten(&mut plan, &context);
let why = about(&plan, &context);
assert!(why.chosen(), "{why}");
assert_eq!(why.to_string(), "a semi or an anti join never reads the parent");
}
#[test]
fn a_parent_that_fits_says_how_large_it_was_rather_than_only_that_it_fitted() {
let mut plan = joined("INNER");
let context = context(25);
rewritten(&mut plan, &context);
let why = about(&plan, &context);
assert!(!why.chosen(), "{why}");
assert_eq!(
why.to_string(),
"the parent is 25 rows and 200 bytes projected, which fits in cache"
);
}
fn over(kind: &str, links: Vec<Linked>) -> (Plan, Context) {
let mut plan = joined(kind);
let mut context = context(1_500_000);
context.relate(Arc::new(links));
rewritten(&mut plan, &context);
(plan, context)
}
#[test]
fn a_relationship_nobody_built_says_the_link_is_missing_rather_than_the_relationship() {
let (plan, context) =
over("INNER", vec![Linked::declared("lineitem", "l_orderkey", "orders", "o_orderkey")]);
assert_eq!(
about(&plan, &context).to_string(),
"the relationship is declared and its link is not in the file"
);
}
#[test]
fn a_relationship_over_other_columns_says_there_is_none_over_these_ones() {
let (plan, context) =
over("INNER", vec![Linked::built("orders", "o_totalprice", "nation", "n_name")]);
assert_eq!(
about(&plan, &context).to_string(),
"no relationship is declared between those two columns"
);
}
#[test]
fn a_join_whose_row_id_would_reach_the_answer_says_that_and_not_that_there_is_no_link() {
let text = "Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n \
Get memory.main.lineitem AS lineitem #0 [l_orderkey::BIGINT]\n \
Get memory.main.orders AS orders #1 [o_orderkey::BIGINT]\n";
let mut plan = Plan::parse(text).expect("the plan parses");
let context = context(1_500_000);
rewritten(&mut plan, &context);
assert_eq!(
about(&plan, &context).to_string(),
"the row id would reach an operator that counts its input's columns"
);
}
#[test]
fn a_right_join_says_the_link_points_the_other_way() {
let mut plan = joined("RIGHT");
let context = context(1_500_000);
rewritten(&mut plan, &context);
assert_eq!(
about(&plan, &context).to_string(),
"a forward link does not answer a right or a full join"
);
}
#[test]
fn the_three_constructors_are_three_steps_up_the_same_ladder() {
let declared = Linked::declared("lineitem", "l_orderkey", "orders", "o_orderkey");
assert!(!declared.built, "nobody built it");
assert!(!declared.total, "and nothing counted the children");
let built = Linked::built("lineitem", "l_orderkey", "orders", "o_orderkey");
assert!(built.built, "the parent side was read and found unique");
assert!(!built.total, "which says nothing about the children");
let verified = Linked::verified("lineitem", "l_orderkey", "orders", "o_orderkey");
assert!(verified.built && verified.total, "both certificates of section 7.3");
for link in [&declared, &built, &verified] {
assert!(link.between(("lineitem", "l_orderkey"), ("orders", "o_orderkey")));
}
}
}