use std::collections::BTreeMap;
use lora_analyzer::{AggregateFunction, ResolvedExpr, ResolvedProjection};
use lora_store::GraphStorage;
use crate::errors::{ExecResult, ExecutorError};
use crate::eval::eval_expr_result;
use crate::executor::{compute_aggregate_expr, GroupValueKey};
use crate::value::{LoraValue, Row};
use super::{drain, hydrate_value, RowSource, StreamCtx};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StreamableAggKind {
CountAll,
CountField,
Sum,
Min,
Max,
Avg,
}
pub(crate) struct StreamableAggSpec {
pub(crate) kind: StreamableAggKind,
pub(crate) arg: Option<ResolvedExpr>,
}
#[derive(Clone, Debug)]
pub(crate) enum AggState {
Count(i64),
Sum {
sum: f64,
all_int: bool,
any: bool,
},
Min(Option<LoraValue>),
Max(Option<LoraValue>),
Avg {
sum: f64,
count: usize,
},
}
impl AggState {
pub(crate) fn seed(kind: StreamableAggKind) -> Self {
match kind {
StreamableAggKind::CountAll | StreamableAggKind::CountField => AggState::Count(0),
StreamableAggKind::Sum => AggState::Sum {
sum: 0.0,
all_int: true,
any: false,
},
StreamableAggKind::Min => AggState::Min(None),
StreamableAggKind::Max => AggState::Max(None),
StreamableAggKind::Avg => AggState::Avg { sum: 0.0, count: 0 },
}
}
pub(crate) fn fold(&mut self, kind: StreamableAggKind, value: LoraValue) {
match self {
AggState::Count(n) => match kind {
StreamableAggKind::CountAll => *n += 1,
StreamableAggKind::CountField if !matches!(value, LoraValue::Null) => *n += 1,
_ => {}
},
AggState::Sum { sum, all_int, any } => match value {
LoraValue::Null => {}
LoraValue::Int(i) => {
*sum += i as f64;
*any = true;
}
LoraValue::Float(f) => {
*sum += f;
*all_int = false;
*any = true;
}
_ => {}
},
AggState::Min(slot) => {
if matches!(value, LoraValue::Null) {
return;
}
match slot {
None => *slot = Some(value),
Some(cur) => {
if cmp_values_total(&value, cur) == std::cmp::Ordering::Less {
*cur = value;
}
}
}
}
AggState::Max(slot) => {
if matches!(value, LoraValue::Null) {
return;
}
match slot {
None => *slot = Some(value),
Some(cur) => {
if cmp_values_total(&value, cur) == std::cmp::Ordering::Greater {
*cur = value;
}
}
}
}
AggState::Avg { sum, count } => {
let n = match value {
LoraValue::Int(i) => Some(i as f64),
LoraValue::Float(f) => Some(f),
_ => None,
};
if let Some(n) = n {
*sum += n;
*count += 1;
}
}
}
}
pub(crate) fn finalize(self, _kind: StreamableAggKind) -> LoraValue {
match self {
AggState::Count(n) => LoraValue::Int(n),
AggState::Sum { sum, all_int, any } => {
if !any {
LoraValue::Null
} else if all_int && sum.fract() == 0.0 {
LoraValue::Int(sum as i64)
} else {
LoraValue::Float(sum)
}
}
AggState::Min(v) | AggState::Max(v) => v.unwrap_or(LoraValue::Null),
AggState::Avg { sum, count } => {
if count == 0 {
LoraValue::Null
} else {
LoraValue::Float(sum / count as f64)
}
}
}
}
}
struct StreamingGroup {
first_row: Row,
aggs: Vec<AggState>,
}
impl StreamingGroup {
fn new(specs: &[StreamableAggSpec], first_row: Row) -> Self {
Self {
first_row,
aggs: specs.iter().map(|spec| AggState::seed(spec.kind)).collect(),
}
}
}
pub(crate) fn classify_streamable_aggregates(
projections: &[ResolvedProjection],
) -> Option<Vec<StreamableAggSpec>> {
let mut specs = Vec::with_capacity(projections.len());
for proj in projections {
let spec = streamable_spec(&proj.expr)?;
specs.push(spec);
}
Some(specs)
}
fn streamable_spec(expr: &ResolvedExpr) -> Option<StreamableAggSpec> {
match expr {
ResolvedExpr::Function {
function,
distinct,
args,
} => {
if *distinct {
return None;
}
let kind = match function.as_aggregate() {
Some(AggregateFunction::Count) if args.is_empty() => StreamableAggKind::CountAll,
Some(AggregateFunction::Count) if args.len() == 1 => StreamableAggKind::CountField,
Some(AggregateFunction::Sum) if args.len() == 1 => StreamableAggKind::Sum,
Some(AggregateFunction::Min) if args.len() == 1 => StreamableAggKind::Min,
Some(AggregateFunction::Max) if args.len() == 1 => StreamableAggKind::Max,
Some(AggregateFunction::Avg) if args.len() == 1 => StreamableAggKind::Avg,
_ => return None,
};
let arg = if args.is_empty() {
None
} else {
Some(args[0].clone())
};
Some(StreamableAggSpec { kind, arg })
}
_ => None,
}
}
fn cmp_values_total(a: &LoraValue, b: &LoraValue) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (a, b) {
(LoraValue::Int(x), LoraValue::Int(y)) => x.cmp(y),
(LoraValue::Float(x), LoraValue::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(LoraValue::Int(x), LoraValue::Float(y)) => {
(*x as f64).partial_cmp(y).unwrap_or(Ordering::Equal)
}
(LoraValue::Float(x), LoraValue::Int(y)) => {
x.partial_cmp(&(*y as f64)).unwrap_or(Ordering::Equal)
}
(LoraValue::String(x), LoraValue::String(y)) => x.cmp(y),
(LoraValue::Bool(x), LoraValue::Bool(y)) => x.cmp(y),
_ => Ordering::Equal,
}
}
pub struct HashAggregationSource<'a, S: GraphStorage> {
state: HashAggregationState<'a, S>,
}
enum HashAggregationState<'a, S: GraphStorage> {
Pending {
upstream: Box<dyn RowSource + 'a>,
ctx: StreamCtx<'a, S>,
group_by: &'a [ResolvedProjection],
aggregates: &'a [ResolvedProjection],
},
Yielding(std::vec::IntoIter<Row>),
}
impl<'a, S: GraphStorage> HashAggregationSource<'a, S> {
pub(super) fn new(
upstream: Box<dyn RowSource + 'a>,
ctx: StreamCtx<'a, S>,
group_by: &'a [ResolvedProjection],
aggregates: &'a [ResolvedProjection],
) -> Self {
Self {
state: HashAggregationState::Pending {
upstream,
ctx,
group_by,
aggregates,
},
}
}
fn materialize(
upstream: &mut Box<dyn RowSource + 'a>,
ctx: &StreamCtx<'a, S>,
group_by: &[ResolvedProjection],
aggregates: &[ResolvedProjection],
) -> ExecResult<Vec<Row>> {
if let Some(specs) = classify_streamable_aggregates(aggregates) {
return Self::materialize_streaming(upstream, ctx, group_by, aggregates, &specs);
}
let input_rows = drain(upstream.as_mut())?;
let eval_ctx = ctx.eval_ctx();
let mut groups: BTreeMap<Vec<GroupValueKey>, Vec<Row>> = BTreeMap::new();
if group_by.is_empty() {
groups.insert(Vec::new(), input_rows);
} else {
for row in input_rows {
let mut key = Vec::with_capacity(group_by.len());
for proj in group_by {
let value = eval_expr_result(&proj.expr, &row, &eval_ctx)
.map_err(ExecutorError::RuntimeError)?;
key.push(GroupValueKey::from_value(&value));
}
groups.entry(key).or_default().push(row);
}
}
let mut out = Vec::new();
for rows in groups.into_values() {
let mut result = Row::new();
if let Some(first) = rows.first() {
for proj in group_by {
let value = eval_expr_result(&proj.expr, first, &eval_ctx)
.map_err(ExecutorError::RuntimeError)?;
let value = hydrate_value(value, ctx.storage);
result.insert_named(proj.output, proj.name.clone(), value);
}
}
for proj in aggregates {
let value = compute_aggregate_expr(&proj.expr, &rows, &eval_ctx)?;
result.insert_named(proj.output, proj.name.clone(), value);
}
out.push(result);
}
Ok(out)
}
fn materialize_streaming(
upstream: &mut Box<dyn RowSource + 'a>,
ctx: &StreamCtx<'a, S>,
group_by: &[ResolvedProjection],
aggregates: &[ResolvedProjection],
specs: &[StreamableAggSpec],
) -> ExecResult<Vec<Row>> {
let eval_ctx = ctx.eval_ctx();
if group_by.is_empty() {
let mut aggs: Vec<AggState> = specs.iter().map(|s| AggState::seed(s.kind)).collect();
while let Some(row) = upstream.next_row()? {
for (i, spec) in specs.iter().enumerate() {
let value = match &spec.arg {
Some(arg) => eval_expr_result(arg, &row, &eval_ctx)
.map_err(ExecutorError::RuntimeError)?,
None => LoraValue::Null,
};
aggs[i].fold(spec.kind, value);
}
}
let mut result = Row::new();
for (i, proj) in aggregates.iter().enumerate() {
let value = std::mem::replace(&mut aggs[i], AggState::seed(specs[i].kind))
.finalize(specs[i].kind);
result.insert_named(proj.output, proj.name.clone(), value);
}
return Ok(vec![result]);
}
let mut groups: BTreeMap<Vec<GroupValueKey>, StreamingGroup> = BTreeMap::new();
while let Some(row) = upstream.next_row()? {
let mut key = Vec::with_capacity(group_by.len());
for proj in group_by {
let value = eval_expr_result(&proj.expr, &row, &eval_ctx)
.map_err(ExecutorError::RuntimeError)?;
key.push(GroupValueKey::from_value(&value));
}
let entry = groups
.entry(key)
.or_insert_with(|| StreamingGroup::new(specs, row.clone()));
for (i, spec) in specs.iter().enumerate() {
let value = match &spec.arg {
Some(arg) => eval_expr_result(arg, &row, &eval_ctx)
.map_err(ExecutorError::RuntimeError)?,
None => LoraValue::Null,
};
entry.aggs[i].fold(spec.kind, value);
}
}
let mut out = Vec::with_capacity(groups.len());
for group in groups.into_values() {
let mut result = Row::new();
for proj in group_by {
let value = eval_expr_result(&proj.expr, &group.first_row, &eval_ctx)
.map_err(ExecutorError::RuntimeError)?;
let value = hydrate_value(value, ctx.storage);
result.insert_named(proj.output, proj.name.clone(), value);
}
for (i, proj) in aggregates.iter().enumerate() {
let value = group.aggs[i].clone().finalize(specs[i].kind);
result.insert_named(proj.output, proj.name.clone(), value);
}
out.push(result);
}
Ok(out)
}
}
impl<'a, S: GraphStorage> RowSource for HashAggregationSource<'a, S> {
fn next_row(&mut self) -> ExecResult<Option<Row>> {
loop {
match &mut self.state {
HashAggregationState::Pending {
upstream,
ctx,
group_by,
aggregates,
} => {
let rows = Self::materialize(upstream, ctx, group_by, aggregates)?;
self.state = HashAggregationState::Yielding(rows.into_iter());
}
HashAggregationState::Yielding(it) => return Ok(it.next()),
}
}
}
}