use rudb_common::Value;
use rudb_plan::{CompareOp, ConjunctionOp, Expr, ExprRef, JoinKind, Plan, Slice};
use crate::tables::TableSet;
pub const TOLERANT: [&str; 1] = ["coalesce"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Known {
Null,
True,
False,
Dropped,
Present,
Unknown,
}
impl Known {
fn dropped(self) -> bool {
matches!(self, Self::Null | Self::False | Self::Dropped)
}
fn present(self) -> bool {
matches!(self, Self::True | Self::False | Self::Present)
}
}
pub(crate) fn narrow(
plan: &Plan,
kind: JoinKind,
pending: &[ExprRef],
below: (&TableSet, &TableSet),
) -> JoinKind {
let padded = match kind {
JoinKind::Left => (false, true),
JoinKind::Right => (true, false),
JoinKind::Full => (true, true),
_ => return kind,
};
let gone = |side: &TableSet| pending.iter().any(|&part| rejects(plan, part, side));
let (left, right) = (padded.0 && gone(below.0), padded.1 && gone(below.1));
match (kind, left, right) {
(JoinKind::Left, _, true) | (JoinKind::Right, true, _) | (JoinKind::Full, true, true) => {
JoinKind::Inner
}
(JoinKind::Full, true, false) => JoinKind::Left,
(JoinKind::Full, false, true) => JoinKind::Right,
_ => kind,
}
}
fn rejects(plan: &Plan, expr: ExprRef, nulled: &TableSet) -> bool {
known(plan, expr, nulled).dropped()
}
fn known(plan: &Plan, expr: ExprRef, nulled: &TableSet) -> Known {
match *plan.expr(expr) {
Expr::Column(binding) => {
if nulled.contains(binding.table) {
Known::Null
} else {
Known::Unknown
}
}
Expr::Constant(value) => match plan.value(value) {
Value::Null => Known::Null,
Value::Boolean(true) => Known::True,
Value::Boolean(false) => Known::False,
_ => Known::Present,
},
Expr::Cast { input, .. } => match known(plan, input, nulled) {
Known::Null => Known::Null,
_ => Known::Unknown,
},
Expr::Compare { op, left, right } => {
compare(op, known(plan, left, nulled), known(plan, right, nulled))
}
Expr::Conjunction { op, children } => {
let parts: Vec<Known> =
plan.expr_list(children).iter().map(|&part| known(plan, part, nulled)).collect();
match op {
ConjunctionOp::And => and(&parts),
ConjunctionOp::Or => or(&parts),
}
}
Expr::Function { name, args } => function(plan, plan.string(name), args, nulled),
Expr::Aggregate { .. } | Expr::Case { .. } => Known::Unknown,
}
}
fn compare(op: CompareOp, left: Known, right: Known) -> Known {
let null = left == Known::Null || right == Known::Null;
let both = left == Known::Null && right == Known::Null;
let one = null && (left.present() || right.present());
match op {
CompareOp::NotDistinctFrom if both => Known::True,
CompareOp::NotDistinctFrom if one => Known::False,
CompareOp::DistinctFrom if both => Known::False,
CompareOp::DistinctFrom if one => Known::True,
CompareOp::NotDistinctFrom | CompareOp::DistinctFrom => Known::Unknown,
_ if null => Known::Null,
_ => Known::Unknown,
}
}
fn and(parts: &[Known]) -> Known {
if parts.contains(&Known::False) {
return Known::False;
}
if parts.iter().any(|part| part.dropped()) {
return Known::Dropped;
}
if parts.iter().all(|part| *part == Known::True) { Known::True } else { Known::Unknown }
}
fn or(parts: &[Known]) -> Known {
if parts.contains(&Known::True) {
return Known::True;
}
if parts.iter().all(|part| *part == Known::False) {
return Known::False;
}
if parts.iter().all(|part| *part == Known::False || *part == Known::Null) {
return Known::Null;
}
if parts.iter().all(|part| part.dropped()) { Known::Dropped } else { Known::Unknown }
}
fn function(plan: &Plan, name: &str, args: Slice, nulled: &TableSet) -> Known {
let parts: Vec<Known> =
plan.expr_list(args).iter().map(|&arg| known(plan, arg, nulled)).collect();
if name == "not" {
return match parts.first() {
Some(Known::True) => Known::False,
Some(Known::False) => Known::True,
Some(Known::Null) => Known::Null,
_ => Known::Unknown,
};
}
if TOLERANT.contains(&name) {
return Known::Unknown;
}
if parts.contains(&Known::Null) { Known::Null } else { Known::Unknown }
}
#[cfg(test)]
mod tests {
use rudb_plan::{JoinKind, Node, Plan};
use crate::tables::produced;
fn narrowed(kind: JoinKind, predicate: &str) -> JoinKind {
let keyword = kind.keyword();
let text = format!(
"\
Filter {predicate}
Join {keyword} on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
"
);
let plan =
Plan::parse(&text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
let Node::Filter { input, predicate } = *plan.node(plan.root()) else {
panic!("{text} is not a filter over a join");
};
let Node::Join { left, right, kind, .. } = *plan.node(input) else {
panic!("{text} is not a filter over a join");
};
let below = (produced(&plan, left), produced(&plan, right));
super::narrow(&plan, kind, &[predicate], (&below.0, &below.1))
}
#[test]
fn a_comparison_against_a_padded_column_leaves_an_inner_join() {
let predicate = "(#1.0::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, predicate), JoinKind::Inner);
}
#[test]
fn a_predicate_over_the_kept_side_says_nothing_about_the_padding() {
let predicate = "(#0.0::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, predicate), JoinKind::Left);
}
#[test]
fn a_predicate_over_both_sides_rejects_when_either_side_of_it_is_padded() {
let predicate = "(#0.1::INTEGER = #1.0::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, predicate), JoinKind::Inner);
}
#[test]
fn is_not_null_rejects_the_padded_row_and_is_null_is_what_asks_for_it() {
let not_null = "(#1.0::INTEGER IS DISTINCT FROM NULL::\"NULL\")::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, not_null), JoinKind::Inner);
let is_null = "(#1.0::INTEGER IS NOT DISTINCT FROM NULL::\"NULL\")::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, is_null), JoinKind::Left);
}
#[test]
fn not_of_is_null_is_the_same_answer_as_is_not_null() {
let predicate =
"not((#1.0::INTEGER IS NOT DISTINCT FROM NULL::\"NULL\")::BOOLEAN)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, predicate), JoinKind::Inner);
}
#[test]
fn an_and_rejects_when_one_part_does_and_an_or_when_every_part_does() {
let kept = "(#0.0::INTEGER > 5::INTEGER)::BOOLEAN";
let padded = "(#1.0::INTEGER > 5::INTEGER)::BOOLEAN";
let and = format!("({kept} AND {padded})::BOOLEAN");
assert_eq!(narrowed(JoinKind::Left, &and), JoinKind::Inner);
let half = format!("({kept} OR {padded})::BOOLEAN");
assert_eq!(narrowed(JoinKind::Left, &half), JoinKind::Left);
let both = format!("({padded} OR {padded})::BOOLEAN");
assert_eq!(narrowed(JoinKind::Left, &both), JoinKind::Inner);
}
#[test]
fn a_function_carries_the_null_up_unless_it_is_one_that_swallows_it() {
let strict = "(\"+\"(#1.0::INTEGER, 1::INTEGER)::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, strict), JoinKind::Inner);
let tolerant = "(coalesce(#1.0::INTEGER, 0::INTEGER)::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, tolerant), JoinKind::Left);
}
#[test]
fn a_full_join_loses_one_kind_of_padded_row_at_a_time() {
let left = "(#0.0::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Full, left), JoinKind::Left);
let right = "(#1.0::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Full, right), JoinKind::Right);
let both = "(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Full, both), JoinKind::Inner);
}
#[test]
fn a_right_join_is_the_left_one_with_the_sides_swapped() {
let padded = "(#0.0::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Right, padded), JoinKind::Inner);
let kept = "(#1.0::INTEGER > 5::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Right, kept), JoinKind::Right);
}
#[test]
fn a_join_that_pads_nothing_is_handed_back_as_it_was() {
let predicate = "(#1.0::INTEGER > 5::INTEGER)::BOOLEAN";
for kind in [
JoinKind::Inner,
JoinKind::Semi,
JoinKind::Anti,
JoinKind::Single,
JoinKind::Positional,
] {
assert_eq!(narrowed(kind, predicate), kind);
}
}
#[test]
fn a_case_is_not_read() {
let predicate = "\
(CASE WHEN (#1.0::INTEGER IS NOT DISTINCT FROM NULL::\"NULL\")::BOOLEAN \
THEN 0::INTEGER ELSE 1::INTEGER END::INTEGER = 1::INTEGER)::BOOLEAN";
assert_eq!(narrowed(JoinKind::Left, predicate), JoinKind::Left);
}
#[test]
fn coalesce_is_the_one_function_that_may_not_be_assumed_strict() {
assert!(super::TOLERANT.contains(&"coalesce"));
}
}