use rudb_common::bounds::{Bound, Op};
use rudb_plan::{ColumnBinding, CompareOp, ConjunctionOp, Expr, ExprRef, Node, Plan};
pub type Test = (usize, Op, Bound);
#[must_use]
pub fn of(plan: &Plan, input: rudb_plan::NodeRef, predicate: ExprRef) -> Vec<Test> {
let Some(index) = scanned(plan, input) else { return Vec::new() };
let mut tests = Vec::new();
conjuncts(plan, predicate, index, &mut tests);
tests
}
#[derive(Debug, Clone)]
pub struct Moved {
pub tests: Vec<Test>,
pub whole: bool,
}
#[must_use]
pub fn into_scan(plan: &Plan, filter: rudb_plan::NodeRef) -> Option<Moved> {
let Node::Filter { input, predicate } = *plan.node(filter) else { return None };
if !matches!(*plan.node(input), Node::Get { .. }) {
return None;
}
let index = scanned(plan, input)?;
let mut tests = Vec::new();
if !every_conjunct(plan, predicate, index, &mut tests) {
return Some(Moved { tests: Vec::new(), whole: false });
}
Some(Moved { whole: !tests.is_empty(), tests })
}
#[must_use]
pub fn scanned(plan: &Plan, node: rudb_plan::NodeRef) -> Option<u32> {
match *plan.node(node) {
Node::TableFunction { index, function, .. } => {
let name = plan.string(function);
(rudb_functions::TableFunction::lookup(name)
== Some(rudb_functions::TableFunction::ReadParquet))
.then_some(index)
}
Node::Get { index, .. } => Some(index),
_ => None,
}
}
fn conjuncts(plan: &Plan, predicate: ExprRef, index: u32, out: &mut Vec<Test>) {
match *plan.expr(predicate) {
Expr::Conjunction { op: ConjunctionOp::And, children } => {
for child in plan.expr_list(children) {
conjuncts(plan, *child, index, out);
}
}
Expr::Compare { op, left, right } => {
if let Some(test) = comparison(plan, op, left, right, index) {
out.push(test);
}
}
_ => {}
}
}
fn every_conjunct(plan: &Plan, predicate: ExprRef, index: u32, out: &mut Vec<Test>) -> bool {
match *plan.expr(predicate) {
Expr::Conjunction { op: ConjunctionOp::And, children } => {
plan.expr_list(children).iter().all(|child| every_conjunct(plan, *child, index, out))
}
Expr::Compare { op, left, right } => match comparison(plan, op, left, right, index) {
Some(test) => {
out.push(test);
true
}
None => false,
},
_ => false,
}
}
fn comparison(
plan: &Plan,
op: CompareOp,
left: ExprRef,
right: ExprRef,
index: u32,
) -> Option<Test> {
let op = match op {
CompareOp::Equal => Op::Equal,
CompareOp::Less => Op::Less,
CompareOp::LessOrEqual => Op::LessOrEqual,
CompareOp::Greater => Op::Greater,
CompareOp::GreaterOrEqual => Op::GreaterOrEqual,
CompareOp::NotEqual | CompareOp::DistinctFrom | CompareOp::NotDistinctFrom => return None,
};
let (op, binding, value) = match (plan.expr(left), plan.expr(right)) {
(Expr::Column(binding), Expr::Constant(value)) => (op, *binding, *value),
(Expr::Constant(value), Expr::Column(binding)) => (op.flipped(), *binding, *value),
_ => return None,
};
let ColumnBinding { table, column } = binding;
if table != index {
return None;
}
Some((column as usize, op, Bound::of_value(plan.value(value))?))
}
#[cfg(test)]
mod tests {
use rudb_common::bounds::{Bound, Op};
use rudb_plan::Plan;
use super::of;
fn read(predicate: &str) -> Vec<super::Test> {
let text =
format!("Filter {predicate}\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n");
let plan =
Plan::parse(&text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
let rudb_plan::Node::Filter { input, predicate } = *plan.node(plan.root()) else {
panic!("the root is the filter");
};
of(&plan, input, predicate)
}
fn test(column: usize, op: Op, number: i128) -> super::Test {
(column, op, Bound::Int(number))
}
#[test]
fn a_column_against_a_constant_reads_as_a_test_on_that_column() {
assert_eq!(read("(#0.0::INTEGER < 5::INTEGER)::BOOLEAN"), vec![test(0, Op::Less, 5)]);
assert_eq!(read("(#0.1::INTEGER = 9::INTEGER)::BOOLEAN"), vec![test(1, Op::Equal, 9)]);
}
#[test]
fn the_constant_on_the_left_flips_the_comparison_rather_than_reversing_its_meaning() {
assert_eq!(read("(5::INTEGER < #0.0::INTEGER)::BOOLEAN"), vec![test(0, Op::Greater, 5)]);
assert_eq!(
read("(5::INTEGER >= #0.0::INTEGER)::BOOLEAN"),
vec![test(0, Op::LessOrEqual, 5)]
);
assert_eq!(read("(5::INTEGER = #0.0::INTEGER)::BOOLEAN"), vec![test(0, Op::Equal, 5)]);
}
#[test]
fn every_conjunct_of_an_and_is_read_and_a_disjunction_is_not_walked_into() {
let and = "((#0.0::INTEGER > 1::INTEGER)::BOOLEAN AND (#0.1::INTEGER < 9::INTEGER)::BOOLEAN)::BOOLEAN";
assert_eq!(read(and), vec![test(0, Op::Greater, 1), test(1, Op::Less, 9)]);
let or = "((#0.0::INTEGER > 1::INTEGER)::BOOLEAN OR (#0.1::INTEGER < 9::INTEGER)::BOOLEAN)::BOOLEAN";
assert_eq!(read(or), Vec::new());
}
#[test]
fn a_conjunct_that_is_not_a_test_is_dropped_and_the_rest_are_still_read() {
let mixed = "((#0.0::INTEGER = #0.1::INTEGER)::BOOLEAN AND (#0.1::INTEGER < 9::INTEGER)::BOOLEAN)::BOOLEAN";
assert_eq!(read(mixed), vec![test(1, Op::Less, 9)]);
assert_eq!(read("(#0.0::INTEGER <> 5::INTEGER)::BOOLEAN"), Vec::new());
assert_eq!(read("(#0.0::INTEGER < NULL::INTEGER)::BOOLEAN"), Vec::new());
}
#[test]
fn a_binding_into_some_other_operator_is_not_a_test_on_this_scan() {
assert_eq!(read("(#1.0::INTEGER < 5::INTEGER)::BOOLEAN"), Vec::new());
}
#[test]
fn a_node_that_is_not_a_scan_has_no_bounds_to_ask_about() {
let text = "Filter (#0.0::INTEGER < 5::INTEGER)::BOOLEAN\n Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n Get memory.main.t AS t #0 [a::INTEGER]\n";
let plan = Plan::parse(text).expect("parses");
let rudb_plan::Node::Filter { input, predicate } = *plan.node(plan.root()) else {
panic!("the root is the filter");
};
assert_eq!(of(&plan, input, predicate), Vec::new());
}
}