Skip to main content

oxirs_arq/
aggregate_executor.rs

1//! SPARQL 1.1 Aggregate Function Executor
2//!
3//! Implements GROUP BY, HAVING, and aggregate functions (COUNT, SUM, AVG, MIN, MAX,
4//! SAMPLE, GROUP_CONCAT) as specified in the SPARQL 1.1 Query Language specification.
5
6use std::collections::HashMap;
7use std::fmt;
8
9/// SPARQL 1.1 aggregate functions.
10#[derive(Debug, Clone, PartialEq)]
11pub enum AggregateFunc {
12    /// COUNT(?var) or COUNT(DISTINCT ?var)
13    Count { distinct: bool },
14    /// SUM(?var)
15    Sum,
16    /// AVG(?var)
17    Avg,
18    /// MIN(?var)
19    Min,
20    /// MAX(?var)
21    Max,
22    /// SAMPLE(?var) — returns an arbitrary non-null value
23    Sample,
24    /// GROUP_CONCAT(?var; separator="...")
25    GroupConcat { separator: String },
26    /// COUNT(*) — counts all rows including those where ?var is unbound
27    CountAll,
28}
29
30/// A value produced by an aggregate expression.
31#[derive(Debug, Clone, PartialEq)]
32pub enum AggregateValue {
33    /// Integer aggregate result (e.g., COUNT)
34    Integer(i64),
35    /// Floating-point aggregate result (SUM/AVG of decimals)
36    Float(f64),
37    /// String aggregate result (SAMPLE, GROUP_CONCAT)
38    Text(String),
39    /// No value — group was empty or variable was unbound throughout
40    Null,
41}
42
43impl fmt::Display for AggregateValue {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            AggregateValue::Integer(n) => write!(f, "{n}"),
47            AggregateValue::Float(v) => write!(f, "{v}"),
48            AggregateValue::Text(s) => write!(f, "{s}"),
49            AggregateValue::Null => write!(f, "NULL"),
50        }
51    }
52}
53
54impl PartialOrd for AggregateValue {
55    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
56        use std::cmp::Ordering;
57        match (self, other) {
58            (AggregateValue::Null, AggregateValue::Null) => Some(Ordering::Equal),
59            (AggregateValue::Null, _) => Some(Ordering::Less),
60            (_, AggregateValue::Null) => Some(Ordering::Greater),
61            (AggregateValue::Integer(a), AggregateValue::Integer(b)) => a.partial_cmp(b),
62            (AggregateValue::Integer(a), AggregateValue::Float(b)) => (*a as f64).partial_cmp(b),
63            (AggregateValue::Float(a), AggregateValue::Integer(b)) => a.partial_cmp(&(*b as f64)),
64            (AggregateValue::Float(a), AggregateValue::Float(b)) => a.partial_cmp(b),
65            (AggregateValue::Text(a), AggregateValue::Text(b)) => a.partial_cmp(b),
66            // Integer / Float < Text
67            (AggregateValue::Integer(_), AggregateValue::Text(_)) => Some(Ordering::Less),
68            (AggregateValue::Float(_), AggregateValue::Text(_)) => Some(Ordering::Less),
69            (AggregateValue::Text(_), AggregateValue::Integer(_)) => Some(Ordering::Greater),
70            (AggregateValue::Text(_), AggregateValue::Float(_)) => Some(Ordering::Greater),
71        }
72    }
73}
74
75impl std::ops::Add for AggregateValue {
76    type Output = AggregateValue;
77    fn add(self, rhs: Self) -> Self::Output {
78        match (self, rhs) {
79            (AggregateValue::Null, _) | (_, AggregateValue::Null) => AggregateValue::Null,
80            (AggregateValue::Integer(a), AggregateValue::Integer(b)) => {
81                AggregateValue::Integer(a.saturating_add(b))
82            }
83            (AggregateValue::Integer(a), AggregateValue::Float(b)) => {
84                AggregateValue::Float(a as f64 + b)
85            }
86            (AggregateValue::Float(a), AggregateValue::Integer(b)) => {
87                AggregateValue::Float(a + b as f64)
88            }
89            (AggregateValue::Float(a), AggregateValue::Float(b)) => AggregateValue::Float(a + b),
90            _ => AggregateValue::Null,
91        }
92    }
93}
94
95impl std::ops::Sub for AggregateValue {
96    type Output = AggregateValue;
97    fn sub(self, rhs: Self) -> Self::Output {
98        match (self, rhs) {
99            (AggregateValue::Null, _) | (_, AggregateValue::Null) => AggregateValue::Null,
100            (AggregateValue::Integer(a), AggregateValue::Integer(b)) => {
101                AggregateValue::Integer(a.saturating_sub(b))
102            }
103            (AggregateValue::Integer(a), AggregateValue::Float(b)) => {
104                AggregateValue::Float(a as f64 - b)
105            }
106            (AggregateValue::Float(a), AggregateValue::Integer(b)) => {
107                AggregateValue::Float(a - b as f64)
108            }
109            (AggregateValue::Float(a), AggregateValue::Float(b)) => AggregateValue::Float(a - b),
110            _ => AggregateValue::Null,
111        }
112    }
113}
114
115impl std::ops::Mul for AggregateValue {
116    type Output = AggregateValue;
117    fn mul(self, rhs: Self) -> Self::Output {
118        match (self, rhs) {
119            (AggregateValue::Null, _) | (_, AggregateValue::Null) => AggregateValue::Null,
120            (AggregateValue::Integer(a), AggregateValue::Integer(b)) => {
121                AggregateValue::Integer(a.saturating_mul(b))
122            }
123            (AggregateValue::Integer(a), AggregateValue::Float(b)) => {
124                AggregateValue::Float(a as f64 * b)
125            }
126            (AggregateValue::Float(a), AggregateValue::Integer(b)) => {
127                AggregateValue::Float(a * b as f64)
128            }
129            (AggregateValue::Float(a), AggregateValue::Float(b)) => AggregateValue::Float(a * b),
130            _ => AggregateValue::Null,
131        }
132    }
133}
134
135/// GROUP BY key: ordered list of (variable_name, value) pairs.
136pub type GroupKey = Vec<(String, String)>;
137
138/// The result of an aggregate over one group.
139#[derive(Debug, Clone)]
140pub struct AggregateResult {
141    /// The values of the GROUP BY variables for this group.
142    pub group_key: GroupKey,
143    /// Map from output variable name to its aggregate value.
144    pub bindings: HashMap<String, AggregateValue>,
145}
146
147/// Executes SPARQL 1.1 aggregate operations.
148pub struct AggregateExecutor;
149
150impl AggregateExecutor {
151    /// Partition `rows` into groups based on the values of `group_vars`.
152    ///
153    /// Rows that do not bind a group variable get an empty string for that variable.
154    /// If `group_vars` is empty, all rows form a single group with an empty key.
155    pub fn group_by(
156        rows: &[HashMap<String, String>],
157        group_vars: &[String],
158    ) -> HashMap<GroupKey, Vec<HashMap<String, String>>> {
159        let mut groups: HashMap<GroupKey, Vec<HashMap<String, String>>> = HashMap::new();
160        for row in rows {
161            let key: GroupKey = group_vars
162                .iter()
163                .map(|v| {
164                    let val = row.get(v).cloned().unwrap_or_default();
165                    (v.clone(), val)
166                })
167                .collect();
168            groups.entry(key).or_default().push(row.clone());
169        }
170        // Ensure at least one group exists even when rows is empty and group_vars is empty
171        if rows.is_empty() && group_vars.is_empty() {
172            groups.entry(vec![]).or_default();
173        }
174        groups
175    }
176
177    /// Apply an aggregate function over a single group.
178    ///
179    /// `var` is the name of the variable being aggregated.
180    pub fn apply(
181        func: &AggregateFunc,
182        var: &str,
183        group: &[HashMap<String, String>],
184    ) -> AggregateValue {
185        match func {
186            AggregateFunc::CountAll => AggregateValue::Integer(group.len() as i64),
187
188            AggregateFunc::Count { distinct } => {
189                let values: Vec<&str> = group
190                    .iter()
191                    .filter_map(|row| row.get(var).map(|s| s.as_str()))
192                    .collect();
193                if *distinct {
194                    let mut seen = std::collections::HashSet::new();
195                    let count = values.into_iter().filter(|v| seen.insert(*v)).count();
196                    AggregateValue::Integer(count as i64)
197                } else {
198                    AggregateValue::Integer(values.len() as i64)
199                }
200            }
201
202            AggregateFunc::Sum => {
203                let nums: Vec<f64> = group
204                    .iter()
205                    .filter_map(|row| row.get(var).and_then(|s| s.parse::<f64>().ok()))
206                    .collect();
207                if nums.is_empty() {
208                    AggregateValue::Null
209                } else {
210                    let sum: f64 = nums.iter().sum();
211                    // If all values are integers, return integer
212                    if nums.iter().all(|n| n.fract() == 0.0) {
213                        AggregateValue::Integer(sum as i64)
214                    } else {
215                        AggregateValue::Float(sum)
216                    }
217                }
218            }
219
220            AggregateFunc::Avg => {
221                let nums: Vec<f64> = group
222                    .iter()
223                    .filter_map(|row| row.get(var).and_then(|s| s.parse::<f64>().ok()))
224                    .collect();
225                if nums.is_empty() {
226                    AggregateValue::Null
227                } else {
228                    let avg = nums.iter().sum::<f64>() / nums.len() as f64;
229                    AggregateValue::Float(avg)
230                }
231            }
232
233            AggregateFunc::Min => {
234                let mut min_val: Option<AggregateValue> = None;
235                for row in group {
236                    if let Some(s) = row.get(var) {
237                        let v = if let Ok(n) = s.parse::<f64>() {
238                            if n.fract() == 0.0 {
239                                AggregateValue::Integer(n as i64)
240                            } else {
241                                AggregateValue::Float(n)
242                            }
243                        } else {
244                            AggregateValue::Text(s.clone())
245                        };
246                        min_val = Some(match min_val {
247                            None => v,
248                            Some(cur) => {
249                                if v < cur {
250                                    v
251                                } else {
252                                    cur
253                                }
254                            }
255                        });
256                    }
257                }
258                min_val.unwrap_or(AggregateValue::Null)
259            }
260
261            AggregateFunc::Max => {
262                let mut max_val: Option<AggregateValue> = None;
263                for row in group {
264                    if let Some(s) = row.get(var) {
265                        let v = if let Ok(n) = s.parse::<f64>() {
266                            if n.fract() == 0.0 {
267                                AggregateValue::Integer(n as i64)
268                            } else {
269                                AggregateValue::Float(n)
270                            }
271                        } else {
272                            AggregateValue::Text(s.clone())
273                        };
274                        max_val = Some(match max_val {
275                            None => v,
276                            Some(cur) => {
277                                if v > cur {
278                                    v
279                                } else {
280                                    cur
281                                }
282                            }
283                        });
284                    }
285                }
286                max_val.unwrap_or(AggregateValue::Null)
287            }
288
289            AggregateFunc::Sample => {
290                for row in group {
291                    if let Some(s) = row.get(var) {
292                        return AggregateValue::Text(s.clone());
293                    }
294                }
295                AggregateValue::Null
296            }
297
298            AggregateFunc::GroupConcat { separator } => {
299                let parts: Vec<&str> = group
300                    .iter()
301                    .filter_map(|row| row.get(var).map(|s| s.as_str()))
302                    .collect();
303                if parts.is_empty() {
304                    AggregateValue::Null
305                } else {
306                    AggregateValue::Text(parts.join(separator.as_str()))
307                }
308            }
309        }
310    }
311
312    /// Execute aggregate functions over all groups, returning sorted results.
313    ///
314    /// `aggregates` is a list of `(input_var, func, output_var)` triples.
315    pub fn execute(
316        rows: &[HashMap<String, String>],
317        group_vars: &[String],
318        aggregates: &[(String, AggregateFunc, String)],
319    ) -> Vec<AggregateResult> {
320        let groups = Self::group_by(rows, group_vars);
321        let mut results: Vec<AggregateResult> = groups
322            .into_iter()
323            .map(|(key, group_rows)| {
324                let mut bindings = HashMap::new();
325                for (input_var, func, output_var) in aggregates {
326                    let value = Self::apply(func, input_var, &group_rows);
327                    bindings.insert(output_var.clone(), value);
328                }
329                AggregateResult {
330                    group_key: key,
331                    bindings,
332                }
333            })
334            .collect();
335
336        // Sort by group key for determinism
337        results.sort_by(|a, b| a.group_key.cmp(&b.group_key));
338        results
339    }
340
341    /// Filter aggregate results using a HAVING-like condition.
342    ///
343    /// Compares the `AggregateValue` bound to `var` against `value` using `op`.
344    /// Supported operators: `"="`, `"!="`, `"<"`, `">"`, `"<="`, `">="`.
345    pub fn having_filter(
346        results: &[AggregateResult],
347        var: &str,
348        op: &str,
349        value: &str,
350    ) -> Vec<AggregateResult> {
351        results
352            .iter()
353            .filter(|r| {
354                let bound = r.bindings.get(var);
355                Self::compare_value(bound, op, value)
356            })
357            .cloned()
358            .collect()
359    }
360
361    /// Compare an optional AggregateValue against a string threshold using the given operator.
362    fn compare_value(bound: Option<&AggregateValue>, op: &str, threshold: &str) -> bool {
363        let Some(av) = bound else {
364            return false;
365        };
366
367        // Try numeric comparison first
368        if let Ok(threshold_f) = threshold.parse::<f64>() {
369            let av_f = match av {
370                AggregateValue::Integer(n) => Some(*n as f64),
371                AggregateValue::Float(f) => Some(*f),
372                AggregateValue::Text(t) => t.parse::<f64>().ok(),
373                AggregateValue::Null => None,
374            };
375            if let Some(av_f) = av_f {
376                return match op {
377                    "=" => (av_f - threshold_f).abs() < f64::EPSILON,
378                    "!=" => (av_f - threshold_f).abs() >= f64::EPSILON,
379                    "<" => av_f < threshold_f,
380                    ">" => av_f > threshold_f,
381                    "<=" => av_f <= threshold_f,
382                    ">=" => av_f >= threshold_f,
383                    _ => false,
384                };
385            }
386        }
387
388        // String comparison
389        let av_s = match av {
390            AggregateValue::Text(t) => t.as_str(),
391            AggregateValue::Integer(n) => {
392                // Avoid allocation — convert to string inline, only for string comparison
393                return match op {
394                    "=" => n.to_string() == threshold,
395                    "!=" => n.to_string() != threshold,
396                    _ => false,
397                };
398            }
399            AggregateValue::Float(f) => {
400                return match op {
401                    "=" => f.to_string() == threshold,
402                    "!=" => f.to_string() != threshold,
403                    _ => false,
404                };
405            }
406            AggregateValue::Null => return false,
407        };
408
409        match op {
410            "=" => av_s == threshold,
411            "!=" => av_s != threshold,
412            "<" => av_s < threshold,
413            ">" => av_s > threshold,
414            "<=" => av_s <= threshold,
415            ">=" => av_s >= threshold,
416            _ => false,
417        }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    fn row(pairs: &[(&str, &str)]) -> HashMap<String, String> {
426        pairs
427            .iter()
428            .map(|(k, v)| (k.to_string(), v.to_string()))
429            .collect()
430    }
431
432    // ------ AggregateValue Display ------
433
434    #[test]
435    fn test_display_integer() {
436        assert_eq!(AggregateValue::Integer(42).to_string(), "42");
437    }
438
439    #[test]
440    fn test_display_float() {
441        assert_eq!(AggregateValue::Float(2.71).to_string(), "2.71");
442    }
443
444    #[test]
445    fn test_display_text() {
446        assert_eq!(
447            AggregateValue::Text("hello".to_string()).to_string(),
448            "hello"
449        );
450    }
451
452    #[test]
453    fn test_display_null() {
454        assert_eq!(AggregateValue::Null.to_string(), "NULL");
455    }
456
457    // ------ Ordering ------
458
459    #[test]
460    fn test_ordering_null_less_than_integer() {
461        assert!(AggregateValue::Null < AggregateValue::Integer(0));
462    }
463
464    #[test]
465    fn test_ordering_integer_less_than_float() {
466        assert!(AggregateValue::Integer(1) < AggregateValue::Float(1.5));
467    }
468
469    #[test]
470    fn test_ordering_float_less_than_text() {
471        assert!(AggregateValue::Float(99.9) < AggregateValue::Text("a".to_string()));
472    }
473
474    #[test]
475    fn test_ordering_integers() {
476        assert!(AggregateValue::Integer(1) < AggregateValue::Integer(2));
477        assert!(AggregateValue::Integer(2) > AggregateValue::Integer(1));
478    }
479
480    // ------ Arithmetic ------
481
482    #[test]
483    fn test_add_integers() {
484        let r = AggregateValue::Integer(3) + AggregateValue::Integer(4);
485        assert_eq!(r, AggregateValue::Integer(7));
486    }
487
488    #[test]
489    fn test_add_float_and_integer() {
490        let r = AggregateValue::Float(1.5) + AggregateValue::Integer(2);
491        assert_eq!(r, AggregateValue::Float(3.5));
492    }
493
494    #[test]
495    fn test_add_null_propagates() {
496        let r = AggregateValue::Null + AggregateValue::Integer(5);
497        assert_eq!(r, AggregateValue::Null);
498    }
499
500    #[test]
501    fn test_sub_integers() {
502        let r = AggregateValue::Integer(10) - AggregateValue::Integer(3);
503        assert_eq!(r, AggregateValue::Integer(7));
504    }
505
506    #[test]
507    fn test_mul_integers() {
508        let r = AggregateValue::Integer(4) * AggregateValue::Integer(5);
509        assert_eq!(r, AggregateValue::Integer(20));
510    }
511
512    // ------ group_by ------
513
514    #[test]
515    fn test_group_by_empty_rows() {
516        let groups = AggregateExecutor::group_by(&[], &["x".to_string()]);
517        assert!(groups.is_empty());
518    }
519
520    #[test]
521    fn test_group_by_no_group_vars_single_group() {
522        let rows = vec![row(&[("x", "1")]), row(&[("x", "2")])];
523        let groups = AggregateExecutor::group_by(&rows, &[]);
524        assert_eq!(groups.len(), 1);
525        let group = groups.get(&vec![]).expect("single empty-key group");
526        assert_eq!(group.len(), 2);
527    }
528
529    #[test]
530    fn test_group_by_single_var() {
531        let rows = vec![
532            row(&[("type", "a"), ("val", "1")]),
533            row(&[("type", "b"), ("val", "2")]),
534            row(&[("type", "a"), ("val", "3")]),
535        ];
536        let groups = AggregateExecutor::group_by(&rows, &["type".to_string()]);
537        assert_eq!(groups.len(), 2);
538        let a_key = vec![("type".to_string(), "a".to_string())];
539        assert_eq!(groups[&a_key].len(), 2);
540    }
541
542    #[test]
543    fn test_group_by_multiple_vars() {
544        let rows = vec![
545            row(&[("a", "1"), ("b", "x")]),
546            row(&[("a", "1"), ("b", "y")]),
547            row(&[("a", "2"), ("b", "x")]),
548        ];
549        let groups = AggregateExecutor::group_by(&rows, &["a".to_string(), "b".to_string()]);
550        assert_eq!(groups.len(), 3);
551    }
552
553    // ------ apply ------
554
555    #[test]
556    fn test_count_basic() {
557        let group = vec![row(&[("x", "a")]), row(&[("x", "b")]), row(&[])];
558        let r = AggregateExecutor::apply(&AggregateFunc::Count { distinct: false }, "x", &group);
559        assert_eq!(r, AggregateValue::Integer(2));
560    }
561
562    #[test]
563    fn test_count_distinct() {
564        let group = vec![row(&[("x", "a")]), row(&[("x", "a")]), row(&[("x", "b")])];
565        let r = AggregateExecutor::apply(&AggregateFunc::Count { distinct: true }, "x", &group);
566        assert_eq!(r, AggregateValue::Integer(2));
567    }
568
569    #[test]
570    fn test_count_all() {
571        let group = vec![row(&[("x", "a")]), row(&[])];
572        let r = AggregateExecutor::apply(&AggregateFunc::CountAll, "x", &group);
573        assert_eq!(r, AggregateValue::Integer(2));
574    }
575
576    #[test]
577    fn test_sum_integers() {
578        let group = vec![row(&[("n", "10")]), row(&[("n", "20")]), row(&[("n", "5")])];
579        let r = AggregateExecutor::apply(&AggregateFunc::Sum, "n", &group);
580        assert_eq!(r, AggregateValue::Integer(35));
581    }
582
583    #[test]
584    fn test_sum_floats() {
585        let group = vec![row(&[("n", "1.5")]), row(&[("n", "2.5")])];
586        let r = AggregateExecutor::apply(&AggregateFunc::Sum, "n", &group);
587        assert_eq!(r, AggregateValue::Float(4.0));
588    }
589
590    #[test]
591    fn test_sum_empty() {
592        let group: Vec<HashMap<String, String>> = vec![];
593        let r = AggregateExecutor::apply(&AggregateFunc::Sum, "n", &group);
594        assert_eq!(r, AggregateValue::Null);
595    }
596
597    #[test]
598    fn test_avg_basic() {
599        let group = vec![row(&[("n", "10")]), row(&[("n", "20")])];
600        let r = AggregateExecutor::apply(&AggregateFunc::Avg, "n", &group);
601        assert_eq!(r, AggregateValue::Float(15.0));
602    }
603
604    #[test]
605    fn test_avg_empty() {
606        let group: Vec<HashMap<String, String>> = vec![];
607        let r = AggregateExecutor::apply(&AggregateFunc::Avg, "n", &group);
608        assert_eq!(r, AggregateValue::Null);
609    }
610
611    #[test]
612    fn test_min_numeric() {
613        let group = vec![row(&[("n", "5")]), row(&[("n", "2")]), row(&[("n", "8")])];
614        let r = AggregateExecutor::apply(&AggregateFunc::Min, "n", &group);
615        assert_eq!(r, AggregateValue::Integer(2));
616    }
617
618    #[test]
619    fn test_max_numeric() {
620        let group = vec![row(&[("n", "5")]), row(&[("n", "2")]), row(&[("n", "8")])];
621        let r = AggregateExecutor::apply(&AggregateFunc::Max, "n", &group);
622        assert_eq!(r, AggregateValue::Integer(8));
623    }
624
625    #[test]
626    fn test_min_empty() {
627        let group: Vec<HashMap<String, String>> = vec![];
628        let r = AggregateExecutor::apply(&AggregateFunc::Min, "n", &group);
629        assert_eq!(r, AggregateValue::Null);
630    }
631
632    #[test]
633    fn test_min_text() {
634        let group = vec![row(&[("s", "banana")]), row(&[("s", "apple")])];
635        let r = AggregateExecutor::apply(&AggregateFunc::Min, "s", &group);
636        assert_eq!(r, AggregateValue::Text("apple".to_string()));
637    }
638
639    #[test]
640    fn test_max_text() {
641        let group = vec![row(&[("s", "banana")]), row(&[("s", "apple")])];
642        let r = AggregateExecutor::apply(&AggregateFunc::Max, "s", &group);
643        assert_eq!(r, AggregateValue::Text("banana".to_string()));
644    }
645
646    #[test]
647    fn test_sample_returns_first_non_null() {
648        let group = vec![row(&[]), row(&[("x", "second")]), row(&[("x", "third")])];
649        let r = AggregateExecutor::apply(&AggregateFunc::Sample, "x", &group);
650        assert_eq!(r, AggregateValue::Text("second".to_string()));
651    }
652
653    #[test]
654    fn test_sample_empty() {
655        let group: Vec<HashMap<String, String>> = vec![];
656        let r = AggregateExecutor::apply(&AggregateFunc::Sample, "x", &group);
657        assert_eq!(r, AggregateValue::Null);
658    }
659
660    #[test]
661    fn test_group_concat_default_separator() {
662        let group = vec![row(&[("x", "a")]), row(&[("x", "b")]), row(&[("x", "c")])];
663        let r = AggregateExecutor::apply(
664            &AggregateFunc::GroupConcat {
665                separator: " ".to_string(),
666            },
667            "x",
668            &group,
669        );
670        assert_eq!(r, AggregateValue::Text("a b c".to_string()));
671    }
672
673    #[test]
674    fn test_group_concat_custom_separator() {
675        let group = vec![row(&[("x", "a")]), row(&[("x", "b")])];
676        let r = AggregateExecutor::apply(
677            &AggregateFunc::GroupConcat {
678                separator: ",".to_string(),
679            },
680            "x",
681            &group,
682        );
683        assert_eq!(r, AggregateValue::Text("a,b".to_string()));
684    }
685
686    #[test]
687    fn test_group_concat_empty() {
688        let group: Vec<HashMap<String, String>> = vec![];
689        let r = AggregateExecutor::apply(
690            &AggregateFunc::GroupConcat {
691                separator: ",".to_string(),
692            },
693            "x",
694            &group,
695        );
696        assert_eq!(r, AggregateValue::Null);
697    }
698
699    // ------ execute ------
700
701    #[test]
702    fn test_execute_grouped_count() {
703        let rows = vec![
704            row(&[("type", "a"), ("val", "1")]),
705            row(&[("type", "a"), ("val", "2")]),
706            row(&[("type", "b"), ("val", "3")]),
707        ];
708        let aggs = vec![(
709            "val".to_string(),
710            AggregateFunc::Count { distinct: false },
711            "cnt".to_string(),
712        )];
713        let results = AggregateExecutor::execute(&rows, &["type".to_string()], &aggs);
714        assert_eq!(results.len(), 2);
715        // sorted: a before b
716        assert_eq!(
717            results[0].bindings.get("cnt"),
718            Some(&AggregateValue::Integer(2))
719        );
720        assert_eq!(
721            results[1].bindings.get("cnt"),
722            Some(&AggregateValue::Integer(1))
723        );
724    }
725
726    #[test]
727    fn test_execute_no_group_vars() {
728        let rows = vec![row(&[("n", "10")]), row(&[("n", "20")])];
729        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "total".to_string())];
730        let results = AggregateExecutor::execute(&rows, &[], &aggs);
731        assert_eq!(results.len(), 1);
732        assert_eq!(
733            results[0].bindings.get("total"),
734            Some(&AggregateValue::Integer(30))
735        );
736    }
737
738    #[test]
739    fn test_execute_multiple_aggregates() {
740        let rows = vec![
741            row(&[("g", "x"), ("n", "10")]),
742            row(&[("g", "x"), ("n", "20")]),
743        ];
744        let aggs = vec![
745            ("n".to_string(), AggregateFunc::Min, "mn".to_string()),
746            ("n".to_string(), AggregateFunc::Max, "mx".to_string()),
747            ("n".to_string(), AggregateFunc::Avg, "av".to_string()),
748        ];
749        let results = AggregateExecutor::execute(&rows, &["g".to_string()], &aggs);
750        assert_eq!(results.len(), 1);
751        assert_eq!(
752            results[0].bindings.get("mn"),
753            Some(&AggregateValue::Integer(10))
754        );
755        assert_eq!(
756            results[0].bindings.get("mx"),
757            Some(&AggregateValue::Integer(20))
758        );
759        assert_eq!(
760            results[0].bindings.get("av"),
761            Some(&AggregateValue::Float(15.0))
762        );
763    }
764
765    #[test]
766    fn test_execute_sorted_deterministic() {
767        let rows = vec![
768            row(&[("g", "c"), ("n", "1")]),
769            row(&[("g", "a"), ("n", "2")]),
770            row(&[("g", "b"), ("n", "3")]),
771        ];
772        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
773        let results = AggregateExecutor::execute(&rows, &["g".to_string()], &aggs);
774        assert_eq!(results[0].group_key[0].1, "a");
775        assert_eq!(results[1].group_key[0].1, "b");
776        assert_eq!(results[2].group_key[0].1, "c");
777    }
778
779    // ------ having_filter ------
780
781    #[test]
782    fn test_having_eq() {
783        let rows = vec![row(&[("n", "1")]), row(&[("n", "2")]), row(&[("n", "2")])];
784        let aggs = vec![(
785            "n".to_string(),
786            AggregateFunc::Count { distinct: false },
787            "cnt".to_string(),
788        )];
789        let results = AggregateExecutor::execute(&rows, &["n".to_string()], &aggs);
790        let filtered = AggregateExecutor::having_filter(&results, "cnt", "=", "2");
791        assert_eq!(filtered.len(), 1);
792    }
793
794    #[test]
795    fn test_having_neq() {
796        let rows = vec![row(&[("n", "1")]), row(&[("n", "2")]), row(&[("n", "2")])];
797        let aggs = vec![(
798            "n".to_string(),
799            AggregateFunc::Count { distinct: false },
800            "cnt".to_string(),
801        )];
802        let results = AggregateExecutor::execute(&rows, &["n".to_string()], &aggs);
803        let filtered = AggregateExecutor::having_filter(&results, "cnt", "!=", "1");
804        assert_eq!(filtered.len(), 1);
805    }
806
807    #[test]
808    fn test_having_gt() {
809        let rows = vec![
810            row(&[("g", "a"), ("n", "10")]),
811            row(&[("g", "a"), ("n", "20")]),
812            row(&[("g", "b"), ("n", "5")]),
813        ];
814        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
815        let results = AggregateExecutor::execute(&rows, &["g".to_string()], &aggs);
816        let filtered = AggregateExecutor::having_filter(&results, "s", ">", "10");
817        assert_eq!(filtered.len(), 1);
818        assert_eq!(filtered[0].group_key[0].1, "a");
819    }
820
821    #[test]
822    fn test_having_lt() {
823        let rows = vec![
824            row(&[("g", "a"), ("n", "10")]),
825            row(&[("g", "b"), ("n", "5")]),
826        ];
827        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
828        let results = AggregateExecutor::execute(&rows, &["g".to_string()], &aggs);
829        let filtered = AggregateExecutor::having_filter(&results, "s", "<", "8");
830        assert_eq!(filtered.len(), 1);
831        assert_eq!(filtered[0].group_key[0].1, "b");
832    }
833
834    #[test]
835    fn test_having_lte() {
836        let rows = vec![
837            row(&[("g", "a"), ("n", "10")]),
838            row(&[("g", "b"), ("n", "5")]),
839        ];
840        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
841        let results = AggregateExecutor::execute(&rows, &["g".to_string()], &aggs);
842        let filtered = AggregateExecutor::having_filter(&results, "s", "<=", "10");
843        assert_eq!(filtered.len(), 2);
844    }
845
846    #[test]
847    fn test_having_gte() {
848        let rows = vec![
849            row(&[("g", "a"), ("n", "10")]),
850            row(&[("g", "b"), ("n", "5")]),
851        ];
852        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
853        let results = AggregateExecutor::execute(&rows, &["g".to_string()], &aggs);
854        let filtered = AggregateExecutor::having_filter(&results, "s", ">=", "10");
855        assert_eq!(filtered.len(), 1);
856        assert_eq!(filtered[0].group_key[0].1, "a");
857    }
858
859    #[test]
860    fn test_having_no_match() {
861        let rows = vec![row(&[("n", "5")])];
862        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
863        let results = AggregateExecutor::execute(&rows, &[], &aggs);
864        let filtered = AggregateExecutor::having_filter(&results, "s", ">", "100");
865        assert!(filtered.is_empty());
866    }
867
868    #[test]
869    fn test_execute_empty_rows_no_group_vars() {
870        let rows: Vec<HashMap<String, String>> = vec![];
871        let aggs = vec![("n".to_string(), AggregateFunc::Sum, "s".to_string())];
872        let results = AggregateExecutor::execute(&rows, &[], &aggs);
873        assert_eq!(results.len(), 1);
874        assert_eq!(results[0].bindings.get("s"), Some(&AggregateValue::Null));
875    }
876
877    #[test]
878    fn test_count_distinct_all_unique() {
879        let group = vec![row(&[("x", "a")]), row(&[("x", "b")]), row(&[("x", "c")])];
880        let r = AggregateExecutor::apply(&AggregateFunc::Count { distinct: true }, "x", &group);
881        assert_eq!(r, AggregateValue::Integer(3));
882    }
883
884    #[test]
885    fn test_group_concat_skips_nulls() {
886        let group = vec![row(&[("x", "a")]), row(&[]), row(&[("x", "b")])];
887        let r = AggregateExecutor::apply(
888            &AggregateFunc::GroupConcat {
889                separator: "-".to_string(),
890            },
891            "x",
892            &group,
893        );
894        assert_eq!(r, AggregateValue::Text("a-b".to_string()));
895    }
896}