use rudb_common::{Field, Result};
use rudb_plan::{Expr, ExprRef, Node, NodeRef, Plan, Slice};
use crate::pass::{Context, Pass};
#[derive(Debug, Clone, Copy)]
pub struct EmptyResultPullup;
impl Pass for EmptyResultPullup {
fn name(&self) -> &'static str {
"empty_result_pullup"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
prune(plan);
Ok(())
}
}
pub fn prune(plan: &mut Plan) {
let root = plan.root();
walk(plan, root);
}
fn walk(plan: &mut Plan, at: NodeRef) {
if !already_empty(plan, at) && empty(plan, at) {
if let Some((index, columns)) = columns_of(plan, at) {
let rows = plan.add_rows(&[]);
*plan.node_mut(at) = Node::Values { index, columns, rows };
return;
}
}
for child in plan.node(at).children().into_iter().flatten() {
walk(plan, child);
}
}
fn already_empty(plan: &Plan, at: NodeRef) -> bool {
match *plan.node(at) {
Node::Values { rows, .. } => plan.row_list(rows).is_empty(),
_ => false,
}
}
fn empty(plan: &Plan, at: NodeRef) -> bool {
match *plan.node(at) {
Node::Values { rows, .. } => plan.row_list(rows).is_empty(),
Node::Filter { input, predicate } => never(plan, predicate) || empty(plan, input),
Node::Limit { input, count, .. } => count == Some(0) || empty(plan, input),
Node::TopN { input, count, .. } => count == 0 || empty(plan, input),
Node::Sort { input, .. } | Node::Distinct { input, .. } | Node::Project { input, .. } => {
empty(plan, input)
}
_ => false,
}
}
fn never(plan: &Plan, predicate: ExprRef) -> bool {
let Expr::Constant(value) = *plan.expr(predicate) else {
return false;
};
let value = plan.value(value);
value.is_null() || value.as_bool() == Some(false)
}
fn columns_of(plan: &mut Plan, at: NodeRef) -> Option<(u32, Slice)> {
match *plan.node(at) {
Node::Get { index, columns, .. }
| Node::Values { index, columns, .. }
| Node::TableFunction { index, columns, .. } => Some((index, columns)),
Node::Project { index, exprs, names, .. } => {
let exprs = plan.expr_list(exprs).to_vec();
let names = plan.name_list(names).to_vec();
let fields: Vec<Field> = exprs
.iter()
.zip(names)
.map(|(&expr, name)| Field::new(plan.string(name), plan.expr_type(expr).clone()))
.collect();
Some((index, plan.add_fields(&fields)))
}
Node::Filter { input, .. }
| Node::Sort { input, .. }
| Node::Limit { input, .. }
| Node::TopN { input, .. }
| Node::Distinct { input, .. } => columns_of(plan, input),
_ => None,
}
}
#[cfg(test)]
mod tests {
use rudb_plan::Plan;
use super::prune;
fn pruned(text: &str) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
prune(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
#[test]
fn a_false_predicate_takes_the_scan_with_it() {
assert_eq!(
pruned(concat!(
"Filter FALSE::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n",
)),
"Values #0 [a::INTEGER, b::INTEGER] rows=[]\n"
);
}
#[test]
fn a_null_predicate_is_as_empty_as_a_false_one() {
assert_eq!(
pruned(
concat!("Filter NULL::BOOLEAN\n", " Get memory.main.t AS t #0 [a::INTEGER]\n",)
),
"Values #0 [a::INTEGER] rows=[]\n"
);
}
#[test]
fn a_predicate_that_depends_on_the_row_is_left_alone() {
let text = concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
);
assert_eq!(pruned(text), text);
}
#[test]
fn everything_above_the_empty_node_that_passes_rows_through_goes_with_it() {
assert_eq!(
pruned(concat!(
"Project #1 [#0.0::INTEGER AS a]\n",
" Sort [#0.0::INTEGER ASC NULLS LAST]\n",
" Filter FALSE::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)),
"Values #1 [a::INTEGER] rows=[]\n"
);
}
#[test]
fn a_limit_of_no_rows_is_an_empty_relation() {
assert_eq!(
pruned(concat!("Limit 0 offset 0\n", " Get memory.main.t AS t #0 [a::INTEGER]\n",)),
"Values #0 [a::INTEGER] rows=[]\n"
);
assert_eq!(
pruned(concat!(
"TopN 0 offset 0 [#0.0::INTEGER ASC NULLS LAST]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)),
"Values #0 [a::INTEGER] rows=[]\n"
);
}
#[test]
fn a_limit_of_one_row_is_not() {
let text = concat!("Limit 1 offset 0\n", " Get memory.main.t AS t #0 [a::INTEGER]\n",);
assert_eq!(pruned(text), text);
}
#[test]
fn an_aggregate_over_nothing_still_produces_its_row() {
assert_eq!(
pruned(concat!(
"Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n",
" Filter FALSE::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)),
concat!(
"Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n",
" Values #0 [a::INTEGER] rows=[]\n",
)
);
}
#[test]
fn an_empty_side_of_a_join_stays_a_side_of_the_join() {
assert_eq!(
pruned(concat!(
"Join INNER on=[]\n",
" Filter FALSE::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
" Get memory.main.u AS u #1 [x::INTEGER]\n",
)),
concat!(
"Join INNER on=[]\n",
" Values #0 [a::INTEGER] rows=[]\n",
" Get memory.main.u AS u #1 [x::INTEGER]\n",
)
);
}
#[test]
fn a_false_filter_over_a_join_is_refused_rather_than_guessed_at() {
let text = concat!(
"Filter FALSE::BOOLEAN\n",
" Join INNER on=[]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
" Get memory.main.u AS u #1 [x::INTEGER]\n",
);
assert_eq!(pruned(text), text);
}
#[test]
fn running_it_twice_is_running_it_once() {
let text = concat!(
"Project #1 [#0.0::INTEGER AS a]\n",
" Filter FALSE::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
);
let once = pruned(text);
assert_eq!(pruned(&once), once);
}
}