use rudb_common::{LogicalType, Result, Span};
use rudb_plan::{ColumnBinding, Expr, ExprRef, Node, NodeRef, Plan, Slice};
use crate::pass::{Context, Pass};
use crate::walk;
const ANSWERABLE: [&str; 5] = ["avg", "count_star", "max", "min", "sum"];
#[derive(Debug, Clone, Copy)]
pub struct AnswersFromTheKey;
impl Pass for AnswersFromTheKey {
fn name(&self) -> &'static str {
"answers_from_the_key"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
collapse_all(plan);
Ok(())
}
}
pub fn collapse_all(plan: &mut Plan) {
let mut moved = false;
let root = walk::restack(plan, plan.root(), &mut moved, &mut collapse);
if moved {
plan.set_root(root);
}
}
fn collapse(plan: &mut Plan, at: NodeRef) -> Option<NodeRef> {
let Node::Aggregate { input, index, groups, aggregates } = *plan.node(at) else { return None };
let keys = plan.expr_list(groups).to_vec();
let calls = plan.expr_list(aggregates).to_vec();
if keys.is_empty() || calls.is_empty() {
return None;
}
let mut read = keys.clone();
for &call in &calls {
let Expr::Aggregate { name, args, distinct, filter } = *plan.expr(call) else {
return None;
};
if distinct || filter.is_some() || !ANSWERABLE.contains(&plan.string(name)) {
return None;
}
read.extend_from_slice(plan.expr_list(args));
}
if calls.iter().all(|&call| counts_rows(plan, call)) {
return None;
}
if !read.iter().all(|&expr| walk::elementwise(plan, expr) && !walk::volatile(plan, expr)) {
return None;
}
let (source, reference) = source_column(plan, &read)?;
if !keys.iter().any(|&key| holds(plan, key, source)) {
return None;
}
if !keys.iter().all(|&key| holds(plan, key, source) || reads_nothing(plan, key)) {
return None;
}
let below = walk::fresh_index(plan);
let span = plan.expr_span(reference);
let ty = plan.expr_type(reference).clone();
let key = plan.add_expr_at(Expr::Column(source), ty.clone(), span);
let groups = plan.add_expr_list(&[key]);
let counted = plan.intern("count_star");
let weight =
Expr::Aggregate { name: counted, args: Slice::EMPTY, distinct: false, filter: None };
let weight = plan.add_expr_at(weight, LogicalType::BigInt, span);
let aggregates = plan.add_expr_list(&[weight]);
let counting = plan.add_node(Node::Aggregate { input, index: below, groups, aggregates });
let value = plan.add_expr_at(Expr::Column(ColumnBinding::new(below, 0)), ty, span);
let count =
plan.add_expr_at(Expr::Column(ColumnBinding::new(below, 1)), LogicalType::BigInt, span);
let outputs = answers(plan, &keys, &calls, source, value, count)?;
let names: Vec<_> =
(0..outputs.len()).map(|position| plan.intern(&format!("column{position}"))).collect();
let exprs = plan.add_expr_list(&outputs);
let names = plan.add_name_list(&names);
Some(plan.add_node(Node::Project { input: counting, index, exprs, names }))
}
fn answers(
plan: &mut Plan,
keys: &[ExprRef],
calls: &[ExprRef],
source: ColumnBinding,
value: ExprRef,
count: ExprRef,
) -> Option<Vec<ExprRef>> {
let mut outputs: Vec<ExprRef> = Vec::with_capacity(keys.len() + calls.len());
for &key in keys {
let key = replace(plan, key, source, value);
outputs.push(key);
}
for &call in calls {
let Expr::Aggregate { name, args, .. } = *plan.expr(call) else { return None };
let name = plan.string(name).to_owned();
let want = plan.expr_type(call).clone();
let span = plan.expr_span(call);
let argument = plan.expr_list(args).first().copied();
let written = match name.as_str() {
"count_star" => count,
"min" | "max" => replace(plan, argument?, source, value),
"sum" => {
let each = replace(plan, argument?, source, value);
weighted(plan, each, count, span)?
}
"avg" => {
let each = replace(plan, argument?, source, value);
let total = weighted(plan, each, count, span)?;
divided(plan, total, count, span)?
}
_ => return None,
};
let written = cast(plan, written, &want, span);
outputs.push(written);
}
Some(outputs)
}
fn source_column(plan: &Plan, exprs: &[ExprRef]) -> Option<(ColumnBinding, ExprRef)> {
let mut found: Option<(ColumnBinding, ExprRef)> = None;
let mut mixed = false;
for &expr in exprs {
walk::columns_at(plan, expr, &mut |at, binding| match found {
None => found = Some((binding, at)),
Some((held, _)) if held == binding => {}
Some(_) => mixed = true,
});
}
if mixed { None } else { found }
}
fn holds(plan: &Plan, expr: ExprRef, source: ColumnBinding) -> bool {
matches!(*plan.expr(expr), Expr::Column(binding) if binding == source)
}
fn reads_nothing(plan: &Plan, expr: ExprRef) -> bool {
let mut any = false;
walk::columns(plan, expr, &mut |_| any = true);
!any
}
fn counts_rows(plan: &Plan, call: ExprRef) -> bool {
matches!(*plan.expr(call), Expr::Aggregate { name, .. } if plan.string(name) == "count_star")
}
fn replace(plan: &mut Plan, expr: ExprRef, source: ColumnBinding, value: ExprRef) -> ExprRef {
if let Expr::Column(binding) = *plan.expr(expr) {
return if binding == source { value } else { expr };
}
walk::rebuild(plan, expr, &mut |plan, inner| replace(plan, inner, source, value))
}
fn weighted(plan: &mut Plan, each: ExprRef, weight: ExprRef, span: Span) -> Option<ExprRef> {
if !plan.expr_type(each).is_integer() {
return None;
}
let left = cast(plan, each, &LogicalType::HugeInt, span);
let right = cast(plan, weight, &LogicalType::HugeInt, span);
let product = scalar(plan, "*", &[left, right], span)?;
(plan.expr_type(product) == &LogicalType::HugeInt).then_some(product)
}
fn divided(plan: &mut Plan, total: ExprRef, seen: ExprRef, span: Span) -> Option<ExprRef> {
let left = cast(plan, total, &LogicalType::Double, span);
let right = cast(plan, seen, &LogicalType::Double, span);
let ratio = scalar(plan, "/", &[left, right], span)?;
(plan.expr_type(ratio) == &LogicalType::Double).then_some(ratio)
}
fn scalar(plan: &mut Plan, name: &str, args: &[ExprRef], span: Span) -> Option<ExprRef> {
let given: Vec<LogicalType> = args.iter().map(|&arg| plan.expr_type(arg).clone()).collect();
let resolved = rudb_functions::resolve(name, &given).ok()?;
if resolved.arguments.len() != args.len() {
return None;
}
let mut cast_to: Vec<ExprRef> = Vec::with_capacity(args.len());
for (&arg, wanted) in args.iter().zip(&resolved.arguments) {
let wanted = wanted.clone();
let arg = cast(plan, arg, &wanted, span);
cast_to.push(arg);
}
let name = plan.intern(resolved.name);
let args = plan.add_expr_list(&cast_to);
Some(plan.add_expr_at(Expr::Function { name, args }, resolved.returns, span))
}
fn cast(plan: &mut Plan, expr: ExprRef, to: &LogicalType, span: Span) -> ExprRef {
if plan.expr_type(expr) == to {
return expr;
}
plan.add_expr_at(Expr::Cast { input: expr, try_cast: false }, to.clone(), span)
}
#[cfg(test)]
mod tests {
use rudb_plan::Plan;
use super::collapse_all;
fn collapsed(text: &str) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
collapse_all(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
#[test]
fn an_extreme_over_the_group_key_becomes_the_group_key() {
let before = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[min(#0.0::VARCHAR)::VARCHAR, count_star()::BIGINT]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
let after = collapsed(before);
assert!(after.contains("Project #1"), "{after}");
assert!(
after.contains("Aggregate #2 groups=[#0.0::VARCHAR] aggregates=[count_star()::BIGINT]"),
"{after}"
);
assert!(!after.contains("min("), "the smallest of one value is that value: {after}");
}
#[test]
fn a_sum_over_the_group_key_becomes_the_key_times_its_count() {
let before = concat!(
"Aggregate #1 groups=[#0.1::INTEGER] aggregates=[sum(#0.1::INTEGER)::HUGEINT]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
let after = collapsed(before);
assert!(after.contains("Project #1"), "{after}");
assert!(after.contains("\"*\"("), "{after}");
assert!(!after.contains("sum("), "{after}");
}
#[test]
fn a_mean_over_the_group_key_is_the_total_over_the_count_the_accumulator_would_have_held() {
let before = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[avg(length(#0.0::VARCHAR)::BIGINT)::DOUBLE]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
let after = collapsed(before);
assert!(after.contains("\"*\"("), "the total carries the count: {after}");
assert!(after.contains("\"/\"("), "one division at the end: {after}");
assert!(!after.contains("avg("), "{after}");
}
#[test]
fn an_expression_over_the_group_key_is_run_once_per_group() {
let before = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[max(upper(#0.0::VARCHAR)::VARCHAR)::VARCHAR]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
let after = collapsed(before);
assert!(after.contains("Project #1 [#2.0::VARCHAR AS column0"), "{after}");
assert!(after.contains("upper(#2.0::VARCHAR)"), "{after}");
assert!(!after.contains("max("), "{after}");
}
#[test]
fn a_constant_beside_the_key_groups_the_same_rows_and_comes_back_in_place() {
let before = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR, 7::INTEGER] aggregates=[min(#0.0::VARCHAR)::VARCHAR]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
let after = collapsed(before);
assert!(
after.contains("Aggregate #2 groups=[#0.0::VARCHAR] aggregates=[count_star()::BIGINT]"),
"the constant does not split a group: {after}"
);
assert!(after.contains("7::INTEGER AS column1"), "{after}");
}
#[test]
fn a_grouped_count_is_left_alone_because_it_is_already_the_shape_this_aims_at() {
let text = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[count_star()::BIGINT]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
assert_eq!(collapsed(text), text);
}
#[test]
fn a_group_key_that_is_a_function_of_the_column_is_left_alone() {
let text = concat!(
"Aggregate #1 groups=[upper(#0.0::VARCHAR)::VARCHAR] aggregates=[min(#0.0::VARCHAR)::VARCHAR]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
assert_eq!(collapsed(text), text, "two referers can share one upper case");
}
#[test]
fn an_aggregate_over_a_second_column_is_left_alone() {
let text = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[min(#0.1::INTEGER)::INTEGER]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
assert_eq!(collapsed(text), text, "the group holds one a and many bs");
}
#[test]
fn an_ungrouped_aggregate_is_left_alone() {
let text = concat!(
"Aggregate #1 groups=[] aggregates=[min(#0.0::VARCHAR)::VARCHAR]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
assert_eq!(collapsed(text), text);
}
#[test]
fn a_distinct_call_is_left_alone() {
let text = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[min(DISTINCT #0.0::VARCHAR)::VARCHAR]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
assert_eq!(collapsed(text), text);
}
#[test]
fn a_sum_over_a_double_is_left_alone() {
let text = concat!(
"Aggregate #1 groups=[#0.2::DOUBLE] aggregates=[sum(#0.2::DOUBLE)::DOUBLE, count_star()::BIGINT]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER, c::DOUBLE]\n",
);
assert_eq!(collapsed(text), text, "n doubles added up is not a double times n");
}
#[test]
fn running_it_twice_is_running_it_once() {
let text = concat!(
"Aggregate #1 groups=[#0.0::VARCHAR] aggregates=[min(#0.0::VARCHAR)::VARCHAR]\n",
" Get memory.main.t AS t #0 [a::VARCHAR, b::INTEGER]\n",
);
let once = collapsed(text);
assert_ne!(once, text);
assert_eq!(collapsed(&once), once);
}
}