use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use futures::StreamExt;
use crate::exec::function::{Accumulator, AggregateFunction};
use crate::exec::{
AccessMode, ContextLevel, EvalContext, ExecOperator, ExecutionContext, FlowResult,
FlowResultExt as _, OperatorMetrics, PhysicalExpr, ValueBatch, ValueBatchStream, buffer_stream,
monitor_stream,
};
use crate::expr::idiom::Idiom;
use crate::val::{Object, Value};
#[derive(Debug, Clone)]
pub struct Aggregate {
pub(crate) input: Arc<dyn ExecOperator>,
pub(crate) group_by: Vec<Idiom>,
pub(crate) group_by_exprs: Vec<Arc<dyn PhysicalExpr>>,
pub(crate) aggregates: Vec<AggregateField>,
pub(crate) metrics: Arc<OperatorMetrics>,
}
impl Aggregate {
pub(crate) fn new(
input: Arc<dyn ExecOperator>,
group_by: Vec<Idiom>,
group_by_exprs: Vec<Arc<dyn PhysicalExpr>>,
aggregates: Vec<AggregateField>,
) -> Self {
Self {
input,
group_by,
group_by_exprs,
aggregates,
metrics: Arc::new(OperatorMetrics::new()),
}
}
}
#[derive(Debug, Clone)]
pub struct AggregateField {
pub output_path: Vec<String>,
pub is_group_key: bool,
pub group_key_index: Option<usize>,
pub aggregate_expr_info: Option<AggregateExprInfo>,
pub fallback_expr: Option<Arc<dyn PhysicalExpr>>,
}
impl AggregateField {
pub fn new(
name: String,
is_group_key: bool,
group_key_index: Option<usize>,
aggregate_expr_info: Option<AggregateExprInfo>,
fallback_expr: Option<Arc<dyn PhysicalExpr>>,
) -> Self {
Self::with_output_path(
vec![name],
is_group_key,
group_key_index,
aggregate_expr_info,
fallback_expr,
)
}
pub fn with_output_path(
output_path: Vec<String>,
is_group_key: bool,
group_key_index: Option<usize>,
aggregate_expr_info: Option<AggregateExprInfo>,
fallback_expr: Option<Arc<dyn PhysicalExpr>>,
) -> Self {
Self {
output_path,
is_group_key,
group_key_index,
aggregate_expr_info,
fallback_expr,
}
}
pub fn is_empty_name(&self) -> bool {
self.output_path.len() == 1 && self.output_path[0].is_empty()
}
}
#[derive(Clone)]
pub struct AggregateExprInfo {
pub aggregates: Vec<ExtractedAggregate>,
pub post_expr: Option<Arc<dyn PhysicalExpr>>,
}
impl std::fmt::Debug for AggregateExprInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AggregateExprInfo")
.field("num_aggregates", &self.aggregates.len())
.field("has_post_expr", &self.post_expr.is_some())
.finish()
}
}
#[derive(Clone)]
pub struct ExtractedAggregate {
pub function: Arc<dyn AggregateFunction>,
pub argument_expr: Arc<dyn PhysicalExpr>,
pub extra_args: Vec<Arc<dyn PhysicalExpr>>,
}
impl std::fmt::Debug for ExtractedAggregate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtractedAggregate").field("function", &self.function.name()).finish()
}
}
pub fn aggregate_field_name(idx: usize) -> String {
format!("_a{}", idx)
}
type GroupKey = Vec<Value>;
struct GroupState {
accumulators: Vec<Vec<Box<dyn Accumulator>>>,
first_values: Vec<Value>,
}
struct GroupMap {
buckets: HashMap<u64, Vec<(GroupKey, GroupState)>>,
}
impl GroupMap {
fn new() -> Self {
Self {
buckets: HashMap::new(),
}
}
fn is_empty(&self) -> bool {
self.buckets.is_empty()
}
fn entry_for_row<F>(
&mut self,
group_key_columns: &[Vec<Value>],
row_idx: usize,
create: F,
) -> &mut GroupState
where
F: FnOnce() -> GroupState,
{
let hash = hash_values(group_key_columns.iter().map(|col| &col[row_idx]));
let bucket = self.buckets.entry(hash).or_default();
let matches = |(k, _): &(GroupKey, _)| -> bool {
k.len() == group_key_columns.len()
&& k.iter().zip(group_key_columns).all(|(stored, col)| *stored == col[row_idx])
};
match bucket.iter().position(matches) {
Some(idx) => &mut bucket[idx].1,
None => {
let new_key: GroupKey =
group_key_columns.iter().map(|col| col[row_idx].clone()).collect();
bucket.push((new_key, create()));
&mut bucket.last_mut().expect("just pushed").1
}
}
}
fn entry_for_empty<F>(&mut self, create: F) -> &mut GroupState
where
F: FnOnce() -> GroupState,
{
let hash = hash_values(std::iter::empty::<&Value>());
let bucket = self.buckets.entry(hash).or_default();
match bucket.iter().position(|(k, _)| k.is_empty()) {
Some(idx) => &mut bucket[idx].1,
None => {
bucket.push((Vec::new(), create()));
&mut bucket.last_mut().expect("just pushed").1
}
}
}
fn insert(&mut self, key: GroupKey, state: GroupState) {
let hash = hash_values(key.iter());
self.buckets.entry(hash).or_default().push((key, state));
}
fn into_sorted(self) -> Vec<(GroupKey, GroupState)> {
let mut all: Vec<(GroupKey, GroupState)> = self.buckets.into_values().flatten().collect();
all.sort_by(|a, b| a.0.cmp(&b.0));
all
}
}
fn hash_values<'a, I>(values: I) -> u64
where
I: IntoIterator<Item = &'a Value>,
{
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for v in values {
v.hash(&mut hasher);
}
hasher.finish()
}
impl ExecOperator for Aggregate {
fn name(&self) -> &'static str {
"Aggregate"
}
fn attrs(&self) -> Vec<(String, String)> {
use surrealdb_types::ToSql;
if self.group_by.is_empty() {
vec![("mode".to_string(), "GROUP ALL".to_string())]
} else {
vec![(
"by".to_string(),
self.group_by.iter().map(|i| i.to_sql()).collect::<Vec<_>>().join(", "),
)]
}
}
fn required_context(&self) -> ContextLevel {
let group_ctx = self
.group_by_exprs
.iter()
.map(|e| e.required_context())
.max()
.unwrap_or(ContextLevel::Root);
let agg_ctx = self
.aggregates
.iter()
.map(|agg| {
let info_ctx = agg
.aggregate_expr_info
.as_ref()
.map(|info| {
let agg_arg_ctx = info
.aggregates
.iter()
.flat_map(|ext| {
std::iter::once(ext.argument_expr.required_context())
.chain(ext.extra_args.iter().map(|e| e.required_context()))
})
.max()
.unwrap_or(ContextLevel::Root);
let post_ctx = info
.post_expr
.as_ref()
.map(|e| e.required_context())
.unwrap_or(ContextLevel::Root);
agg_arg_ctx.max(post_ctx)
})
.unwrap_or(ContextLevel::Root);
let fallback_ctx = agg
.fallback_expr
.as_ref()
.map(|e| e.required_context())
.unwrap_or(ContextLevel::Root);
info_ctx.max(fallback_ctx)
})
.max()
.unwrap_or(ContextLevel::Root);
group_ctx.max(agg_ctx).max(self.input.required_context())
}
fn access_mode(&self) -> AccessMode {
let mut mode = self.input.access_mode();
for expr in &self.group_by_exprs {
mode = mode.combine(expr.access_mode());
}
for agg in &self.aggregates {
if let Some(info) = &agg.aggregate_expr_info {
for extracted in &info.aggregates {
mode = mode.combine(extracted.argument_expr.access_mode());
for extra_arg in &extracted.extra_args {
mode = mode.combine(extra_arg.access_mode());
}
}
if let Some(post_expr) = &info.post_expr {
mode = mode.combine(post_expr.access_mode());
}
}
if let Some(expr) = &agg.fallback_expr {
mode = mode.combine(expr.access_mode());
}
}
mode
}
fn children(&self) -> Vec<&Arc<dyn ExecOperator>> {
vec![&self.input]
}
fn metrics(&self) -> Option<&OperatorMetrics> {
Some(&self.metrics)
}
fn expressions(&self) -> Vec<(&str, &Arc<dyn PhysicalExpr>)> {
let mut exprs = Vec::new();
for expr in &self.group_by_exprs {
exprs.push(("group_by", expr));
}
for agg in &self.aggregates {
if let Some(info) = &agg.aggregate_expr_info {
for extracted in &info.aggregates {
exprs.push(("agg_arg", &extracted.argument_expr));
for extra in &extracted.extra_args {
exprs.push(("agg_extra", extra));
}
}
if let Some(post) = &info.post_expr {
exprs.push(("agg_post_expr", post));
}
}
if let Some(fallback) = &agg.fallback_expr {
exprs.push(("agg_fallback", fallback));
}
}
exprs
}
fn execute(&self, ctx: &ExecutionContext) -> FlowResult<ValueBatchStream> {
let input_stream = buffer_stream(
self.input.execute(ctx)?,
self.input.access_mode(),
self.input.cardinality_hint(),
ctx.root().ctx.config.operator_buffer_size,
);
let group_by_exprs = self.group_by_exprs.clone();
let aggregates = self.aggregates.clone();
let ctx = ctx.clone();
let aggregate_stream = async_stream::try_stream! {
let eval_ctx_for_args = EvalContext::from_exec_ctx(&ctx);
let mut evaluated_extra_args: Vec<Vec<Vec<Value>>> = Vec::with_capacity(aggregates.len());
for agg in &aggregates {
if let Some(info) = &agg.aggregate_expr_info {
let mut field_args = Vec::with_capacity(info.aggregates.len());
for extracted in &info.aggregates {
let mut args = Vec::with_capacity(extracted.extra_args.len());
for extra_arg in &extracted.extra_args {
let value =
extra_arg.evaluate(eval_ctx_for_args.clone()).await.or_none()?;
args.push(value);
}
field_args.push(args);
}
evaluated_extra_args.push(field_args);
} else {
evaluated_extra_args.push(vec![]);
}
}
let mut groups = GroupMap::new();
futures::pin_mut!(input_stream);
while let Some(batch_result) = input_stream.next().await {
if ctx.cancellation().is_cancelled() {
Err(crate::expr::ControlFlow::Err(
anyhow::anyhow!(crate::err::Error::QueryCancelled),
))?;
}
let batch = batch_result?;
let eval_ctx = EvalContext::from_exec_ctx(&ctx);
let mut group_key_columns: Vec<Vec<Value>> =
Vec::with_capacity(group_by_exprs.len());
for expr in &group_by_exprs {
let keys = match expr
.evaluate_batch(eval_ctx.clone(), &batch.values)
.await
{
Ok(v) => v,
Err(_) => {
let mut keys = Vec::with_capacity(batch.values.len());
for value in &batch.values {
let v = expr
.evaluate(eval_ctx.with_value(value))
.await
.or_none()?;
keys.push(v);
}
keys
}
};
group_key_columns.push(keys);
}
let mut agg_arg_columns: Vec<Vec<Vec<Value>>> =
Vec::with_capacity(aggregates.len());
for agg in &aggregates {
if let Some(info) = &agg.aggregate_expr_info {
let mut field_cols = Vec::with_capacity(info.aggregates.len());
for extracted in &info.aggregates {
let col = match extracted
.argument_expr
.evaluate_batch(eval_ctx.clone(), &batch.values)
.await
{
Ok(v) => v,
Err(_) => {
let mut col =
Vec::with_capacity(batch.values.len());
for value in &batch.values {
let v = extracted
.argument_expr
.evaluate(eval_ctx.with_value(value))
.await
.or_none()?;
col.push(v);
}
col
}
};
field_cols.push(col);
}
agg_arg_columns.push(field_cols);
} else {
agg_arg_columns.push(vec![]);
}
}
if group_by_exprs.is_empty() {
let state = groups.entry_for_empty(|| {
create_group_state(&aggregates, &evaluated_extra_args)
});
for (field_idx, agg) in aggregates.iter().enumerate() {
if agg.is_group_key {
continue;
}
if agg.aggregate_expr_info.is_some() {
for (agg_idx, arg_col) in
agg_arg_columns[field_idx].iter().enumerate()
{
if let Some(acc) =
state.accumulators[field_idx].get_mut(agg_idx)
&& let Err(e) = acc.update_batch(arg_col)
{
tracing::debug!(error = %e, "Accumulator batch update failed, skipping batch");
}
}
} else if let Some(expr) = &agg.fallback_expr {
if state.first_values[field_idx].is_none()
&& let Some(first_value) = batch.values.first() {
match expr.evaluate(eval_ctx.with_value(first_value)).await {
Ok(field_value) => {
state.first_values[field_idx] = field_value;
}
Err(cf) if cf.is_ignorable() => {
tracing::debug!(error = %cf, "Fallback expression evaluation failed (ignorable)");
}
Err(cf) => Err(cf)?,
}
}
}
}
} else {
for (row_idx, value) in batch.values.iter().enumerate() {
let state = groups.entry_for_row(
&group_key_columns,
row_idx,
|| create_group_state(&aggregates, &evaluated_extra_args),
);
for (field_idx, agg) in aggregates.iter().enumerate() {
if agg.is_group_key {
continue;
}
if agg.aggregate_expr_info.is_some() {
for (agg_idx, arg_col) in
agg_arg_columns[field_idx].iter().enumerate()
{
let arg_value = arg_col[row_idx].clone();
if let Some(acc) =
state.accumulators[field_idx].get_mut(agg_idx)
&& let Err(e) = acc.update(arg_value)
{
tracing::debug!(error = %e, "Accumulator update failed, skipping value");
}
}
} else if let Some(expr) = &agg.fallback_expr {
if state.first_values[field_idx].is_none() {
match expr.evaluate(eval_ctx.with_value(value)).await {
Ok(field_value) => {
state.first_values[field_idx] = field_value;
}
Err(cf) if cf.is_ignorable() => {
tracing::debug!(error = %cf, "Fallback expression evaluation failed (ignorable)");
}
Err(cf) => Err(cf)?,
}
}
}
}
}
}
}
if group_by_exprs.is_empty() && groups.is_empty() {
let perms_active = ctx
.should_check_perms(crate::iam::Action::View)
.unwrap_or(true);
if !perms_active {
let state = create_group_state(&aggregates, &evaluated_extra_args);
groups.insert(Vec::new(), state);
}
}
let sorted_groups = groups.into_sorted();
let mut results = Vec::with_capacity(sorted_groups.len());
for (group_key, state) in sorted_groups {
let result = compute_group_result_async(
&group_key,
state,
&aggregates,
&ctx,
).await?;
results.push(result);
}
yield ValueBatch { values: results };
};
Ok(monitor_stream(Box::pin(aggregate_stream), "Aggregate", &self.metrics))
}
}
fn create_group_state(
aggregates: &[AggregateField],
evaluated_extra_args: &[Vec<Vec<Value>>],
) -> GroupState {
let accumulators = aggregates
.iter()
.enumerate()
.map(|(i, agg)| {
if let Some(info) = &agg.aggregate_expr_info {
info.aggregates
.iter()
.enumerate()
.map(|(agg_idx, extracted)| {
let extra_args = evaluated_extra_args
.get(i)
.and_then(|field_args| field_args.get(agg_idx))
.map(|v| v.as_slice())
.unwrap_or(&[]);
extracted.function.create_accumulator_with_args(extra_args)
})
.collect()
} else {
vec![]
}
})
.collect();
let first_values = aggregates.iter().map(|_| Value::None).collect();
GroupState {
accumulators,
first_values,
}
}
async fn compute_single_field_value(
agg: &AggregateField,
group_key: &GroupKey,
accumulators: &[Box<dyn Accumulator>],
first_value: Value,
ctx: &ExecutionContext,
) -> FlowResult<Value> {
if let Some(idx) = agg.group_key_index {
Ok(group_key.get(idx).cloned().unwrap_or(Value::None))
} else if let Some(info) = &agg.aggregate_expr_info {
compute_aggregate_field_value(info, accumulators, ctx).await
} else {
Ok(first_value)
}
}
async fn compute_group_result_async(
group_key: &GroupKey,
state: GroupState,
aggregates: &[AggregateField],
ctx: &ExecutionContext,
) -> FlowResult<Value> {
if aggregates.len() == 1 && aggregates[0].is_empty_name() {
let agg = &aggregates[0];
let first_value = state.first_values.into_iter().next().unwrap_or(Value::None);
let accumulators = state.accumulators.into_iter().next().unwrap_or_default();
return compute_single_field_value(agg, group_key, &accumulators, first_value, ctx).await;
}
let mut result = Object::default();
let field_data = aggregates.iter().zip(state.accumulators).zip(state.first_values);
for ((agg, accumulators), first_value) in field_data {
let field_value =
compute_single_field_value(agg, group_key, &accumulators, first_value, ctx).await?;
set_nested_value(&mut result, &agg.output_path, field_value);
}
Ok(Value::Object(result))
}
fn set_nested_value(obj: &mut Object, path: &[String], value: Value) {
if path.is_empty() {
return;
}
if path.len() == 1 {
obj.insert(path[0].clone(), value);
return;
}
let key = &path[0];
let rest = &path[1..];
let nested = obj.entry(key.clone()).or_insert_with(|| Value::Object(Object::default()));
match nested {
Value::Object(nested_obj) => {
set_nested_value(nested_obj, rest, value);
}
_ => {
let mut new_obj = Object::default();
set_nested_value(&mut new_obj, rest, value);
*nested = Value::Object(new_obj);
}
}
}
async fn compute_aggregate_field_value(
info: &AggregateExprInfo,
accumulators: &[Box<dyn Accumulator>],
ctx: &ExecutionContext,
) -> FlowResult<Value> {
if info.aggregates.is_empty() {
return Ok(Value::Null);
}
let mut agg_doc = Object::default();
for (idx, acc) in accumulators.iter().enumerate() {
let value = match acc.finalize() {
Ok(v) => v,
Err(e) => {
tracing::debug!(error = %e, idx, "Accumulator finalize failed, using Null");
Value::Null
}
};
agg_doc.insert(aggregate_field_name(idx), value);
}
if let Some(post_expr) = &info.post_expr {
let doc_value = Value::Object(agg_doc);
let eval_ctx = EvalContext::from_exec_ctx(ctx).with_value(&doc_value);
match post_expr.evaluate(eval_ctx).await {
Ok(v) => Ok(v),
Err(cf) if cf.is_ignorable() => {
tracing::debug!(error = %cf, "Post-expression evaluation failed (ignorable), using Null");
Ok(Value::Null)
}
Err(cf) => Err(cf),
}
} else {
Ok(agg_doc.0.into_values().next().unwrap_or(Value::Null))
}
}