use std::collections::{BTreeMap, HashMap};
use nodedb_array::query::aggregate::{GroupAggregate, aggregate_attr, group_by_dim};
use nodedb_array::schema::ArraySchema;
use nodedb_array::segment::TilePayload;
use nodedb_array::types::ArrayId;
use nodedb_array::types::coord::value::CoordValue;
use nodedb_cluster::distributed_array::merge::ArrayAggPartial;
use nodedb_types::SurrogateBitmap;
use crate::bridge::envelope::{ErrorCode, Response};
use crate::data::executor::core_loop::CoreLoop;
use crate::data::executor::task::ExecutionTask;
use nodedb_physical::physical_plan::ArrayReducer;
use super::aggregate_helpers::{
AggCell, agg_result_to_partial, apply_surrogate_filter, coord_to_agg_cell, coord_to_group_key,
encode_agg_rows, encode_bitemporal_agg_partial, float_or_null, map_reducer, unwrap_sparse,
};
pub(in crate::data::executor) struct AggParams<'a> {
pub array_id: &'a ArrayId,
pub attr_idx: u32,
pub reducer: ArrayReducer,
pub group_by_dim_idx: i32,
pub cell_filter: Option<&'a SurrogateBitmap>,
pub return_partial: bool,
pub hilbert_range: Option<(u64, u64)>,
pub system_as_of: Option<i64>,
pub valid_at_ms: Option<i64>,
}
struct AggEmit<'a> {
task: &'a ExecutionTask,
schema: &'a ArraySchema,
all_tiles: Vec<TilePayload>,
attr_idx: u32,
reducer: ArrayReducer,
group_by_dim_idx: i32,
cell_filter: Option<&'a SurrogateBitmap>,
return_partial: bool,
truncated_before_horizon: bool,
emit_horizon: bool,
}
fn hilbert_prefix_in_range(hp: u64, range: Option<(u64, u64)>) -> bool {
match range {
Some((lo, hi)) => hp >= lo && hp <= hi,
None => true,
}
}
impl CoreLoop {
pub(in crate::data::executor) fn dispatch_array_aggregate(
&mut self,
task: &ExecutionTask,
p: AggParams<'_>,
) -> Response {
let AggParams {
array_id,
attr_idx,
reducer,
group_by_dim_idx,
cell_filter,
return_partial,
hilbert_range,
system_as_of,
valid_at_ms,
} = p;
if let Err(resp) = self.ensure_array_open(task, array_id) {
return resp;
}
let schema = match self.array_engine.store(array_id) {
Ok(store) => store.schema().clone(),
Err(e) => {
return self.response_error(
task,
ErrorCode::Internal {
detail: format!("array '{}' not open: {e}", array_id.name),
},
);
}
};
let temporal = system_as_of.is_some() || valid_at_ms.is_some();
let (all_tiles, truncated_before_horizon) =
match self.collect_agg_tiles(array_id, hilbert_range, system_as_of, valid_at_ms) {
Ok(v) => v,
Err(detail) => {
return self.response_error(task, ErrorCode::Internal { detail });
}
};
self.reduce_and_encode_agg(AggEmit {
task,
schema: &schema,
all_tiles,
attr_idx,
reducer,
group_by_dim_idx,
cell_filter,
return_partial,
truncated_before_horizon,
emit_horizon: temporal,
})
}
fn collect_agg_tiles(
&self,
array_id: &ArrayId,
hilbert_range: Option<(u64, u64)>,
system_as_of: Option<i64>,
valid_at_ms: Option<i64>,
) -> Result<(Vec<TilePayload>, bool), String> {
let cutoff = system_as_of.unwrap_or(i64::MAX);
let store = self
.array_engine
.store(array_id)
.map_err(|e| format!("array '{}' not open: {e}", array_id.name))?;
let (resolved_tiles, truncated_before_horizon) =
store
.scan_tiles_at(cutoff, valid_at_ms)
.map_err(|e| format!("array aggregate scan: {e}"))?;
let tiles = resolved_tiles
.into_iter()
.filter(|(hp, _)| hilbert_prefix_in_range(*hp, hilbert_range))
.map(|(_, tile)| TilePayload::Sparse(tile))
.collect();
Ok((tiles, truncated_before_horizon))
}
fn reduce_and_encode_agg(&self, e: AggEmit<'_>) -> Response {
let AggEmit {
task,
schema,
all_tiles,
attr_idx,
reducer,
group_by_dim_idx,
cell_filter,
return_partial,
truncated_before_horizon,
emit_horizon,
} = e;
let r = map_reducer(reducer);
let attr = attr_idx as usize;
if group_by_dim_idx < 0 {
let mut acc = None;
for tile in all_tiles {
let sparse = match unwrap_sparse(tile) {
Ok(s) => s,
Err(code) => return self.response_error(task, code),
};
let sparse = match apply_surrogate_filter(schema, sparse, cell_filter) {
Ok(s) => s,
Err(code) => return self.response_error(task, code),
};
let part = aggregate_attr(&sparse, attr, r);
acc = Some(match acc {
Some(prev) => {
nodedb_array::query::aggregate::AggregateResult::merge(prev, part)
}
None => part,
});
}
if return_partial {
let partial =
acc.map(|a| agg_result_to_partial(0, a))
.unwrap_or_else(|| ArrayAggPartial {
group_key: 0,
count: 0,
sum: 0.0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
welford_mean: 0.0,
welford_m2: 0.0,
});
return encode_bitemporal_agg_partial(
self,
task,
&[partial],
truncated_before_horizon,
);
}
let final_val = acc.and_then(|a| a.finalize());
let mut rows: Vec<BTreeMap<&'static str, AggCell>> = Vec::new();
let mut row: BTreeMap<&'static str, AggCell> = BTreeMap::new();
row.insert("result", float_or_null(final_val));
rows.push(row);
push_horizon_summary(&mut rows, emit_horizon, truncated_before_horizon);
return encode_agg_rows(self, task, &rows);
}
let dim = group_by_dim_idx as usize;
let mut order: Vec<CoordValue> = Vec::new();
let mut by_key: HashMap<CoordValue, nodedb_array::query::aggregate::AggregateResult> =
HashMap::new();
for tile in all_tiles {
let sparse = match unwrap_sparse(tile) {
Ok(s) => s,
Err(code) => return self.response_error(task, code),
};
let sparse = match apply_surrogate_filter(schema, sparse, cell_filter) {
Ok(s) => s,
Err(code) => return self.response_error(task, code),
};
let groups: Vec<GroupAggregate> = group_by_dim(&sparse, dim, attr, r);
for g in groups {
match by_key.get_mut(&g.key) {
Some(prev) => *prev = prev.merge(g.result),
None => {
order.push(g.key.clone());
by_key.insert(g.key, g.result);
}
}
}
}
if return_partial {
let partials: Vec<ArrayAggPartial> = order
.iter()
.filter_map(|key| {
by_key
.remove(key)
.map(|agg| agg_result_to_partial(coord_to_group_key(key), agg))
})
.collect();
return encode_bitemporal_agg_partial(self, task, &partials, truncated_before_horizon);
}
let mut rows: Vec<BTreeMap<&'static str, AggCell>> = Vec::with_capacity(order.len() + 1);
for key in order {
let result_val = by_key.remove(&key).and_then(|r| r.finalize());
let mut row: BTreeMap<&'static str, AggCell> = BTreeMap::new();
row.insert("group", coord_to_agg_cell(&key));
row.insert("result", float_or_null(result_val));
rows.push(row);
}
push_horizon_summary(&mut rows, emit_horizon, truncated_before_horizon);
encode_agg_rows(self, task, &rows)
}
}
fn push_horizon_summary(
rows: &mut Vec<BTreeMap<&'static str, AggCell>>,
emit_horizon: bool,
truncated_before_horizon: bool,
) {
if !emit_horizon {
return;
}
let mut summary: BTreeMap<&'static str, AggCell> = BTreeMap::new();
summary.insert(
"truncated_before_horizon",
AggCell::Bool(truncated_before_horizon),
);
rows.push(summary);
}