use rudb_common::Result;
use rudb_common::rules::Rule;
use rudb_plan::{ColumnBinding, Expr, Node, NodeRef, Plan};
use crate::pass::{Context, Pass, top_down};
#[derive(Debug)]
pub struct AggregateCluster;
impl Pass for AggregateCluster {
fn name(&self) -> &'static str {
"aggregate_cluster"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
if context.allows(Rule::ClosedGroups) {
cluster(plan);
}
Ok(())
}
}
fn cluster(plan: &mut Plan) {
let mut found = Vec::new();
for node in top_down(plan) {
let Node::Aggregate { input, index, groups, .. } = *plan.node(node) else {
continue;
};
let &[key] = plan.expr_list(groups) else { continue };
let &Expr::Column(binding) = plan.expr(key) else { continue };
if sorted(plan, input, binding, 16) {
found.push(index);
}
}
for index in found {
plan.cluster(index);
}
}
fn sorted(plan: &Plan, node: NodeRef, binding: ColumnBinding, depth: u32) -> bool {
let Some(depth) = depth.checked_sub(1) else { return false };
match *plan.node(node) {
Node::Filter { input, .. } => sorted(plan, input, binding, depth),
Node::Project { input, index, exprs, .. } if index == binding.table => {
let Some(&carried) = plan.expr_list(exprs).get(binding.column as usize) else {
return false;
};
let &Expr::Column(carried) = plan.expr(carried) else { return false };
sorted(plan, input, carried, depth)
}
Node::Get { index, columns, .. } if index == binding.table => plan
.field_list(columns)
.get(binding.column as usize)
.is_some_and(|field| plan.ascending(index, &field.name)),
_ => false,
}
}
#[cfg(test)]
mod tests {
use rudb_plan::Plan;
use super::{AggregateCluster, Rule};
use crate::pass::{Context, Pass};
const SCAN: &str = "Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]";
fn plan(text: &str) -> Plan {
let mut plan = Plan::parse(text).expect("a plan that parses");
plan.mark_ascending(0, "a");
plan
}
fn run(plan: &mut Plan, context: &Context) {
AggregateCluster.run(plan, context).expect("a pass that cannot fail");
}
#[test]
fn a_key_the_table_is_sorted_on_is_clustered() {
let mut plan =
plan(&format!("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n {SCAN}\n"));
run(&mut plan, &Context::new());
assert!(plan.clustered(1));
}
#[test]
fn a_key_the_table_is_not_sorted_on_is_left_alone() {
let mut plan =
plan(&format!("Aggregate #1 groups=[#0.1::INTEGER] aggregates=[]\n {SCAN}\n"));
run(&mut plan, &Context::new());
assert!(!plan.clustered(1));
}
#[test]
fn two_keys_are_left_alone() {
let mut plan = plan(&format!(
"Aggregate #1 groups=[#0.0::INTEGER, #0.1::INTEGER] aggregates=[]\n {SCAN}\n"
));
run(&mut plan, &Context::new());
assert!(!plan.clustered(1));
}
#[test]
fn a_filter_keeps_the_order() {
let mut plan = plan(&format!(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n Filter (#0.1::INTEGER > 3::INTEGER)::BOOLEAN\n {SCAN}\n"
));
run(&mut plan, &Context::new());
assert!(plan.clustered(1));
}
#[test]
fn the_rule_s_own_setting_turns_it_off() {
let mut plan =
plan(&format!("Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n {SCAN}\n"));
let mut context = Context::new();
let mut rules = rudb_common::rules::Rules::default();
rules.set(Rule::ClosedGroups, false);
context.govern(rules);
run(&mut plan, &context);
assert!(!plan.clustered(1));
}
}