use std::hash::{BuildHasherDefault, Hash, Hasher};
use std::sync::{Arc, Mutex, RwLock};
use ahash::AHasher;
use hashbrown::hash_map::RawEntryMut;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use radixdb_core::{CompactArc, CompactVec, I64Map, StringMap};
use radixdb_core::{Error, Result, Row, RowVec, Value, ValueMap, ValueSet};
use radixdb_functions::aggregate::{numeric::NumericAccumulator, CompiledAggregate};
use radixdb_functions::{AggregateFunction, AggregateOrderBySpec, FunctionRegistry};
use radixdb_sql::ast::*;
use radixdb_storage::mvcc::engine::MVCCEngine;
use radixdb_storage::traits::{Engine, QueryResult};
use super::compiled_plan::{CompiledCountDistinct, CompiledExecution};
use super::context::ExecutionContext;
#[allow(deprecated)]
use super::expression::CompiledEvaluator;
use super::expression::{ExpressionEval, RowFilter};
use super::mutation::host::ActiveTransaction;
use super::query_classification::QueryClassification;
use super::result::ExecutorResult;
use super::utils::build_column_index_map;
pub use super::utils::{expression_contains_aggregate, is_aggregate_function};
mod execute;
mod finalize;
mod global;
mod grouped;
mod planning;
mod rollup;
mod storage;
mod streaming;
#[cfg(test)]
mod tests;
pub trait AggregationHost: Sync {
fn aggregation_engine(&self) -> &Arc<MVCCEngine>;
fn aggregation_function_registry(&self) -> &FunctionRegistry;
fn aggregation_active_transaction(&self) -> &Mutex<Option<ActiveTransaction>>;
fn aggregation_process_where_subqueries(
&self,
expression: &Expression,
context: &ExecutionContext,
) -> Result<Expression>;
fn aggregation_try_process_select_subqueries(
&self,
columns: &[Expression],
context: &ExecutionContext,
) -> Result<Option<Vec<Expression>>>;
fn aggregation_has_correlated_subqueries(&self, expression: &Expression) -> bool;
fn aggregation_process_correlated_expression(
&self,
expression: &Expression,
context: &ExecutionContext,
) -> Result<Expression>;
fn aggregation_output_column_names(
&self,
select_expressions: &[Expression],
source_columns: &[String],
table_alias: Option<&str>,
) -> Vec<String>;
}
pub struct AggregationExecutor<'a, H: AggregationHost + ?Sized> {
host: &'a H,
}
impl<'a, H: AggregationHost + ?Sized> AggregationExecutor<'a, H> {
fn new(host: &'a H) -> Self {
Self { host }
}
}
pub trait AggregationExecutorExt: AggregationHost {
fn execute_select_with_aggregation(
&self,
statement: &SelectStatement,
context: &ExecutionContext,
rows: RowVec,
columns: &[String],
) -> Result<Box<dyn QueryResult>> {
AggregationExecutor::new(self)
.execute_select_with_aggregation(statement, context, rows, columns)
}
fn execute_aggregation_for_window(
&self,
statement: &SelectStatement,
context: &ExecutionContext,
rows: &[(i64, Row)],
columns: &[String],
) -> Result<(Vec<String>, RowVec)> {
AggregationExecutor::new(self)
.execute_aggregation_for_window(statement, context, rows, columns)
}
fn try_aggregation_pushdown(
&self,
table: &dyn radixdb_storage::traits::Table,
statement: &SelectStatement,
context: &ExecutionContext,
classification: &Arc<QueryClassification>,
) -> Result<Option<Box<dyn QueryResult>>> {
AggregationExecutor::new(self).try_aggregation_pushdown(
table,
statement,
context,
classification,
)
}
fn try_filtered_aggregation_pushdown(
&self,
table: &dyn radixdb_storage::traits::Table,
statement: &SelectStatement,
context: &ExecutionContext,
classification: &Arc<QueryClassification>,
columns: &[String],
) -> Result<Option<Box<dyn QueryResult>>> {
AggregationExecutor::new(self).try_filtered_aggregation_pushdown(
table,
statement,
context,
classification,
columns,
)
}
fn try_streaming_global_aggregation(
&self,
table: &dyn radixdb_storage::traits::Table,
statement: &SelectStatement,
context: &ExecutionContext,
classification: &Arc<QueryClassification>,
) -> Result<Option<Box<dyn QueryResult>>> {
AggregationExecutor::new(self).try_streaming_global_aggregation(
table,
statement,
context,
classification,
)
}
fn try_streaming_derived_table_aggregation(
&self,
source: Box<dyn QueryResult>,
statement: &SelectStatement,
classification: &Arc<QueryClassification>,
context: &ExecutionContext,
) -> Result<DerivedAggregationAttempt> {
AggregationExecutor::new(self).try_streaming_derived_table_aggregation(
source,
statement,
classification,
context,
)
}
fn try_storage_aggregation(
&self,
table: &dyn radixdb_storage::traits::Table,
statement: &SelectStatement,
context: &ExecutionContext,
columns: &[String],
classification: &QueryClassification,
) -> Option<Box<dyn QueryResult>> {
AggregationExecutor::new(self).try_storage_aggregation(
table,
statement,
context,
columns,
classification,
)
}
fn try_fast_count_distinct_compiled(
&self,
statement: &SelectStatement,
compiled: &RwLock<CompiledExecution>,
) -> Option<Result<Box<dyn QueryResult>>> {
AggregationExecutor::new(self).try_fast_count_distinct_compiled(statement, compiled)
}
fn try_fast_count_star_compiled(
&self,
statement: &SelectStatement,
compiled: &RwLock<CompiledExecution>,
) -> Option<Result<Box<dyn QueryResult>>> {
AggregationExecutor::new(self).try_fast_count_star_compiled(statement, compiled)
}
}
impl<T: AggregationHost + ?Sized> AggregationExecutorExt for T {}
#[derive(Clone, Debug)]
struct HavingCondition {
agg_index: usize,
op: ComparisonOp,
threshold: f64,
}
#[derive(Clone, Debug)]
struct SimpleHavingFilter {
conditions: Vec<HavingCondition>,
}
#[derive(Clone, Copy, Debug)]
enum ComparisonOp {
Gt,
Gte,
Lt,
Lte,
Eq,
Neq,
}
impl HavingCondition {
fn matches(&self, value: f64) -> bool {
match self.op {
ComparisonOp::Gt => value > self.threshold,
ComparisonOp::Gte => value >= self.threshold,
ComparisonOp::Lt => value < self.threshold,
ComparisonOp::Lte => value <= self.threshold,
ComparisonOp::Eq => (value - self.threshold).abs() < f64::EPSILON,
ComparisonOp::Neq => (value - self.threshold).abs() >= f64::EPSILON,
}
}
}
impl SimpleHavingFilter {
fn single(agg_index: usize, op: ComparisonOp, threshold: f64) -> Self {
Self {
conditions: vec![HavingCondition {
agg_index,
op,
threshold,
}],
}
}
fn and(mut self, other: Self) -> Self {
self.conditions.extend(other.conditions);
self
}
}
#[derive(Clone)]
enum SimpleAgg {
Count(Option<usize>), Sum(usize), Avg(usize), Min(usize), Max(usize), }
impl SimpleAgg {
#[inline]
fn count_includes_row(&self, row: &Row) -> bool {
match self {
Self::Count(None) => true,
Self::Count(Some(column_index)) => {
row.get(*column_index).is_some_and(|value| !value.is_null())
}
_ => false,
}
}
}
pub enum DerivedAggregationAttempt {
Applied(Box<dyn QueryResult>),
Rejected(Box<dyn QueryResult>),
}
struct DerivedAggregationPlan {
group_col_name: String,
group_col_idx: usize,
aggregations: Vec<SqlAggregateFunction>,
simple_aggs: Vec<SimpleAgg>,
}
fn try_parse_simple_having(
having: &Expression,
aggregations: &[SqlAggregateFunction],
) -> Option<SimpleHavingFilter> {
if let Expression::Infix(binop) = having {
if binop.operator.eq_ignore_ascii_case("AND") {
let left = try_parse_simple_having(&binop.left, aggregations)?;
let right = try_parse_simple_having(&binop.right, aggregations)?;
return Some(left.and(right));
}
}
try_parse_single_having_condition(having, aggregations)
.map(|(agg_index, op, threshold)| SimpleHavingFilter::single(agg_index, op, threshold))
}
fn try_parse_single_having_condition(
having: &Expression,
aggregations: &[SqlAggregateFunction],
) -> Option<(usize, ComparisonOp, f64)> {
if let Expression::Infix(binop) = having {
let (op, threshold) = match binop.operator.as_str() {
">" => (ComparisonOp::Gt, extract_numeric_value(&binop.right)?),
">=" => (ComparisonOp::Gte, extract_numeric_value(&binop.right)?),
"<" => (ComparisonOp::Lt, extract_numeric_value(&binop.right)?),
"<=" => (ComparisonOp::Lte, extract_numeric_value(&binop.right)?),
"=" => (ComparisonOp::Eq, extract_numeric_value(&binop.right)?),
"!=" | "<>" => (ComparisonOp::Neq, extract_numeric_value(&binop.right)?),
_ => return None,
};
if let Expression::FunctionCall(func) = &*binop.left {
let func_upper = func.function.to_uppercase();
if matches!(func_upper.as_str(), "SUM" | "COUNT" | "AVG" | "MIN" | "MAX") {
for (i, agg) in aggregations.iter().enumerate() {
if agg.name.to_uppercase() == func_upper && !agg.distinct {
let col_matches = if func_upper == "COUNT" {
func.arguments.first().is_none_or(|arg| {
matches!(arg, Expression::Star(_))
|| match arg {
Expression::Identifier(id) => {
id.value_lower == agg.column_lower
}
_ => false,
}
})
} else {
func.arguments.first().is_some_and(|arg| match arg {
Expression::Identifier(id) => id.value_lower == agg.column_lower,
_ => false,
})
};
if col_matches {
return Some((i, op, threshold));
}
}
}
}
}
}
None
}
fn extract_numeric_value(expr: &Expression) -> Option<f64> {
match expr {
Expression::IntegerLiteral(lit) => Some(lit.value as f64),
Expression::FloatLiteral(lit) => Some(lit.value),
Expression::Prefix(unary) if unary.operator == "-" => {
extract_numeric_value(&unary.right).map(|v| -v)
}
_ => None,
}
}
#[derive(Clone, Debug)]
struct GroupingSet {
active_columns: Vec<bool>,
}
fn expression_canonical_key(expr: &Expression) -> String {
match expr {
Expression::Identifier(id) => id.value_lower.to_string(),
Expression::QualifiedIdentifier(qid) => {
format!("{}.{}", qid.qualifier.value_lower, qid.name.value_lower)
}
Expression::IntegerLiteral(lit) => format!("$pos:{}", lit.value),
Expression::FloatLiteral(lit) => format!("$float:{}", lit.value),
Expression::StringLiteral(lit) => format!("$str:{}", lit.value.to_lowercase()),
Expression::BooleanLiteral(lit) => format!("$bool:{}", lit.value),
Expression::FunctionCall(func) => {
let args: Vec<String> = func
.arguments
.iter()
.map(expression_canonical_key)
.collect();
format!("{}({})", func.function.to_lowercase(), args.join(","))
}
Expression::Infix(bin) => {
format!(
"({} {} {})",
expression_canonical_key(&bin.left),
bin.operator.to_lowercase(),
expression_canonical_key(&bin.right)
)
}
Expression::Prefix(un) => {
format!(
"({}{})",
un.operator.to_lowercase(),
expression_canonical_key(&un.right)
)
}
Expression::Aliased(aliased) => {
expression_canonical_key(&aliased.expression)
}
_ => format!("{}", expr).to_lowercase(),
}
}
fn group_by_item_canonical_key(item: &GroupByItem) -> String {
match item {
GroupByItem::Column(name) => name.to_lowercase(),
GroupByItem::Position(pos) => format!("$pos:{}", pos),
GroupByItem::Expression { expr, .. } => expression_canonical_key(expr),
}
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum GroupByItem {
Column(String),
Position(usize),
Expression {
expr: Expression,
display_name: String,
},
}
#[derive(Clone, Debug)]
enum ColumnSource {
AggColumn(String),
Expression(Box<Expression>),
CorrelatedExpression(Box<Expression>),
GroupingFlag(usize),
}
#[inline]
fn hash_group_key(values: &[Value]) -> u64 {
let mut hasher = AHasher::default();
for v in values {
v.hash(&mut hasher);
}
hasher.finish()
}
#[inline]
fn track_distinct_value(seen: &mut ValueSet, value: &Value) -> bool {
seen.insert(value.clone())
}
struct GroupEntry {
key_values: Vec<Value>,
row_indices: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct SqlAggregateFunction {
pub name: String,
pub column: String,
pub column_lower: String,
pub alias: Option<String>,
pub distinct: bool,
pub extra_args: Vec<Value>,
pub expression: Option<Expression>,
pub order_by: Vec<radixdb_sql::ast::OrderByExpression>,
pub filter: Option<Expression>,
pub hidden: bool,
}
impl SqlAggregateFunction {
pub fn get_column_name(&self) -> String {
if let Some(ref alias) = self.alias {
alias.clone()
} else if self.column == "*" {
format!("{}(*)", self.name)
} else if self.extra_args.is_empty() {
format!("{}({})", self.name, self.column)
} else {
let args_str: Vec<String> = std::iter::once(self.column.clone())
.chain(self.extra_args.iter().map(|v| match v {
Value::Text(s) => format!("'{}'", s),
other => other.to_string(),
}))
.collect();
format!("{}({})", self.name, args_str.join(", "))
}
}
pub fn get_expression_name(&self) -> String {
if self.column == "*" {
format!("{}(*)", self.name)
} else {
format!("{}({})", self.name, self.column)
}
}
}