Skip to main content

reddb_engine/query/executors/
aggregation.rs

1//! Aggregation Framework
2//!
3//! Provides aggregation functions for query results.
4//!
5//! # Supported Functions
6//!
7//! - **COUNT**: Count rows (including COUNT(*) and COUNT DISTINCT)
8//! - **SUM**: Sum numeric values
9//! - **AVG**: Average of numeric values
10//! - **MIN**: Minimum value
11//! - **MAX**: Maximum value
12//! - **STDDEV**: Standard deviation
13//! - **VARIANCE**: Statistical variance
14//! - **PERCENTILE**: Nth percentile value
15//!
16//! # GROUP BY
17//!
18//! Aggregations can be grouped by one or more columns.
19//! HAVING clause filters groups after aggregation.
20
21use std::collections::HashMap;
22
23use super::super::engine::binding::{Binding, Value, Var};
24use super::value_compare::total_compare_values;
25
26// ============================================================================
27// Aggregator Trait
28// ============================================================================
29
30/// Trait for aggregation functions
31pub trait Aggregator: Send + Sync {
32    /// Process a single value
33    fn accumulate(&mut self, value: Option<&Value>);
34
35    /// Get the final aggregated result
36    fn finalize(&self) -> Value;
37
38    /// Reset for new group
39    fn reset(&mut self);
40
41    /// Create a fresh copy for a new group
42    fn new_instance(&self) -> Box<dyn Aggregator>;
43
44    /// Name of the aggregator
45    fn name(&self) -> &'static str;
46}
47
48// ============================================================================
49// COUNT Aggregator
50// ============================================================================
51
52/// COUNT aggregator
53#[derive(Debug, Clone, Default)]
54pub struct CountAggregator {
55    count: i64,
56    count_all: bool, // COUNT(*) vs COUNT(column)
57}
58
59impl CountAggregator {
60    /// Create COUNT(*) aggregator
61    pub fn count_all() -> Self {
62        Self {
63            count: 0,
64            count_all: true,
65        }
66    }
67
68    /// Create COUNT(column) aggregator (ignores nulls)
69    pub fn count_column() -> Self {
70        Self {
71            count: 0,
72            count_all: false,
73        }
74    }
75}
76
77impl Aggregator for CountAggregator {
78    fn accumulate(&mut self, value: Option<&Value>) {
79        if self.count_all || (value.is_some() && !matches!(value, Some(Value::Null))) {
80            self.count += 1;
81        }
82    }
83
84    fn finalize(&self) -> Value {
85        Value::Integer(self.count)
86    }
87
88    fn reset(&mut self) {
89        self.count = 0;
90    }
91
92    fn new_instance(&self) -> Box<dyn Aggregator> {
93        Box::new(Self {
94            count: 0,
95            count_all: self.count_all,
96        })
97    }
98
99    fn name(&self) -> &'static str {
100        "COUNT"
101    }
102}
103
104// ============================================================================
105// COUNT DISTINCT Aggregator
106// ============================================================================
107
108/// COUNT DISTINCT aggregator
109#[derive(Debug, Clone, Default)]
110pub struct CountDistinctAggregator {
111    seen: std::collections::HashSet<String>,
112}
113
114impl CountDistinctAggregator {
115    pub fn new() -> Self {
116        Self {
117            seen: std::collections::HashSet::new(),
118        }
119    }
120}
121
122impl Aggregator for CountDistinctAggregator {
123    fn accumulate(&mut self, value: Option<&Value>) {
124        if let Some(v) = value {
125            if !matches!(v, Value::Null) {
126                self.seen.insert(value_to_string(v));
127            }
128        }
129    }
130
131    fn finalize(&self) -> Value {
132        Value::Integer(self.seen.len() as i64)
133    }
134
135    fn reset(&mut self) {
136        self.seen.clear();
137    }
138
139    fn new_instance(&self) -> Box<dyn Aggregator> {
140        Box::new(Self::new())
141    }
142
143    fn name(&self) -> &'static str {
144        "COUNT_DISTINCT"
145    }
146}
147
148// ============================================================================
149// SUM Aggregator
150// ============================================================================
151
152/// SUM aggregator
153#[derive(Debug, Clone, Default)]
154pub struct SumAggregator {
155    sum: f64,
156    has_value: bool,
157    all_integers: bool,
158}
159
160impl SumAggregator {
161    pub fn new() -> Self {
162        Self {
163            sum: 0.0,
164            has_value: false,
165            all_integers: true,
166        }
167    }
168}
169
170impl Aggregator for SumAggregator {
171    fn accumulate(&mut self, value: Option<&Value>) {
172        if let Some(v) = value {
173            match v {
174                Value::Integer(i) => {
175                    self.sum += *i as f64;
176                    self.has_value = true;
177                }
178                Value::Float(f) => {
179                    self.sum += *f;
180                    self.has_value = true;
181                    if f.fract() != 0.0 {
182                        self.all_integers = false;
183                    }
184                }
185                _ => {}
186            }
187        }
188    }
189
190    fn finalize(&self) -> Value {
191        if self.has_value {
192            if self.all_integers && self.sum.fract() == 0.0 {
193                Value::Integer(self.sum as i64)
194            } else {
195                Value::Float(self.sum)
196            }
197        } else {
198            Value::Null
199        }
200    }
201
202    fn reset(&mut self) {
203        self.sum = 0.0;
204        self.has_value = false;
205        self.all_integers = true;
206    }
207
208    fn new_instance(&self) -> Box<dyn Aggregator> {
209        Box::new(Self::new())
210    }
211
212    fn name(&self) -> &'static str {
213        "SUM"
214    }
215}
216
217// ============================================================================
218// AVG Aggregator
219// ============================================================================
220
221/// AVG aggregator
222#[derive(Debug, Clone, Default)]
223pub struct AvgAggregator {
224    sum: f64,
225    count: i64,
226}
227
228impl AvgAggregator {
229    pub fn new() -> Self {
230        Self { sum: 0.0, count: 0 }
231    }
232}
233
234impl Aggregator for AvgAggregator {
235    fn accumulate(&mut self, value: Option<&Value>) {
236        if let Some(v) = value {
237            if let Some(n) = value_to_number(v) {
238                self.sum += n;
239                self.count += 1;
240            }
241        }
242    }
243
244    fn finalize(&self) -> Value {
245        if self.count > 0 {
246            Value::Float(self.sum / self.count as f64)
247        } else {
248            Value::Null
249        }
250    }
251
252    fn reset(&mut self) {
253        self.sum = 0.0;
254        self.count = 0;
255    }
256
257    fn new_instance(&self) -> Box<dyn Aggregator> {
258        Box::new(Self::new())
259    }
260
261    fn name(&self) -> &'static str {
262        "AVG"
263    }
264}
265
266// ============================================================================
267// MIN Aggregator
268// ============================================================================
269
270/// MIN aggregator
271#[derive(Debug, Clone, Default)]
272pub struct MinAggregator {
273    min: Option<Value>,
274}
275
276impl MinAggregator {
277    pub fn new() -> Self {
278        Self { min: None }
279    }
280}
281
282impl Aggregator for MinAggregator {
283    fn accumulate(&mut self, value: Option<&Value>) {
284        if let Some(v) = value {
285            if matches!(v, Value::Null) {
286                return;
287            }
288            match &self.min {
289                None => self.min = Some(v.clone()),
290                Some(current) => {
291                    if total_compare_values(v, current) == std::cmp::Ordering::Less {
292                        self.min = Some(v.clone());
293                    }
294                }
295            }
296        }
297    }
298
299    fn finalize(&self) -> Value {
300        self.min.clone().unwrap_or(Value::Null)
301    }
302
303    fn reset(&mut self) {
304        self.min = None;
305    }
306
307    fn new_instance(&self) -> Box<dyn Aggregator> {
308        Box::new(Self::new())
309    }
310
311    fn name(&self) -> &'static str {
312        "MIN"
313    }
314}
315
316// ============================================================================
317// MAX Aggregator
318// ============================================================================
319
320/// MAX aggregator
321#[derive(Debug, Clone, Default)]
322pub struct MaxAggregator {
323    max: Option<Value>,
324}
325
326impl MaxAggregator {
327    pub fn new() -> Self {
328        Self { max: None }
329    }
330}
331
332impl Aggregator for MaxAggregator {
333    fn accumulate(&mut self, value: Option<&Value>) {
334        if let Some(v) = value {
335            if matches!(v, Value::Null) {
336                return;
337            }
338            match &self.max {
339                None => self.max = Some(v.clone()),
340                Some(current) => {
341                    if total_compare_values(v, current) == std::cmp::Ordering::Greater {
342                        self.max = Some(v.clone());
343                    }
344                }
345            }
346        }
347    }
348
349    fn finalize(&self) -> Value {
350        self.max.clone().unwrap_or(Value::Null)
351    }
352
353    fn reset(&mut self) {
354        self.max = None;
355    }
356
357    fn new_instance(&self) -> Box<dyn Aggregator> {
358        Box::new(Self::new())
359    }
360
361    fn name(&self) -> &'static str {
362        "MAX"
363    }
364}
365
366// ============================================================================
367// SAMPLE Aggregator
368// ============================================================================
369
370/// SAMPLE aggregator (returns first non-null value)
371#[derive(Debug, Clone, Default)]
372pub struct SampleAggregator {
373    value: Option<Value>,
374}
375
376impl SampleAggregator {
377    pub fn new() -> Self {
378        Self { value: None }
379    }
380}
381
382impl Aggregator for SampleAggregator {
383    fn accumulate(&mut self, value: Option<&Value>) {
384        if self.value.is_some() {
385            return;
386        }
387        if let Some(v) = value {
388            if !matches!(v, Value::Null) {
389                self.value = Some(v.clone());
390            }
391        }
392    }
393
394    fn finalize(&self) -> Value {
395        self.value.clone().unwrap_or(Value::Null)
396    }
397
398    fn reset(&mut self) {
399        self.value = None;
400    }
401
402    fn new_instance(&self) -> Box<dyn Aggregator> {
403        Box::new(Self::new())
404    }
405
406    fn name(&self) -> &'static str {
407        "SAMPLE"
408    }
409}
410
411// ============================================================================
412// GROUP_CONCAT Aggregator
413// ============================================================================
414
415/// GROUP_CONCAT aggregator
416#[derive(Debug, Clone)]
417pub struct GroupConcatAggregator {
418    separator: String,
419    values: Vec<String>,
420}
421
422impl GroupConcatAggregator {
423    pub fn new(separator: Option<String>) -> Self {
424        Self {
425            separator: separator.unwrap_or_else(|| " ".to_string()),
426            values: Vec::new(),
427        }
428    }
429}
430
431impl Aggregator for GroupConcatAggregator {
432    fn accumulate(&mut self, value: Option<&Value>) {
433        if let Some(v) = value {
434            if !matches!(v, Value::Null) {
435                self.values.push(value_to_string(v));
436            }
437        }
438    }
439
440    fn finalize(&self) -> Value {
441        if self.values.is_empty() {
442            Value::Null
443        } else {
444            Value::String(self.values.join(&self.separator))
445        }
446    }
447
448    fn reset(&mut self) {
449        self.values.clear();
450    }
451
452    fn new_instance(&self) -> Box<dyn Aggregator> {
453        Box::new(Self::new(Some(self.separator.clone())))
454    }
455
456    fn name(&self) -> &'static str {
457        "GROUP_CONCAT"
458    }
459}
460
461// ============================================================================
462// STDDEV Aggregator (Population Standard Deviation)
463// ============================================================================
464
465/// Standard deviation aggregator
466#[derive(Debug, Clone, Default)]
467pub struct StdDevAggregator {
468    values: Vec<f64>,
469}
470
471impl StdDevAggregator {
472    pub fn new() -> Self {
473        Self { values: Vec::new() }
474    }
475}
476
477impl Aggregator for StdDevAggregator {
478    fn accumulate(&mut self, value: Option<&Value>) {
479        if let Some(v) = value {
480            if let Some(n) = value_to_number(v) {
481                self.values.push(n);
482            }
483        }
484    }
485
486    fn finalize(&self) -> Value {
487        if self.values.is_empty() {
488            return Value::Null;
489        }
490
491        let n = self.values.len() as f64;
492        let mean = self.values.iter().sum::<f64>() / n;
493        let variance = self.values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
494
495        Value::Float(variance.sqrt())
496    }
497
498    fn reset(&mut self) {
499        self.values.clear();
500    }
501
502    fn new_instance(&self) -> Box<dyn Aggregator> {
503        Box::new(Self::new())
504    }
505
506    fn name(&self) -> &'static str {
507        "STDDEV"
508    }
509}
510
511// ============================================================================
512// VARIANCE Aggregator
513// ============================================================================
514
515/// Variance aggregator
516#[derive(Debug, Clone, Default)]
517pub struct VarianceAggregator {
518    values: Vec<f64>,
519}
520
521impl VarianceAggregator {
522    pub fn new() -> Self {
523        Self { values: Vec::new() }
524    }
525}
526
527impl Aggregator for VarianceAggregator {
528    fn accumulate(&mut self, value: Option<&Value>) {
529        if let Some(v) = value {
530            if let Some(n) = value_to_number(v) {
531                self.values.push(n);
532            }
533        }
534    }
535
536    fn finalize(&self) -> Value {
537        if self.values.is_empty() {
538            return Value::Null;
539        }
540
541        let n = self.values.len() as f64;
542        let mean = self.values.iter().sum::<f64>() / n;
543        let variance = self.values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
544
545        Value::Float(variance)
546    }
547
548    fn reset(&mut self) {
549        self.values.clear();
550    }
551
552    fn new_instance(&self) -> Box<dyn Aggregator> {
553        Box::new(Self::new())
554    }
555
556    fn name(&self) -> &'static str {
557        "VARIANCE"
558    }
559}
560
561// ============================================================================
562// PERCENTILE Aggregator
563// ============================================================================
564
565/// Percentile aggregator
566#[derive(Debug, Clone)]
567pub struct PercentileAggregator {
568    values: Vec<f64>,
569    percentile: f64, // 0.0 to 1.0 (e.g., 0.5 for median)
570}
571
572impl PercentileAggregator {
573    pub fn new(percentile: f64) -> Self {
574        Self {
575            values: Vec::new(),
576            percentile: percentile.clamp(0.0, 1.0),
577        }
578    }
579
580    /// Create median aggregator (50th percentile)
581    pub fn median() -> Self {
582        Self::new(0.5)
583    }
584}
585
586impl Aggregator for PercentileAggregator {
587    fn accumulate(&mut self, value: Option<&Value>) {
588        if let Some(v) = value {
589            if let Some(n) = value_to_number(v) {
590                self.values.push(n);
591            }
592        }
593    }
594
595    fn finalize(&self) -> Value {
596        if self.values.is_empty() {
597            return Value::Null;
598        }
599
600        let mut sorted = self.values.clone();
601        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
602
603        let index = (self.percentile * (sorted.len() - 1) as f64).round() as usize;
604        Value::Float(sorted[index])
605    }
606
607    fn reset(&mut self) {
608        self.values.clear();
609    }
610
611    fn new_instance(&self) -> Box<dyn Aggregator> {
612        Box::new(Self::new(self.percentile))
613    }
614
615    fn name(&self) -> &'static str {
616        "PERCENTILE"
617    }
618}
619
620// ============================================================================
621// GROUP BY Executor
622// ============================================================================
623
624/// Definition of an aggregation to compute
625pub struct AggregationDef {
626    /// Source variable to aggregate
627    pub source_var: Var,
628    /// Result variable name
629    pub result_var: Var,
630    /// Aggregator factory
631    pub aggregator: Box<dyn Aggregator>,
632}
633
634impl std::fmt::Debug for AggregationDef {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        f.debug_struct("AggregationDef")
637            .field("source_var", &self.source_var)
638            .field("result_var", &self.result_var)
639            .field("aggregator", &self.aggregator.name())
640            .finish()
641    }
642}
643
644/// Soft memory cap for in-process hash aggregation.
645///
646/// When the groups HashMap grows beyond this threshold, an OOM-guard
647/// warning fires. Full spill-to-disk requires changing the calling
648/// convention to a row-at-a-time streaming API (tracked separately).
649#[cfg(debug_assertions)]
650const WORK_MEM_BYTES: usize = 64 * 1024 * 1024; // 64 MiB
651
652/// Estimated heap cost per group entry in the streaming HashMap.
653///
654/// In practice each entry holds:
655///   - a String group-key (~32 B avg)
656///   - group key Var/Value pairs (~64 B)
657///   - one Box<dyn Aggregator> per agg_def (~64 B each, assume ≤4 defs → ~256 B)
658///
659/// 512 B is deliberately conservative to avoid premature eviction
660/// in the common case.
661#[cfg(debug_assertions)]
662const AVG_GROUP_ENTRY_BYTES: usize = 512;
663
664/// 1-pass streaming GROUP BY.
665///
666/// Previous implementation accumulated ALL input bindings per group
667/// (`HashMap<String, Vec<Binding>>`), then ran aggregations in a
668/// second pass. Memory cost: O(input_rows) in the groups map.
669///
670/// This version keeps only the incremental aggregation state per group
671/// — one `Box<dyn Aggregator>` per `AggregationDef`. Memory cost drops
672/// to O(distinct_groups × agg_defs), which is dramatically lower for
673/// high-cardinality inputs with few distinct groups.
674pub fn execute_group_by(
675    bindings: Vec<Binding>,
676    group_vars: &[Var],
677    aggregations: &[AggregationDef],
678) -> Vec<Binding> {
679    // Each entry: (snapshot of group-key values from first binding,
680    //              incremental aggregator state for each agg_def)
681    let mut groups: HashMap<String, (Binding, Vec<Box<dyn Aggregator>>)> = HashMap::new();
682
683    // Reused scratch buffer for the group key. Rows falling into an existing
684    // group only borrow it for the lookup, so the key String is cloned once
685    // per *distinct* group (on insert) rather than once per row.
686    let mut scratch = String::with_capacity(64);
687
688    for binding in &bindings {
689        scratch.clear();
690        write_group_key(binding, group_vars, &mut scratch);
691        if !groups.contains_key(scratch.as_str()) {
692            // New group: capture group key values once from this first binding.
693            let mut key_binding = Binding::empty();
694            for var in group_vars {
695                if let Some(value) = binding.get(var) {
696                    let partial = Binding::one(var.clone(), value.clone());
697                    key_binding = key_binding.merge(&partial).unwrap_or(key_binding);
698                }
699            }
700            // Allocate one fresh aggregator instance per agg def.
701            let agg_instances = aggregations
702                .iter()
703                .map(|a| a.aggregator.new_instance())
704                .collect();
705            groups.insert(scratch.clone(), (key_binding, agg_instances));
706        }
707        let entry = groups
708            .get_mut(scratch.as_str())
709            .expect("group inserted above when absent");
710
711        // Accumulate each aggregation in a single pass over the binding.
712        for (i, agg_def) in aggregations.iter().enumerate() {
713            entry.1[i].accumulate(binding.get(&agg_def.source_var));
714        }
715
716        // Memory guard: O(1) check, avoids estimating actual heap usage.
717        // When the number of distinct groups × avg cost exceeds WORK_MEM,
718        // we've likely exhausted the intended budget. For now we continue
719        // (the data is already in memory via the input Vec<Binding>) but
720        // emit a debug trace so operators can see when this fires.
721        #[cfg(debug_assertions)]
722        if groups.len() * AVG_GROUP_ENTRY_BYTES > WORK_MEM_BYTES {
723            // Only log once — on entry count crossing the threshold,
724            // not on every subsequent row.
725            static WARNED: std::sync::atomic::AtomicBool =
726                std::sync::atomic::AtomicBool::new(false);
727            if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
728                eprintln!(
729                    "[reddb] hash-agg: {} distinct groups × {} B ≈ {} MiB exceeds WORK_MEM {}  MiB; \
730                     disk spill not yet wired — upgrade calling convention to streaming for OOM safety",
731                    groups.len(),
732                    AVG_GROUP_ENTRY_BYTES,
733                    (groups.len() * AVG_GROUP_ENTRY_BYTES) / (1024 * 1024),
734                    WORK_MEM_BYTES / (1024 * 1024),
735                );
736            }
737        }
738    }
739
740    // Finalize: emit one output Binding per distinct group.
741    let mut results = Vec::with_capacity(groups.len());
742    for (_, (key_binding, agg_instances)) in groups {
743        let mut result = key_binding;
744        for (i, agg_def) in aggregations.iter().enumerate() {
745            let agg_result = agg_instances[i].finalize();
746            let partial = Binding::one(agg_def.result_var.clone(), agg_result);
747            result = result.merge(&partial).unwrap_or(result);
748        }
749        results.push(result);
750    }
751    results
752}
753
754/// Execute HAVING clause (filter on aggregated results)
755pub fn execute_having<F>(bindings: Vec<Binding>, predicate: F) -> Vec<Binding>
756where
757    F: Fn(&Binding) -> bool,
758{
759    bindings.into_iter().filter(|b| predicate(b)).collect()
760}
761
762// ============================================================================
763// Helper Functions
764// ============================================================================
765
766/// Format a single `Value` as a String. **Cold path** — used by
767/// non-hot consumers like GROUP_CONCAT result formatting. The
768/// hot group-by key path inlines this logic into a shared buffer
769/// in [`write_group_key`] to avoid per-row allocations.
770fn value_to_string(value: &Value) -> String {
771    match value {
772        Value::Node(id) => format!("node:{}", id),
773        Value::Edge(id) => format!("edge:{}", id),
774        Value::String(s) => s.clone(),
775        Value::Integer(i) => i.to_string(),
776        Value::Float(f) => f.to_string(),
777        Value::Boolean(b) => b.to_string(),
778        Value::Uri(u) => u.clone(),
779        Value::Null => "null".to_string(),
780    }
781}
782
783/// Build a group-by key for one row into a caller-provided buffer.
784///
785/// **Hot path** — called once per row in `execute_group_by`. The caller
786/// reuses a single scratch `String` across all rows (clearing it before
787/// each call), so rows landing in an existing group allocate nothing:
788/// the buffer is only cloned when a *new* group is inserted. Everything
789/// for one key is written into the shared buffer with no intermediate
790/// per-value String allocations.
791#[doc(hidden)]
792pub fn write_group_key(binding: &Binding, group_vars: &[Var], key: &mut String) {
793    use std::fmt::Write;
794    for (i, var) in group_vars.iter().enumerate() {
795        if i > 0 {
796            key.push('|');
797        }
798        key.push_str(var.name());
799        key.push('=');
800        match binding.get(var) {
801            None => key.push_str("NULL"),
802            Some(Value::Null) => key.push_str("null"),
803            Some(Value::String(s)) => key.push_str(s),
804            Some(Value::Integer(n)) => {
805                let _ = write!(key, "{n}");
806            }
807            Some(Value::Float(f)) => {
808                let _ = write!(key, "{f}");
809            }
810            Some(Value::Boolean(b)) => {
811                let _ = write!(key, "{b}");
812            }
813            Some(Value::Node(id)) => {
814                key.push_str("node:");
815                key.push_str(id);
816            }
817            Some(Value::Edge(id)) => {
818                key.push_str("edge:");
819                key.push_str(id);
820            }
821            Some(Value::Uri(u)) => key.push_str(u),
822        }
823    }
824}
825
826fn value_to_number(value: &Value) -> Option<f64> {
827    match value {
828        Value::Integer(i) => Some(*i as f64),
829        Value::Float(f) => Some(*f),
830        Value::String(s) => s.parse().ok(),
831        _ => None,
832    }
833}
834
835// ============================================================================
836// Aggregator Factory
837// ============================================================================
838
839/// Create an aggregator by name
840pub fn create_aggregator(name: &str) -> Option<Box<dyn Aggregator>> {
841    match name.to_uppercase().as_str() {
842        "COUNT" => Some(Box::new(CountAggregator::count_all())),
843        "COUNT_COLUMN" => Some(Box::new(CountAggregator::count_column())),
844        "COUNT_DISTINCT" => Some(Box::new(CountDistinctAggregator::new())),
845        "SUM" => Some(Box::new(SumAggregator::new())),
846        "AVG" => Some(Box::new(AvgAggregator::new())),
847        "MIN" => Some(Box::new(MinAggregator::new())),
848        "MAX" => Some(Box::new(MaxAggregator::new())),
849        "STDDEV" => Some(Box::new(StdDevAggregator::new())),
850        "VARIANCE" => Some(Box::new(VarianceAggregator::new())),
851        "MEDIAN" => Some(Box::new(PercentileAggregator::median())),
852        "SAMPLE" => Some(Box::new(SampleAggregator::new())),
853        "GROUP_CONCAT" => Some(Box::new(GroupConcatAggregator::new(None))),
854        _ => None,
855    }
856}
857
858// ============================================================================
859// Tests
860// ============================================================================
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    fn make_binding(pairs: &[(&str, Value)]) -> Binding {
867        if pairs.is_empty() {
868            return Binding::empty();
869        }
870
871        let mut result = Binding::one(Var::new(pairs[0].0), pairs[0].1.clone());
872
873        for (k, v) in pairs.iter().skip(1) {
874            let next = Binding::one(Var::new(k), v.clone());
875            result = result.merge(&next).unwrap_or(result);
876        }
877
878        result
879    }
880
881    #[test]
882    fn test_count() {
883        let mut counter = CountAggregator::count_all();
884        counter.accumulate(Some(&Value::Integer(1)));
885        counter.accumulate(Some(&Value::Integer(2)));
886        counter.accumulate(None);
887        counter.accumulate(Some(&Value::Null));
888
889        assert_eq!(counter.finalize(), Value::Integer(4));
890    }
891
892    #[test]
893    fn test_count_column() {
894        let mut counter = CountAggregator::count_column();
895        counter.accumulate(Some(&Value::Integer(1)));
896        counter.accumulate(None);
897        counter.accumulate(Some(&Value::Null));
898        counter.accumulate(Some(&Value::Integer(2)));
899
900        assert_eq!(counter.finalize(), Value::Integer(2)); // Only non-null values
901    }
902
903    #[test]
904    fn test_sum() {
905        let mut sum = SumAggregator::new();
906        sum.accumulate(Some(&Value::Integer(10)));
907        sum.accumulate(Some(&Value::Float(5.5)));
908        sum.accumulate(Some(&Value::Integer(4)));
909
910        assert_eq!(sum.finalize(), Value::Float(19.5));
911    }
912
913    #[test]
914    fn test_avg() {
915        let mut avg = AvgAggregator::new();
916        avg.accumulate(Some(&Value::Integer(10)));
917        avg.accumulate(Some(&Value::Integer(20)));
918        avg.accumulate(Some(&Value::Integer(30)));
919
920        assert_eq!(avg.finalize(), Value::Float(20.0));
921    }
922
923    #[test]
924    fn test_min_max() {
925        let mut min = MinAggregator::new();
926        let mut max = MaxAggregator::new();
927
928        for val in [5, 2, 8, 1, 9] {
929            min.accumulate(Some(&Value::Integer(val)));
930            max.accumulate(Some(&Value::Integer(val)));
931        }
932
933        assert_eq!(min.finalize(), Value::Integer(1));
934        assert_eq!(max.finalize(), Value::Integer(9));
935    }
936
937    #[test]
938    fn test_count_distinct() {
939        let mut distinct = CountDistinctAggregator::new();
940        distinct.accumulate(Some(&Value::String("a".to_string())));
941        distinct.accumulate(Some(&Value::String("b".to_string())));
942        distinct.accumulate(Some(&Value::String("a".to_string())));
943        distinct.accumulate(Some(&Value::String("c".to_string())));
944
945        assert_eq!(distinct.finalize(), Value::Integer(3));
946    }
947
948    #[test]
949    fn test_group_by() {
950        let bindings = vec![
951            make_binding(&[
952                ("dept", Value::String("Sales".to_string())),
953                ("salary", Value::Integer(50000)),
954            ]),
955            make_binding(&[
956                ("dept", Value::String("Sales".to_string())),
957                ("salary", Value::Integer(60000)),
958            ]),
959            make_binding(&[
960                ("dept", Value::String("Engineering".to_string())),
961                ("salary", Value::Integer(80000)),
962            ]),
963            make_binding(&[
964                ("dept", Value::String("Engineering".to_string())),
965                ("salary", Value::Integer(90000)),
966            ]),
967        ];
968
969        let aggs = vec![
970            AggregationDef {
971                source_var: Var::new("salary"),
972                result_var: Var::new("total"),
973                aggregator: Box::new(SumAggregator::new()),
974            },
975            AggregationDef {
976                source_var: Var::new("salary"),
977                result_var: Var::new("count"),
978                aggregator: Box::new(CountAggregator::count_all()),
979            },
980        ];
981
982        let results = execute_group_by(bindings, &[Var::new("dept")], &aggs);
983
984        assert_eq!(results.len(), 2);
985
986        // Find Sales result
987        let sales = results
988            .iter()
989            .find(|b| b.get(&Var::new("dept")) == Some(&Value::String("Sales".to_string())))
990            .expect("Sales group not found");
991
992        assert_eq!(sales.get(&Var::new("total")), Some(&Value::Integer(110000)));
993        assert_eq!(sales.get(&Var::new("count")), Some(&Value::Integer(2)));
994    }
995
996    #[test]
997    fn test_percentile() {
998        let mut p50 = PercentileAggregator::median();
999        for v in [1, 2, 3, 4, 5, 6, 7, 8, 9] {
1000            p50.accumulate(Some(&Value::Integer(v)));
1001        }
1002        // Median of 1-9 is 5
1003        assert_eq!(p50.finalize(), Value::Float(5.0));
1004    }
1005}