use std::collections::hash_map::Entry;
use std::collections::HashMap;
use serde_json::Value;
use super::super::aggregation::AggregateAccumulator;
use super::super::types::Context;
use super::super::QueryExecutor;
use super::clauses::BUDGET_CHECK_INTERVAL;
use crate::error::{DbError, DbResult};
use crate::sdbql::ast::CollectClause;
struct Group {
ctx: Context,
count: i64,
members: Vec<Value>,
aggregates: Vec<AggregateAccumulator>,
}
impl<'a> QueryExecutor<'a> {
pub(super) fn execute_collect(
&self,
collect: &CollectClause,
rows: Vec<Context>,
) -> DbResult<Vec<Context>> {
let mut groups: HashMap<String, Group> = HashMap::new();
let mut retained = 0usize;
let mut keep_validated = collect.into_var.is_none() || collect.keep_vars.is_empty();
for (seen, ctx) in rows.into_iter().enumerate() {
let mut key_parts = Vec::with_capacity(collect.group_vars.len());
let mut group_vals = Vec::with_capacity(collect.group_vars.len());
for (var_name, expr) in &collect.group_vars {
let val = self.evaluate_expr_with_context(expr, &ctx)?;
key_parts.push(serde_json::to_string(&val).unwrap_or_default());
group_vals.push((var_name, val));
}
let group_key = key_parts.join("|");
let mut agg_values = Vec::with_capacity(collect.aggregates.len());
for agg in &collect.aggregates {
agg_values.push(match &agg.argument {
Some(expr) => Some(self.evaluate_expr_with_context(expr, &ctx)?),
None => None,
});
}
if !keep_validated {
for keep in &collect.keep_vars {
if !ctx.contains_key(keep) {
return Err(DbError::ExecutionError(format!(
"KEEP variable '{}' is not in scope at COLLECT",
keep
)));
}
}
keep_validated = true;
}
let group = match groups.entry(group_key) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let mut group_ctx = Context::with_capacity(
collect.group_vars.len()
+ collect.aggregates.len()
+ usize::from(collect.into_var.is_some())
+ usize::from(collect.count_var.is_some()),
);
for (name, val) in group_vals {
group_ctx.insert(name.clone(), val);
}
let aggregates = collect
.aggregates
.iter()
.map(|a| AggregateAccumulator::new(&a.function, a.argument.is_some()))
.collect::<DbResult<Vec<_>>>()?;
e.insert(Group {
ctx: group_ctx,
count: 0,
members: Vec::new(),
aggregates,
})
}
};
group.count += 1;
for (acc, value) in group.aggregates.iter_mut().zip(agg_values) {
if acc.push(value) {
retained += 1;
}
}
if collect.into_var.is_some() {
group.members.push(project_into(&collect.keep_vars, ctx));
retained += 1;
}
if (seen + 1) % BUDGET_CHECK_INTERVAL == 0 {
self.check_budget(retained.max(groups.len()))?;
}
}
let mut out = Vec::with_capacity(groups.len());
for group in groups.into_values() {
let Group {
mut ctx,
count,
members,
aggregates,
} = group;
if let Some(into_var) = &collect.into_var {
ctx.insert(into_var.clone(), Value::Array(members));
}
if let Some(count_var) = &collect.count_var {
ctx.insert(count_var.clone(), Value::Number(count.into()));
}
for (agg, acc) in collect.aggregates.iter().zip(aggregates) {
ctx.insert(agg.variable.clone(), acc.finish());
}
out.push(ctx);
}
Ok(out)
}
}
fn project_into(keep_vars: &[String], ctx: Context) -> Value {
let obj: serde_json::Map<String, Value> = if keep_vars.is_empty() {
ctx.into_iter().collect()
} else {
ctx.into_iter()
.filter(|(k, _)| keep_vars.contains(k))
.collect()
};
Value::Object(obj)
}