use super::super::select_ast::AggregateType;
use super::super::select_optimizer::{AggregateComputation, AggregationPlan};
use super::value_ops::compare_values_ordering;
use crate::{
query::result::QueryRow,
types::{RowKey, Value},
};
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[cfg(test)]
thread_local! {
pub(crate) static GROUP_KEY_COMPARISONS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
#[derive(Debug)]
pub(super) struct AggregationState {
pub(super) groups: Vec<(Vec<Value>, Vec<AggregateValue>)>,
pub(super) group_index: FxHashMap<u64, Vec<usize>>,
pub(super) memory_usage_bytes: usize,
pub(super) memory_limit_bytes: usize,
}
fn hash_group_key(key: &[Value]) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.len().hash(&mut hasher);
for v in key {
hash_one_value(v, &mut hasher);
}
hasher.finish()
}
fn hash_one_value<H: Hasher>(v: &Value, h: &mut H) {
std::mem::discriminant(v).hash(h);
match v {
Value::Null => {}
Value::Boolean(b) => b.hash(h),
Value::Integer(x) => x.hash(h),
Value::BigInt(x) | Value::Counter(x) | Value::Timestamp(x) | Value::Time(x) => x.hash(h),
Value::Date(x) => x.hash(h),
Value::TinyInt(x) => x.hash(h),
Value::SmallInt(x) => x.hash(h),
Value::Float(f) => (if *f == 0.0 { 0.0f64 } else { *f }).to_bits().hash(h),
Value::Float32(f) => (if *f == 0.0 { 0.0f32 } else { *f }).to_bits().hash(h),
Value::Text(s) => s.hash(h),
Value::Blob(b) | Value::Varint(b) | Value::Inet(b) => b.hash(h),
Value::Uuid(u) => u.hash(h),
Value::Decimal { scale, unscaled } => {
scale.hash(h);
unscaled.hash(h);
}
Value::Duration {
months,
days,
nanos,
} => {
months.hash(h);
days.hash(h);
nanos.hash(h);
}
Value::List(items) | Value::Set(items) | Value::Tuple(items) => {
for item in items {
hash_one_value(item, h);
}
}
Value::Map(pairs) => {
for (k, val) in pairs {
hash_one_value(k, h);
hash_one_value(val, h);
}
}
Value::Frozen(inner) => hash_one_value(inner, h),
Value::Json(_) | Value::Udt(_) | Value::Tombstone(_) => {}
}
}
#[derive(Debug, Clone)]
pub(super) enum AggregateValue {
Count(u64),
Sum(f64),
Avg { sum: f64, count: u64 },
Min(Value),
Max(Value),
}
pub(super) fn build_group_key(row: &QueryRow, group_by_columns: &[String]) -> Vec<Value> {
if group_by_columns.is_empty() {
return vec![Value::Null];
}
group_by_columns
.iter()
.map(|col| row.values.get(col.as_str()).cloned().unwrap_or(Value::Null))
.collect()
}
pub(super) fn find_or_init_group(
state: &mut AggregationState,
key: Vec<Value>,
aggregates: &[AggregateComputation],
) -> usize {
let hash = hash_group_key(&key);
if let Some(candidates) = state.group_index.get(&hash) {
for &idx in candidates {
#[cfg(test)]
GROUP_KEY_COMPARISONS.with(|c| c.set(c.get() + 1));
if state.groups[idx].0 == key {
return idx;
}
}
}
let initial: Vec<_> = aggregates
.iter()
.map(|c| match c.function {
AggregateType::Count => AggregateValue::Count(0),
AggregateType::Sum => AggregateValue::Sum(0.0),
AggregateType::Avg => AggregateValue::Avg { sum: 0.0, count: 0 },
AggregateType::Min => AggregateValue::Min(Value::Null),
AggregateType::Max => AggregateValue::Max(Value::Null),
})
.collect();
let new_index = state.groups.len();
state.groups.push((key, initial));
state.group_index.entry(hash).or_default().push(new_index);
new_index
}
pub(super) fn update_aggregate(
state: &mut AggregateValue,
agg_comp: &AggregateComputation,
row: &QueryRow,
) {
let is_star = agg_comp.column == "*";
let value: Option<&Value> = if is_star {
None
} else {
row.values.get(agg_comp.column.as_str())
};
let is_null = !is_star && value.is_none_or(Value::is_null);
match state {
AggregateValue::Count(count) => {
if is_star || !is_null {
*count += 1;
}
}
AggregateValue::Sum(sum) => {
if let Some(v) = value.and_then(Value::as_f64) {
*sum += v;
}
}
AggregateValue::Avg { sum, count } => {
if let Some(v) = value.and_then(Value::as_f64) {
*sum += v;
*count += 1;
}
}
AggregateValue::Min(min_val) => {
if let Some(v) = value {
if !v.is_null()
&& (min_val.is_null() || compare_values_ordering(v, min_val).is_lt())
{
*min_val = v.clone();
}
}
}
AggregateValue::Max(max_val) => {
if let Some(v) = value {
if !v.is_null()
&& (max_val.is_null() || compare_values_ordering(v, max_val).is_gt())
{
*max_val = v.clone();
}
}
}
}
}
pub(super) fn finalize_group(
group_key: Vec<Value>,
group_aggregates: Vec<AggregateValue>,
agg_plan: &AggregationPlan,
) -> QueryRow {
let mut row_values: HashMap<std::sync::Arc<str>, Value> =
HashMap::with_capacity(agg_plan.group_by_columns.len() + agg_plan.aggregates.len());
for (i, out_name) in agg_plan.group_by_output_names.iter().enumerate() {
if let Some(v) = group_key.get(i) {
row_values.insert(out_name.as_str().into(), v.clone());
}
}
for (i, agg_comp) in agg_plan.aggregates.iter().enumerate() {
let result_value = match &group_aggregates[i] {
AggregateValue::Count(count) => Value::BigInt(*count as i64),
AggregateValue::Sum(sum) => Value::Float(*sum),
AggregateValue::Avg { sum, count } => {
if *count > 0 {
Value::Float(sum / (*count as f64))
} else {
Value::Null
}
}
AggregateValue::Min(val) | AggregateValue::Max(val) => val.clone(),
};
row_values.insert(agg_comp.alias.as_str().into(), result_value);
}
QueryRow {
values: row_values,
key: RowKey::new(vec![]),
metadata: Default::default(),
cell_metadata: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::select_ast::AggregateType;
fn count_star_plan() -> AggregationPlan {
AggregationPlan {
group_by_columns: vec!["g".to_string()],
group_by_output_names: vec!["g".to_string()],
aggregates: vec![AggregateComputation {
function: AggregateType::Count,
column: "*".to_string(),
alias: "c".to_string(),
distinct: false,
}],
}
}
fn new_state() -> AggregationState {
AggregationState {
groups: Vec::new(),
group_index: FxHashMap::default(),
memory_usage_bytes: 0,
memory_limit_bytes: usize::MAX,
}
}
#[test]
fn find_or_init_group_is_subquadratic() {
let plan = count_star_plan();
let groups_count = 200usize;
let repeats = 3usize; let rows = groups_count * repeats;
let mut state = new_state();
GROUP_KEY_COMPARISONS.with(|c| c.set(0));
for r in 0..repeats {
for g in 0..groups_count {
let key = vec![Value::Integer(g as i32)];
let idx = find_or_init_group(&mut state, key, &plan.aggregates);
assert_eq!(idx, g, "row (repeat {r}, group {g}) maps to a stable index");
}
}
let comparisons = GROUP_KEY_COMPARISONS.with(|c| c.get());
assert_eq!(
state.groups.len(),
groups_count,
"exactly one group per key"
);
assert!(
comparisons <= rows * 2,
"issue #1587: group lookup must be sub-quadratic — {comparisons} comparisons \
for {rows} rows / {groups_count} groups (linear scan would be ~{})",
rows * groups_count / 2
);
}
#[test]
fn find_or_init_group_preserves_equality_semantics() {
let plan = count_star_plan();
let mut state = new_state();
let i0 = find_or_init_group(&mut state, vec![Value::Float(0.0)], &plan.aggregates);
let i1 = find_or_init_group(&mut state, vec![Value::Float(-0.0)], &plan.aggregates);
assert_eq!(i0, i1, "-0.0 and +0.0 must land in the same group");
let n0 = find_or_init_group(&mut state, vec![Value::Float(f64::NAN)], &plan.aggregates);
let n1 = find_or_init_group(&mut state, vec![Value::Float(f64::NAN)], &plan.aggregates);
assert_ne!(n0, n1, "each NaN key forms its own group (NaN != NaN)");
let a = find_or_init_group(&mut state, vec![Value::Integer(1)], &plan.aggregates);
let b = find_or_init_group(&mut state, vec![Value::BigInt(1)], &plan.aggregates);
assert_ne!(a, b, "Integer(1) and BigInt(1) are distinct groups");
}
}