use rudb_common::bounds::{Bound, End};
use rudb_common::stat::Use;
use rudb_common::{Field, LogicalType, Result, Value};
use rudb_plan::{ColumnBinding, Expr, Node, NodeRef, Plan};
use crate::fold::days_after;
use crate::pass::{Context, Pass};
pub const EXTREME: Use = Use::Answer;
#[derive(Debug, Clone, Copy)]
pub struct StatisticsPropagation;
impl Pass for StatisticsPropagation {
fn name(&self) -> &'static str {
"statistics_propagation"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
fold(plan);
Ok(())
}
}
pub fn fold(plan: &mut Plan) {
for at in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
if let Some((index, fields, values)) = answered(plan, at) {
let row: Vec<_> = fields
.iter()
.zip(values)
.map(|(field, value)| {
let value = plan.add_value(value);
plan.add_expr(Expr::Constant(value), field.ty.clone())
})
.collect();
let columns = plan.add_fields(&fields);
let row = plan.add_expr_list(&row);
let rows = plan.add_rows(&[row]);
*plan.node_mut(at) = Node::Values { index, columns, rows };
}
}
}
fn answered(plan: &Plan, at: NodeRef) -> Option<(u32, Vec<Field>, Vec<Value>)> {
let Node::Aggregate { input, index, groups, aggregates } = *plan.node(at) else {
return None;
};
if !plan.expr_list(groups).is_empty() {
return None;
}
let aggregates = plan.expr_list(aggregates).to_vec();
if aggregates.is_empty() {
return None;
}
let mut fields = Vec::with_capacity(aggregates.len());
let mut values = Vec::with_capacity(aggregates.len());
for aggregate in aggregates {
let Expr::Aggregate { name, args, filter, .. } = *plan.expr(aggregate) else {
return None;
};
if filter.is_some() {
return None;
}
let end = match plan.string(name) {
"min" => End::Low,
"max" => End::High,
_ => return None,
};
let &[arg] = plan.expr_list(args) else { return None };
let &Expr::Column(binding) = plan.expr(arg) else { return None };
let ty = plan.expr_type(aggregate).clone();
let value = extreme(plan, input, binding.table, binding.column as usize, end, &ty)?;
fields.push(Field::new(plan.string(name), ty));
values.push(value);
}
Some((index, fields, values))
}
fn extreme(
plan: &Plan,
input: NodeRef,
table: u32,
column: usize,
end: End,
ty: &LogicalType,
) -> Option<Value> {
let (index, name, origin) = scanned(plan, input, table, column, None, 16)?;
let zones = plan.zones(index)?;
let stat = zones.extreme(zones.column(&name)?, end);
let bound = stat.read(EXTREME)?;
match origin {
None => bound.into_value(ty),
Some(origin) => {
let &Bound::Int(count) = bound else { return None };
let day = i32::try_from(i128::from(origin) + count).ok()?;
(*ty == LogicalType::Date).then_some(Value::Date(day))
}
}
}
pub(crate) fn span(plan: &Plan, binding: ColumnBinding, depth: u32) -> Option<(i128, i128)> {
let depth = depth.checked_sub(1)?;
for at in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
match *plan.node(at) {
Node::Get { index, columns, .. } if index == binding.table => {
let name = &plan.field_list(columns).get(binding.column as usize)?.name;
let zones = plan.zones(index)?;
let column = zones.column(name)?;
let low = zones.extreme(column, End::Low);
let high = zones.extreme(column, End::High);
return match (low.read(SPAN)?, high.read(SPAN)?) {
(&Bound::Int(low), &Bound::Int(high)) if low <= high => Some((low, high)),
_ => None,
};
}
Node::Project { index, exprs, .. } if index == binding.table => {
let &carried = plan.expr_list(exprs).get(binding.column as usize)?;
let &Expr::Column(carried) = plan.expr(carried) else { return None };
return span(plan, carried, depth);
}
_ => {}
}
}
None
}
const SPAN: Use = Use::Decide;
fn scanned(
plan: &Plan,
at: NodeRef,
table: u32,
column: usize,
origin: Option<i32>,
depth: u32,
) -> Option<(u32, String, Option<i32>)> {
let depth = depth.checked_sub(1)?;
match *plan.node(at) {
Node::Get { index, columns, .. } | Node::TableFunction { index, columns, .. }
if index == table =>
{
Some((index, plan.field_list(columns).get(column)?.name.clone(), origin))
}
Node::Project { index, input, exprs, .. } if index == table => {
let &carried = plan.expr_list(exprs).get(column)?;
let (binding, origin) = match *plan.expr(carried) {
Expr::Column(binding) => (binding, origin),
_ if origin.is_none() => {
let (day, count) = days_after(plan, carried)?;
let count = match *plan.expr(count) {
Expr::Cast { input, .. } => input,
_ => count,
};
let &Expr::Column(binding) = plan.expr(count) else { return None };
(binding, Some(day))
}
_ => return None,
};
scanned(plan, input, binding.table, binding.column as usize, origin, depth)
}
_ => None,
}
}
#[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::fold;
#[derive(Debug)]
struct Stub {
low: Stat<Bound>,
high: Stat<Bound>,
}
impl Stub {
fn exact(low: i128, high: i128) -> Arc<Self> {
Arc::new(Self {
low: Stat::exact(Bound::Int(low), Provenance::ZoneMap),
high: Stat::exact(Bound::Int(high), Provenance::ZoneMap),
})
}
}
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> {
match end {
End::Low => self.low.clone(),
End::High => self.high.clone(),
}
}
fn nulls(&self, _column: usize) -> Stat<u64> {
Stat::Unknown
}
}
fn folded(text: &str, zones: &Arc<Stub>) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
plan.set_zones(0, Arc::clone(zones) as Arc<dyn Zones>);
fold(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
#[test]
fn a_minimum_and_a_maximum_over_a_whole_column_become_the_two_numbers_the_bounds_hold() {
let text = "Aggregate #1 groups=[] aggregates=[min(#0.0::INTEGER)::INTEGER, \
max(#0.0::INTEGER)::INTEGER]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(
folded(text, &Stub::exact(3, 91)),
"Values #1 [min::INTEGER, max::INTEGER] rows=[[3::INTEGER, 91::INTEGER]]\n"
);
}
#[test]
fn a_projection_between_the_two_is_followed_through_and_the_renamed_column_still_answers() {
let text = "Aggregate #2 groups=[] aggregates=[min(#1.0::INTEGER)::INTEGER]\n \
Project #1 [#0.0::INTEGER AS renamed]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(
folded(text, &Stub::exact(3, 91)),
"Values #2 [min::INTEGER] rows=[[3::INTEGER]]\n"
);
}
#[test]
fn a_date_made_from_a_count_of_days_answers_from_the_counts_bounds_moved_by_the_origin() {
let text = "Aggregate #2 groups=[] aggregates=[min(#1.0::DATE)::DATE, max(#1.0::DATE)::DATE]\n \
Project #1 [\"+\"(10::DATE, CAST(#0.0::USMALLINT)::INTEGER)::DATE AS d]\n \
Get memory.main.t AS t #0 [d::USMALLINT]\n";
assert_eq!(
folded(text, &Stub::exact(15_887, 15_917)),
"Values #2 [min::DATE, max::DATE] rows=[[15897::DATE, 15927::DATE]]\n"
);
}
#[test]
fn a_date_made_from_a_bare_integer_is_left_to_the_scan_since_the_sum_could_raise() {
let text = "Aggregate #2 groups=[] aggregates=[min(#1.0::DATE)::DATE]\n \
Project #1 [\"+\"(0::DATE, #0.0::INTEGER)::DATE AS d]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(folded(text, &Stub::exact(3, 91)), text);
}
#[test]
fn a_filter_between_the_two_stops_it_because_the_bounds_are_the_whole_columns() {
let text = "Aggregate #1 groups=[] aggregates=[min(#0.0::INTEGER)::INTEGER]\n \
Filter (#0.0::INTEGER > 50::INTEGER)::BOOLEAN\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(folded(text, &Stub::exact(3, 91)), text);
}
#[test]
fn a_bound_the_writer_widened_is_not_an_answer_however_good_a_bound_it_is() {
let text = "Aggregate #1 groups=[] aggregates=[min(#0.0::INTEGER)::INTEGER]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
let widened = Arc::new(Stub { low: Stat::Unknown, high: Stat::Unknown });
assert_eq!(folded(text, &widened), text);
}
#[test]
fn one_aggregate_the_bounds_cannot_answer_keeps_the_scan_for_all_of_them() {
let text = "Aggregate #1 groups=[] aggregates=[min(#0.0::INTEGER)::INTEGER, \
sum(#0.0::INTEGER)::HUGEINT]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(folded(text, &Stub::exact(3, 91)), text);
}
#[test]
fn a_group_by_is_not_this_even_when_every_aggregate_in_it_is_a_minimum() {
let text = "Aggregate #1 groups=[#0.0::INTEGER] aggregates=[min(#0.0::INTEGER)::INTEGER]\n \
Get memory.main.t AS t #0 [d::INTEGER]\n";
assert_eq!(folded(text, &Stub::exact(3, 91)), text);
}
#[test]
fn a_table_that_kept_no_bounds_at_all_is_left_to_the_scan() {
let text = "Aggregate #1 groups=[] aggregates=[min(#0.0::INTEGER)::INTEGER]\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}"));
fold(&mut plan);
assert_eq!(plan.to_string(), text);
}
}