#[cfg(feature = "parallel")]
use ahash::AHashMap;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[cfg(feature = "parallel")]
use vibesql_storage::Row;
#[cfg(feature = "parallel")]
use vibesql_types::SqlValue;
#[cfg(feature = "parallel")]
use super::aggregates::AggregateAccumulator;
#[cfg(feature = "parallel")]
use crate::errors::ExecutorError;
#[cfg(feature = "parallel")]
use crate::evaluator::CombinedExpressionEvaluator;
#[cfg(feature = "parallel")]
use crate::schema::CombinedSchema;
#[cfg(feature = "parallel")]
use crate::timeout::TimeoutContext;
#[cfg(feature = "parallel")]
#[allow(dead_code)] struct ThreadLocalAggregationState {
groups: AHashMap<Vec<SqlValue>, Vec<AggregateAccumulator>>,
}
#[cfg(feature = "parallel")]
#[allow(dead_code)] impl ThreadLocalAggregationState {
fn new() -> Self {
Self { groups: AHashMap::new() }
}
}
#[cfg(feature = "parallel")]
#[allow(dead_code)] #[allow(clippy::type_complexity)] pub fn parallel_group_aggregate<'a>(
rows: &[Row],
group_by_exprs: &[vibesql_ast::Expression],
aggregate_infos: &[AggregateInfo],
schema: &'a CombinedSchema,
database: &'a vibesql_storage::Database,
) -> Result<Vec<(Vec<SqlValue>, Vec<SqlValue>)>, ExecutorError> {
use crate::select::parallel::ParallelConfig;
let config = ParallelConfig::global();
if !config.should_parallelize_aggregate(rows.len()) {
return sequential_group_aggregate(rows, group_by_exprs, aggregate_infos, schema, database);
}
let timeout_ctx = TimeoutContext::new_default();
timeout_ctx.check()?;
let num_threads = rayon::current_num_threads();
let chunk_size = rows.len().div_ceil(num_threads);
let thread_local_results: Vec<
Result<AHashMap<Vec<SqlValue>, Vec<AggregateAccumulator>>, ExecutorError>,
> = rows
.par_chunks(chunk_size.max(1))
.map(|chunk| {
let evaluator = CombinedExpressionEvaluator::with_database(schema, database);
let mut state = ThreadLocalAggregationState::new();
for row in chunk {
evaluator.clear_cse_cache();
let mut key = Vec::with_capacity(group_by_exprs.len());
for expr in group_by_exprs {
let value = evaluator.eval(expr, row)?;
key.push(value);
}
let accumulators = state.groups.entry(key).or_insert_with(|| {
aggregate_infos
.iter()
.map(|info| AggregateAccumulator::new(&info.function_name, info.distinct))
.collect::<Result<Vec<_>, _>>()
.unwrap_or_default()
});
for (i, info) in aggregate_infos.iter().enumerate() {
if i < accumulators.len() {
let value = evaluator.eval(&info.expr, row)?;
accumulators[i].accumulate(&value);
}
}
}
Ok(state.groups)
})
.collect();
timeout_ctx.check()?;
let mut thread_results: Vec<AHashMap<Vec<SqlValue>, Vec<AggregateAccumulator>>> =
Vec::with_capacity(thread_local_results.len());
for result in thread_local_results {
thread_results.push(result?);
}
let mut global_groups: AHashMap<Vec<SqlValue>, Vec<AggregateAccumulator>> = AHashMap::new();
for local_groups in thread_results {
for (key, local_accumulators) in local_groups {
match global_groups.entry(key) {
std::collections::hash_map::Entry::Occupied(mut entry) => {
let global_accumulators = entry.get_mut();
for (i, local_acc) in local_accumulators.into_iter().enumerate() {
if i < global_accumulators.len() {
global_accumulators[i].combine(local_acc)?;
}
}
}
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(local_accumulators);
}
}
}
}
let mut results: Vec<(Vec<SqlValue>, Vec<SqlValue>)> = Vec::with_capacity(global_groups.len());
for (key, accumulators) in global_groups {
let values: Vec<SqlValue> =
accumulators.iter().map(|acc| acc.finalize()).collect::<Result<Vec<_>, _>>()?;
results.push((key, values));
}
Ok(results)
}
#[cfg(feature = "parallel")]
#[allow(dead_code)] #[allow(clippy::type_complexity)] fn sequential_group_aggregate<'a>(
rows: &[Row],
group_by_exprs: &[vibesql_ast::Expression],
aggregate_infos: &[AggregateInfo],
schema: &'a CombinedSchema,
database: &'a vibesql_storage::Database,
) -> Result<Vec<(Vec<SqlValue>, Vec<SqlValue>)>, ExecutorError> {
let evaluator = CombinedExpressionEvaluator::with_database(schema, database);
let mut groups: AHashMap<Vec<SqlValue>, Vec<AggregateAccumulator>> = AHashMap::new();
for row in rows {
evaluator.clear_cse_cache();
let mut key = Vec::with_capacity(group_by_exprs.len());
for expr in group_by_exprs {
let value = evaluator.eval(expr, row)?;
key.push(value);
}
let accumulators = groups.entry(key).or_insert_with(|| {
aggregate_infos
.iter()
.map(|info| AggregateAccumulator::new(&info.function_name, info.distinct))
.collect::<Result<Vec<_>, _>>()
.unwrap_or_default()
});
for (i, info) in aggregate_infos.iter().enumerate() {
if i < accumulators.len() {
let value = evaluator.eval(&info.expr, row)?;
accumulators[i].accumulate(&value);
}
}
}
let mut results: Vec<(Vec<SqlValue>, Vec<SqlValue>)> = Vec::with_capacity(groups.len());
for (key, accumulators) in groups {
let values: Vec<SqlValue> =
accumulators.iter().map(|acc| acc.finalize()).collect::<Result<Vec<_>, _>>()?;
results.push((key, values));
}
Ok(results)
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct AggregateInfo {
pub function_name: String,
pub expr: vibesql_ast::Expression,
pub distinct: bool,
}
#[allow(dead_code)] impl AggregateInfo {
pub fn new(function_name: String, expr: vibesql_ast::Expression, distinct: bool) -> Self {
Self { function_name, expr, distinct }
}
}
#[cfg(all(test, feature = "parallel"))]
mod tests {
use super::*;
#[test]
fn test_aggregate_info_creation() {
let expr = vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col1", false,
));
let info = AggregateInfo::new("SUM".to_string(), expr, false);
assert_eq!(info.function_name, "SUM");
assert!(!info.distinct);
}
}