use std::collections::HashMap;
use crate::bridge::scan_filter::ScanFilter;
use crate::data::executor::core_loop::CoreLoop;
use crate::data::executor::handlers::accum::GroupState;
use crate::data::executor::handlers::spill::groupby::GroupBySpiller;
use nodedb_physical::physical_plan::{AggregateSpec, GroupKeySpec};
use nodedb_query::msgpack_scan;
pub(in crate::data::executor) type AccumulatedGroups = (
HashMap<String, GroupState>,
HashMap<String, HashMap<String, GroupState>>,
);
pub(in crate::data::executor) struct AccumulateGroupsParams<'a> {
pub docs: &'a [(String, Vec<u8>)],
pub group_by: &'a [GroupKeySpec],
pub aggregates: &'a [AggregateSpec],
pub filters: &'a [u8],
pub sub_group_by: &'a [String],
pub sub_aggregates: &'a [AggregateSpec],
}
impl CoreLoop {
pub(in crate::data::executor) fn accumulate_groups(
&mut self,
params: AccumulateGroupsParams<'_>,
) -> crate::Result<AccumulatedGroups> {
let AccumulateGroupsParams {
docs,
group_by,
aggregates,
filters,
sub_group_by,
sub_aggregates,
} = params;
let filter_predicates: Vec<ScanFilter> = if filters.is_empty() {
Vec::new()
} else {
match zerompk::from_msgpack(filters) {
Ok(f) => f,
Err(e) => {
tracing::warn!(core = self.core_id, error = %e, "filter predicate deserialization failed");
Vec::new()
}
}
};
let use_field_index = filter_predicates.len() + group_by.len() >= 2;
let need_sub = !sub_group_by.is_empty() && !sub_aggregates.is_empty();
let sub_specs: Vec<GroupKeySpec> = if need_sub {
sub_group_by
.iter()
.map(|s| GroupKeySpec::column(s.as_str()))
.collect()
} else {
Vec::new()
};
let spill_dir = self
.data_dir
.join("groupby-spill")
.join(format!("core-{}", self.core_id));
let cap = self.query_tuning.groupby_max_groups_in_mem;
let mut spiller = GroupBySpiller::new(spill_dir, cap, self.governor.clone())?;
let mut spill_err: Option<crate::Error> = None;
let chunk_size = self.query_tuning.aggregate_chunk_size;
for chunk in docs.chunks(chunk_size) {
if spill_err.is_some() {
break;
}
for (_, value) in chunk {
let outer_key = if use_field_index {
let idx = msgpack_scan::FieldIndex::build(value, 0)
.unwrap_or_else(msgpack_scan::FieldIndex::empty);
if !filter_predicates
.iter()
.all(|f| f.matches_binary_indexed(value, &idx))
{
continue;
}
msgpack_scan::group_key::build_group_key_indexed(value, group_by, &idx)
} else {
if !filter_predicates.iter().all(|f| f.matches_binary(value)) {
continue;
}
msgpack_scan::build_group_key(value, group_by)
};
if let Err(e) = spiller.feed(outer_key.clone(), aggregates, value) {
spill_err = Some(e);
break;
}
if need_sub {
let sub_key = msgpack_scan::build_group_key(value, &sub_specs);
let composite = format!("{outer_key}\x1F{sub_key}");
if let Err(e) = spiller.feed(composite, sub_aggregates, value) {
spill_err = Some(e);
break;
}
}
}
}
if let Some(e) = spill_err {
return Err(e);
}
let consolidated = spiller.finalize()?;
let mut groups: HashMap<String, GroupState> = HashMap::new();
let mut sub_groups: HashMap<String, HashMap<String, GroupState>> = HashMap::new();
for (key, state) in consolidated {
if let Some(sep_pos) = key.find('\x1F') {
let outer = key[..sep_pos].to_string();
let sub = key[sep_pos + 1..].to_string();
sub_groups.entry(outer).or_default().insert(sub, state);
} else {
groups.insert(key, state);
}
}
Ok((groups, sub_groups))
}
}