use super::aggregation::{
build_group_key, finalize_empty_global_aggregate, finalize_group, find_or_init_group,
init_aggregate_accumulators, update_aggregate, AggregationState,
};
use super::row_build::parse_cql_type_str;
use super::{
apply_forcing, build_row_from_scan_cached, classify_partition_lookup, evaluate_predicates,
validate_token_predicates, AccessPath, ExecutionContext, ExecutionStep, FallbackReason,
ForcedPlan, PartitionKeyCache, PartitionLookupOutcome, SelectExecutor,
};
use crate::query::result::QueryRow;
use crate::query::select_ast::WhereExpression;
use crate::query::select_naming::aggregate_result_cql_type;
use crate::query::select_optimizer::{AggregationPlan, SSTablePredicate};
use crate::schema::{CqlType, TableSchema};
use crate::{Error, Result, TableId, Value};
fn resolve_aggregate_result_types(
agg_plan: &AggregationPlan,
schema: Option<&TableSchema>,
) -> Vec<Option<CqlType>> {
agg_plan
.aggregates
.iter()
.map(|agg| {
let arg_type = if agg.column == "*" {
None
} else {
schema.and_then(|s| {
s.columns
.iter()
.find(|c| c.name == agg.column)
.and_then(|c| parse_cql_type_str(&c.data_type))
})
};
aggregate_result_cql_type(&agg.function, arg_type)
})
.collect()
}
const AGG_FOLD_SCAN_BUFFER: usize = 1024;
struct GlobalAggregatePlan<'a> {
table: &'a TableId,
predicates: &'a [SSTablePredicate],
projection: &'a [String],
filters: Vec<&'a WhereExpression>,
agg_plan: &'a AggregationPlan,
}
impl SelectExecutor {
pub(super) fn execute_aggregation(
&self,
rows: Vec<QueryRow>,
agg_plan: &AggregationPlan,
schema_opt: Option<&TableSchema>,
_context: &mut ExecutionContext,
) -> Result<Vec<QueryRow>> {
const PER_ROW_MEMORY_ESTIMATE_BYTES: usize = 100;
const DEFAULT_AGGREGATION_MEMORY_LIMIT: usize = 512 * 1024 * 1024;
crate::query::agg_stream_probe::record_buffered_rows(rows.len() as u64);
let mut agg_state = AggregationState {
groups: Vec::new(),
group_index: rustc_hash::FxHashMap::default(),
memory_usage_bytes: 0,
memory_limit_bytes: DEFAULT_AGGREGATION_MEMORY_LIMIT,
};
let result_types = resolve_aggregate_result_types(agg_plan, schema_opt);
for row in rows {
let group_key = build_group_key(&row, &agg_plan.group_by_columns);
let group_index = find_or_init_group(
&mut agg_state,
group_key,
&agg_plan.aggregates,
&result_types,
);
let group_aggregates = &mut agg_state.groups[group_index].1;
for (i, agg_comp) in agg_plan.aggregates.iter().enumerate() {
update_aggregate(&mut group_aggregates[i], agg_comp, &row);
}
agg_state.memory_usage_bytes += PER_ROW_MEMORY_ESTIMATE_BYTES;
if agg_state.memory_usage_bytes > agg_state.memory_limit_bytes {
return Err(Error::query_execution(
"Aggregation memory limit exceeded".to_string(),
));
}
}
if agg_state.groups.is_empty() && agg_plan.group_by_columns.is_empty() {
return Ok(vec![finalize_empty_global_aggregate(agg_plan)]);
}
let result_rows = agg_state
.groups
.into_iter()
.map(|(group_key, group_aggregates)| {
finalize_group(group_key, group_aggregates, agg_plan)
})
.collect();
Ok(result_rows)
}
pub(super) async fn try_execute_global_aggregate(
&self,
steps: &[ExecutionStep],
schema_opt: Option<&TableSchema>,
context: &mut ExecutionContext,
) -> Result<Option<Vec<QueryRow>>> {
if context.projection_flags.include_cell_metadata {
return Ok(None);
}
let Some(eligible) = classify_global_aggregate(steps) else {
return Ok(None);
};
let GlobalAggregatePlan {
table,
predicates,
projection,
filters,
agg_plan,
} = eligible;
validate_token_predicates(predicates, schema_opt)?;
let mode = self.resolved_read_path_mode()?;
let outcome = classify_partition_lookup(predicates, schema_opt);
let reason = match apply_forcing(outcome, mode)? {
ForcedPlan::ForceFullScan => FallbackReason::ForcedFullScan,
ForcedPlan::Proceed(PartitionLookupOutcome::Fallback(reason)) => reason,
ForcedPlan::Proceed(
PartitionLookupOutcome::Targeted(_) | PartitionLookupOutcome::MultiTargeted(_),
) => {
return Ok(None);
}
};
context.access_path = Some(AccessPath::FallbackFullScan { reason });
crate::query::access_path::record(AccessPath::FallbackFullScan { reason });
let result_types = resolve_aggregate_result_types(agg_plan, schema_opt);
let mut accumulators = init_aggregate_accumulators(&agg_plan.aggregates, &result_types);
let mut folded_any = false;
let mut scan_stream = self
.storage
.scan_stream_batched(table, None, None, schema_opt, AGG_FOLD_SCAN_BUFFER)
.await?;
let mut pk_cache = PartitionKeyCache::default();
while let Some(batch) = scan_stream.recv().await {
for (key, value) in batch? {
context.rows_processed += 1;
context.scan_rows += 1;
let Some(row) =
build_row_from_scan_cached(key, value, projection, schema_opt, &mut pk_cache)
else {
continue;
};
if !evaluate_predicates(&row, predicates)? {
continue;
}
let mut keep = true;
for filter in &filters {
if !self.evaluate_where_expression(filter, &row)? {
keep = false;
break;
}
}
if !keep {
continue;
}
folded_any = true;
for (i, agg_comp) in agg_plan.aggregates.iter().enumerate() {
update_aggregate(&mut accumulators[i], agg_comp, &row);
}
}
}
if !folded_any {
let row = finalize_empty_global_aggregate(agg_plan);
return Ok(Some(vec![row]));
}
let row = finalize_group(vec![Value::Null], accumulators, agg_plan);
Ok(Some(vec![row]))
}
}
fn classify_global_aggregate(steps: &[ExecutionStep]) -> Option<GlobalAggregatePlan<'_>> {
let mut scan: Option<(&TableId, &[SSTablePredicate], &[String])> = None;
let mut agg_plan: Option<&AggregationPlan> = None;
let mut filters: Vec<&WhereExpression> = Vec::new();
for step in steps {
match step {
ExecutionStep::SSTableScan {
table,
predicates,
projection,
..
} => {
if scan.is_some() {
return None;
}
scan = Some((table, predicates.as_slice(), projection.as_slice()));
}
ExecutionStep::Aggregate { plan, .. } => {
if !plan.group_by_columns.is_empty() || agg_plan.is_some() {
return None;
}
agg_plan = Some(plan);
}
ExecutionStep::Filter { expression, .. } => filters.push(expression),
_ => return None,
}
}
let (table, predicates, projection) = scan?;
let agg_plan = agg_plan?;
Some(GlobalAggregatePlan {
table,
predicates,
projection,
filters,
agg_plan,
})
}