use rudb_common::rules::Rule;
use rudb_common::{Result, Value};
use rudb_plan::{ColumnBinding, CompareOp, Expr, ExprRef, Node, NodeRef, Plan, Slice};
use crate::pass::{Context, Pass};
use crate::{columns, eliminate, estimate, fold, link, walk};
#[derive(Debug, Clone, Copy)]
pub struct NoNulls;
impl Pass for NoNulls {
fn name(&self) -> &'static str {
"no_nulls"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
if context.allows(Rule::ValidityFree) && settle(plan) {
columns::prune(plan);
eliminate::sweep(plan, context);
}
Ok(())
}
}
fn settle(plan: &mut Plan) -> bool {
let consumers = link::consumers(plan);
let mut rewrote = false;
for at in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
match *plan.node(at) {
Node::Filter { input, predicate } => {
let decided = decided(plan, input, predicate);
if decided == predicate {
continue;
}
rewrote = true;
let decided = fold::rewritten(plan, decided);
if certain(plan, decided) {
eliminate::stand_in(plan, at, input, &consumers);
continue;
}
match plan.node_mut(at) {
Node::Filter { predicate, .. } => *predicate = decided,
_ => unreachable!("the node was a filter a moment ago"),
}
}
Node::Aggregate { input, aggregates, .. } => {
let Some(rewritten) = counted(plan, input, aggregates) else { continue };
rewrote = true;
match plan.node_mut(at) {
Node::Aggregate { aggregates, .. } => *aggregates = rewritten,
_ => unreachable!("the node was an aggregate a moment ago"),
}
}
_ => {}
}
}
rewrote
}
fn certain(plan: &Plan, expr: ExprRef) -> bool {
matches!(*plan.expr(expr), Expr::Constant(value) if plan.value(value) == &Value::Boolean(true))
}
fn decided(plan: &mut Plan, input: NodeRef, expr: ExprRef) -> ExprRef {
let rebuilt = walk::rebuild(plan, expr, &mut |plan, child| decided(plan, input, child));
let Expr::Compare { op, left, right } = *plan.expr(rebuilt) else { return rebuilt };
let answer = match op {
CompareOp::NotDistinctFrom => false,
CompareOp::DistinctFrom => true,
_ => return rebuilt,
};
let Some(binding) = tested(plan, left, right) else { return rebuilt };
if !estimate::never_null(plan, input, binding) {
return rebuilt;
}
let ty = plan.expr_type(rebuilt).clone();
let span = plan.expr_span(rebuilt);
let value = plan.add_value(Value::Boolean(answer));
plan.add_expr_at(Expr::Constant(value), ty, span)
}
fn tested(plan: &Plan, left: ExprRef, right: ExprRef) -> Option<ColumnBinding> {
let null = |expr| matches!(*plan.expr(expr), Expr::Constant(v) if plan.value(v).is_null());
let column = |expr| match *plan.expr(expr) {
Expr::Column(binding) => Some(binding),
_ => None,
};
column(left).filter(|_| null(right)).or_else(|| column(right).filter(|_| null(left)))
}
fn counted(plan: &mut Plan, input: NodeRef, aggregates: Slice) -> Option<Slice> {
let held = plan.expr_list(aggregates).to_vec();
let mut rewritten = held.clone();
for slot in &mut rewritten {
let Expr::Aggregate { name, args, distinct: false, filter } = *plan.expr(*slot) else {
continue;
};
if plan.string(name) != "count" {
continue;
}
let [argument] = *plan.expr_list(args) else { continue };
let Expr::Column(binding) = *plan.expr(argument) else { continue };
if !estimate::never_null(plan, input, binding) {
continue;
}
let name = plan.intern("count_star");
let ty = plan.expr_type(*slot).clone();
let span = plan.expr_span(*slot);
let aggregate = Expr::Aggregate { name, args: Slice::EMPTY, distinct: false, filter };
*slot = plan.add_expr_at(aggregate, ty, span);
}
(rewritten != held).then(|| plan.add_expr_list(&rewritten))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rudb_common::Stat;
use rudb_common::bounds::{Bound, End, Spread, Test, Zones};
use rudb_common::stat::Provenance;
use rudb_plan::Plan;
use super::settle;
#[derive(Debug)]
struct Stub(Stat<u64>);
impl Stub {
fn new(nulls: Stat<u64>) -> Arc<Self> {
Arc::new(Self(nulls))
}
}
impl Zones for Stub {
fn column(&self, name: &str) -> Option<usize> {
(name == "d").then_some(0)
}
fn surviving(&self, _tests: &[Test]) -> Option<u64> {
None
}
fn spread(&self, _tests: &[Test]) -> Option<Spread> {
None
}
fn extreme(&self, _column: usize, _end: End) -> Stat<Bound> {
Stat::Unknown
}
fn nulls(&self, _column: usize) -> Stat<u64> {
self.0
}
}
fn empty() -> Arc<Stub> {
Stub::new(Stat::exact(0, Provenance::NullCount))
}
fn settled(text: &str, zones: &Arc<Stub>) -> String {
settled_at(text, 0, zones)
}
fn settled_at(text: &str, index: u32, zones: &Arc<Stub>) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_zones(index, Arc::clone(zones) as Arc<dyn Zones>);
settle(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
const NOT_NULL: &str = "Filter (#0.0::INTEGER IS DISTINCT FROM NULL::INTEGER)::BOOLEAN\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
#[test]
fn a_filter_that_only_asks_whether_a_column_with_no_nulls_is_null_goes_away_entirely() {
assert_eq!(settled(NOT_NULL, &empty()), "Get memory.main.t AS t #0 [d::INTEGER]\n");
}
#[test]
fn the_other_half_of_the_question_answers_false_and_the_filter_keeps_every_row_out() {
let text = "Filter (#0.0::INTEGER IS NOT DISTINCT FROM NULL::INTEGER)::BOOLEAN\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(
settled(text, &empty()),
"Filter FALSE::BOOLEAN\n Get memory.main.t AS t #0 [d::INTEGER]\n"
);
}
#[test]
fn a_settled_half_of_a_conjunction_leaves_the_half_that_is_still_a_question() {
let text = "Filter ((#0.0::INTEGER IS DISTINCT FROM NULL::INTEGER)::BOOLEAN AND \
(#0.0::INTEGER > 50::INTEGER)::BOOLEAN)::BOOLEAN\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(
settled(text, &empty()),
"Filter (#0.0::INTEGER > 50::INTEGER)::BOOLEAN\n \
Get memory.main.t AS t #0 [d::INTEGER]\n"
);
}
#[test]
fn a_count_of_a_column_with_no_nulls_is_a_count_of_the_rows() {
let text = "Aggregate #1 groups=[] aggregates=[count(#0.0::INTEGER)::BIGINT]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(
settled(text, &empty()),
"Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n"
);
}
#[test]
fn a_distinct_count_is_not_a_row_count_however_few_nulls_the_column_holds() {
let text = "Aggregate #1 groups=[] aggregates=[count(DISTINCT #0.0::INTEGER)::BIGINT]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(settled(text, &empty()), text);
}
#[test]
fn a_null_count_the_store_estimated_is_not_a_proof_and_nothing_is_rewritten() {
let guessed = Stub::new(Stat::estimated(0, Provenance::NullCount));
assert_eq!(settled(NOT_NULL, &guessed), NOT_NULL);
}
#[test]
fn a_column_the_store_counted_nulls_in_keeps_its_question() {
let some = Stub::new(Stat::exact(7, Provenance::NullCount));
assert_eq!(settled(NOT_NULL, &some), NOT_NULL);
}
#[test]
fn a_table_that_says_nothing_about_its_nulls_keeps_its_question_too() {
assert_eq!(settled(NOT_NULL, &Stub::new(Stat::Unknown)), NOT_NULL);
}
fn padded(kind: &str) -> String {
format!(
"Filter (#1.0::INTEGER IS NOT DISTINCT FROM NULL::INTEGER)::BOOLEAN\n \
Join {kind} on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n \
Get memory.main.u AS u #1 [d::INTEGER]\n"
)
}
#[test]
fn a_question_about_the_side_a_left_join_pads_is_not_the_file_s_question() {
let text = padded("LEFT");
assert_eq!(settled_at(&text, 1, &empty()), text);
}
#[test]
fn the_same_question_over_an_inner_join_is_the_file_s_question_after_all() {
let settled = settled_at(&padded("INNER"), 1, &empty());
assert!(settled.contains("Filter FALSE::BOOLEAN"), "{settled}");
}
#[test]
fn running_it_twice_is_running_it_once() {
let text = "Filter ((#0.0::INTEGER IS DISTINCT FROM NULL::INTEGER)::BOOLEAN AND \
(#0.0::INTEGER > 50::INTEGER)::BOOLEAN)::BOOLEAN\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_zones(0, empty() as Arc<dyn Zones>);
settle(&mut plan);
let once = plan.to_string();
settle(&mut plan);
assert_eq!(plan.to_string(), once, "the pass did not settle");
}
}