1use crate::error::OxirsError;
11use crate::model::{Literal, Term};
12use crate::rdf_store::VariableBinding;
13use crate::sparql::modifiers::compare_terms;
14use crate::Result;
15use ahash::{AHashMap, AHashSet};
16use std::collections::hash_map::Entry;
17
18#[cfg(feature = "parallel")]
19use rayon::prelude::*;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum AggregateFunction {
24 Count,
25 Sum,
26 Avg,
27 Min,
28 Max,
29 GroupConcat {
30 separator: String,
31 },
32 Sample,
33 Median,
35 Variance,
36 StdDev,
37 Percentile {
38 percentile: u8,
39 }, }
41
42#[derive(Debug, Clone)]
44pub struct AggregateExpression {
45 pub function: AggregateFunction,
46 pub variable: Option<String>, pub alias: String,
48 pub distinct: bool, }
50
51#[derive(Debug, Clone)]
53pub struct GroupBySpec {
54 pub variables: Vec<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59struct GroupKey(Vec<TermHash>);
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
63enum TermHash {
64 NamedNode(String),
65 BlankNode(String),
66 Literal {
67 value: String,
68 datatype: Option<String>,
69 language: Option<String>,
70 },
71 Unbound,
72}
73
74impl From<&Term> for TermHash {
75 fn from(term: &Term) -> Self {
76 match term {
77 Term::NamedNode(n) => TermHash::NamedNode(n.as_str().to_string()),
78 Term::BlankNode(b) => TermHash::BlankNode(b.as_str().to_string()),
79 Term::Literal(l) => TermHash::Literal {
80 value: l.value().to_string(),
81 datatype: Some(l.datatype().as_str().to_string()),
82 language: l.language().map(|lang| lang.to_string()),
83 },
84 Term::Variable(v) => TermHash::NamedNode(format!("?{}", v.as_str())),
85 Term::QuotedTriple(qt) => TermHash::NamedNode(format!("<<{}>>", qt)),
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92struct AggregateAccumulator {
93 function: AggregateFunction,
94 count: usize,
95 sum: f64,
96 values: Vec<Term>,
97 seen_values: AHashSet<TermHash>, min_value: Option<Term>,
99 max_value: Option<Term>,
100 concat_values: Vec<String>, sample_value: Option<Term>, distinct: bool,
103}
104
105impl AggregateAccumulator {
106 fn new(function: AggregateFunction, distinct: bool) -> Self {
108 Self {
109 function,
110 count: 0,
111 sum: 0.0,
112 values: Vec::new(),
113 seen_values: AHashSet::new(),
114 min_value: None,
115 max_value: None,
116 concat_values: Vec::new(),
117 sample_value: None,
118 distinct,
119 }
120 }
121
122 fn add_value(&mut self, term: Option<&Term>) {
124 let Some(term) = term else {
125 return;
126 };
127
128 if self.distinct {
130 let term_hash = TermHash::from(term);
131 if !self.seen_values.insert(term_hash) {
132 return; }
134 }
135
136 self.count += 1;
137
138 match &self.function {
139 AggregateFunction::Count => {
140 }
142 AggregateFunction::Sum | AggregateFunction::Avg => {
143 if let Term::Literal(lit) = term {
144 if let Ok(val) = lit.value().parse::<f64>() {
145 self.sum += val;
146 if matches!(self.function, AggregateFunction::Avg) {
147 self.values.push(term.clone());
148 }
149 }
150 }
151 }
152 AggregateFunction::Min => {
153 if let Some(ref current_min) = self.min_value {
154 if compare_terms(term, current_min).is_lt() {
155 self.min_value = Some(term.clone());
156 }
157 } else {
158 self.min_value = Some(term.clone());
159 }
160 }
161 AggregateFunction::Max => {
162 if let Some(ref current_max) = self.max_value {
163 if compare_terms(term, current_max).is_gt() {
164 self.max_value = Some(term.clone());
165 }
166 } else {
167 self.max_value = Some(term.clone());
168 }
169 }
170 AggregateFunction::GroupConcat { .. } => {
171 if let Term::Literal(lit) = term {
172 self.concat_values.push(lit.value().to_string());
173 } else {
174 self.concat_values.push(term.to_string());
175 }
176 }
177 AggregateFunction::Sample => {
178 if self.sample_value.is_none() {
179 self.sample_value = Some(term.clone());
180 }
181 }
182 AggregateFunction::Median
184 | AggregateFunction::Variance
185 | AggregateFunction::StdDev
186 | AggregateFunction::Percentile { .. } => {
187 if let Term::Literal(lit) = term {
188 if lit.value().parse::<f64>().is_ok() {
189 self.values.push(term.clone());
190 }
191 }
192 }
193 }
194 }
195
196 fn finalize(&self) -> Term {
198 match &self.function {
199 AggregateFunction::Count => Term::from(Literal::new(self.count.to_string())),
200 AggregateFunction::Sum => Term::from(Literal::new(self.sum.to_string())),
201 AggregateFunction::Avg => {
202 let avg = if self.count > 0 {
203 self.sum / self.count as f64
204 } else {
205 0.0
206 };
207 Term::from(Literal::new(avg.to_string()))
208 }
209 AggregateFunction::Min => self
210 .min_value
211 .clone()
212 .unwrap_or_else(|| Term::from(Literal::new(""))),
213 AggregateFunction::Max => self
214 .max_value
215 .clone()
216 .unwrap_or_else(|| Term::from(Literal::new(""))),
217 AggregateFunction::GroupConcat { separator } => {
218 let concatenated = self.concat_values.join(separator);
219 Term::from(Literal::new(concatenated))
220 }
221 AggregateFunction::Sample => self
222 .sample_value
223 .clone()
224 .unwrap_or_else(|| Term::from(Literal::new(""))),
225 AggregateFunction::Median => {
227 let result = compute_median(&self.values);
228 Term::from(Literal::new(result.to_string()))
229 }
230 AggregateFunction::Variance => {
231 let result = compute_variance(&self.values);
232 Term::from(Literal::new(result.to_string()))
233 }
234 AggregateFunction::StdDev => {
235 let variance = compute_variance(&self.values);
236 let stddev = variance.sqrt();
237 Term::from(Literal::new(stddev.to_string()))
238 }
239 AggregateFunction::Percentile { percentile } => {
240 let result = compute_percentile(&self.values, *percentile);
241 Term::from(Literal::new(result.to_string()))
242 }
243 }
244 }
245}
246
247pub fn extract_aggregates(sparql: &str) -> Result<Vec<AggregateExpression>> {
249 let mut aggregates = Vec::new();
250
251 if let Some(select_start) = super::query_locator::find_keyword(sparql, "SELECT") {
252 if let Some(clause_end) = super::query_locator::select_projection_end(sparql, select_start)
255 {
256 let select_clause = &sparql[select_start + 6..clause_end];
257
258 let mut pos = 0;
260 while pos < select_clause.len() {
261 if let Some(paren_start) = select_clause[pos..].find('(') {
262 let abs_pos = pos + paren_start;
263
264 if let Some(paren_end) = find_matching_paren(&select_clause[abs_pos..]) {
266 let expr = &select_clause[abs_pos..abs_pos + paren_end + 1];
267
268 let expr_upper = expr.to_uppercase();
270 let function = if expr_upper.starts_with("(COUNT") {
271 Some(AggregateFunction::Count)
272 } else if expr_upper.starts_with("(SUM") {
273 Some(AggregateFunction::Sum)
274 } else if expr_upper.starts_with("(AVG") {
275 Some(AggregateFunction::Avg)
276 } else if expr_upper.starts_with("(MIN") {
277 Some(AggregateFunction::Min)
278 } else if expr_upper.starts_with("(MAX") {
279 Some(AggregateFunction::Max)
280 } else {
281 None
282 };
283
284 if let Some(func) = function {
285 let inner = &expr[1..expr.len() - 1]; let func_name_end = if let Some(inner_paren) = inner.find('(') {
290 inner_paren
291 } else {
292 continue;
293 };
294
295 let after_func = &inner[func_name_end..];
297 let after_func_upper = after_func.to_uppercase();
298 let (var_part, alias_part) =
299 if let Some(as_pos) = after_func_upper.find(" AS ") {
300 (&after_func[1..as_pos], &after_func[as_pos + 4..])
301 } else {
302 (&after_func[1..], "")
303 };
304
305 let args_trimmed = var_part.trim_end_matches(')').trim();
306
307 let variable = if args_trimmed == "*" {
309 None
310 } else if let Some(var_name) = args_trimmed.strip_prefix('?') {
311 Some(var_name.to_string())
312 } else {
313 Some(args_trimmed.to_string())
314 };
315
316 let mut alias = String::from("aggregate");
318 if !alias_part.is_empty() {
319 for token in alias_part.split_whitespace() {
320 if let Some(var_name) = token.strip_prefix('?') {
321 alias = var_name.trim_end_matches(')').to_string();
322 break;
323 }
324 }
325 }
326
327 let distinct = expr_upper.contains("DISTINCT");
329
330 aggregates.push(AggregateExpression {
331 function: func,
332 variable,
333 alias,
334 distinct,
335 });
336 }
337
338 pos = abs_pos + paren_end + 1;
339 } else {
340 break;
341 }
342 } else {
343 break;
344 }
345 }
346 }
347 }
348
349 Ok(aggregates)
350}
351
352pub fn find_matching_paren(text: &str) -> Option<usize> {
354 let mut paren_count = 1;
355 let chars: Vec<char> = text.chars().collect();
356
357 for (i, &ch) in chars.iter().enumerate().skip(1) {
358 if ch == '(' {
359 paren_count += 1;
360 } else if ch == ')' {
361 paren_count -= 1;
362 if paren_count == 0 {
363 return Some(i);
364 }
365 }
366 }
367
368 None
369}
370
371pub fn apply_aggregates(
379 results: Vec<VariableBinding>,
380 aggregates: &[AggregateExpression],
381) -> Result<(Vec<VariableBinding>, Vec<String>)> {
382 if aggregates.is_empty() {
383 return Err(OxirsError::Query("No aggregates to apply".to_string()));
384 }
385
386 apply_aggregates_no_grouping(results, aggregates)
388}
389
390pub fn apply_aggregates_with_grouping(
394 results: Vec<VariableBinding>,
395 aggregates: &[AggregateExpression],
396 group_by: &GroupBySpec,
397) -> Result<(Vec<VariableBinding>, Vec<String>)> {
398 if aggregates.is_empty() {
399 return Err(OxirsError::Query("No aggregates to apply".to_string()));
400 }
401
402 let mut groups: AHashMap<GroupKey, Vec<VariableBinding>> = AHashMap::new();
404
405 for binding in results {
407 let key = extract_group_key(&binding, &group_by.variables);
408 match groups.entry(key) {
409 Entry::Occupied(mut entry) => {
410 entry.get_mut().push(binding);
411 }
412 Entry::Vacant(entry) => {
413 entry.insert(vec![binding]);
414 }
415 }
416 }
417
418 #[cfg(feature = "parallel")]
420 let group_results: Vec<_> = {
421 let groups_vec: Vec<_> = groups.into_iter().collect();
422 if groups_vec.len() > 10 {
423 groups_vec
425 .into_par_iter()
426 .map(|(key, group_bindings)| {
427 process_group(key, group_bindings, aggregates, &group_by.variables)
428 })
429 .collect::<Result<Vec<_>>>()?
430 } else {
431 groups_vec
432 .into_iter()
433 .map(|(key, group_bindings)| {
434 process_group(key, group_bindings, aggregates, &group_by.variables)
435 })
436 .collect::<Result<Vec<_>>>()?
437 }
438 };
439
440 #[cfg(not(feature = "parallel"))]
441 let group_results: Vec<_> = groups
442 .into_iter()
443 .map(|(key, group_bindings)| {
444 process_group(key, group_bindings, aggregates, &group_by.variables)
445 })
446 .collect::<Result<Vec<_>>>()?;
447
448 let mut result_variables = group_by.variables.clone();
450 for agg_expr in aggregates {
451 result_variables.push(agg_expr.alias.clone());
452 }
453
454 Ok((group_results, result_variables))
455}
456
457fn apply_aggregates_no_grouping(
459 results: Vec<VariableBinding>,
460 aggregates: &[AggregateExpression],
461) -> Result<(Vec<VariableBinding>, Vec<String>)> {
462 let mut result_variables = Vec::new();
463 let mut aggregate_binding = VariableBinding::new();
464
465 let mut accumulators: Vec<AggregateAccumulator> = aggregates
467 .iter()
468 .map(|agg| AggregateAccumulator::new(agg.function.clone(), agg.distinct))
469 .collect();
470
471 for binding in &results {
473 for (acc, agg_expr) in accumulators.iter_mut().zip(aggregates.iter()) {
474 let value = if let Some(var) = &agg_expr.variable {
475 binding.get(var)
476 } else {
477 Some(&Term::from(Literal::new("1")))
479 };
480 acc.add_value(value);
481 }
482 }
483
484 for (acc, agg_expr) in accumulators.iter().zip(aggregates.iter()) {
486 let value = acc.finalize();
487 aggregate_binding.bind(agg_expr.alias.clone(), value);
488 result_variables.push(agg_expr.alias.clone());
489 }
490
491 Ok((vec![aggregate_binding], result_variables))
492}
493
494fn extract_group_key(binding: &VariableBinding, group_vars: &[String]) -> GroupKey {
496 let key_terms: Vec<TermHash> = group_vars
497 .iter()
498 .map(|var| {
499 binding
500 .get(var)
501 .map(TermHash::from)
502 .unwrap_or(TermHash::Unbound)
503 })
504 .collect();
505 GroupKey(key_terms)
506}
507
508fn process_group(
510 _key: GroupKey,
511 group_bindings: Vec<VariableBinding>,
512 aggregates: &[AggregateExpression],
513 group_vars: &[String],
514) -> Result<VariableBinding> {
515 let mut result_binding = VariableBinding::new();
516
517 if let Some(first_binding) = group_bindings.first() {
519 for var in group_vars {
520 if let Some(value) = first_binding.get(var) {
521 result_binding.bind(var.clone(), value.clone());
522 }
523 }
524 }
525
526 let mut accumulators: Vec<AggregateAccumulator> = aggregates
528 .iter()
529 .map(|agg| AggregateAccumulator::new(agg.function.clone(), agg.distinct))
530 .collect();
531
532 for binding in &group_bindings {
534 for (acc, agg_expr) in accumulators.iter_mut().zip(aggregates.iter()) {
535 let value = if let Some(var) = &agg_expr.variable {
536 binding.get(var)
537 } else {
538 Some(&Term::from(Literal::new("1")))
540 };
541 acc.add_value(value);
542 }
543 }
544
545 for (acc, agg_expr) in accumulators.iter().zip(aggregates.iter()) {
547 let value = acc.finalize();
548 result_binding.bind(agg_expr.alias.clone(), value);
549 }
550
551 Ok(result_binding)
552}
553
554fn compute_median(values: &[Term]) -> f64 {
558 if values.is_empty() {
559 return 0.0;
560 }
561
562 let mut nums: Vec<f64> = values
563 .iter()
564 .filter_map(|term| {
565 if let Term::Literal(lit) = term {
566 lit.value().parse::<f64>().ok()
567 } else {
568 None
569 }
570 })
571 .collect();
572
573 if nums.is_empty() {
574 return 0.0;
575 }
576
577 nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
578
579 let len = nums.len();
580 if len % 2 == 0 {
581 (nums[len / 2 - 1] + nums[len / 2]) / 2.0
583 } else {
584 nums[len / 2]
586 }
587}
588
589fn compute_variance(values: &[Term]) -> f64 {
592 if values.len() < 2 {
593 return 0.0;
594 }
595
596 let nums: Vec<f64> = values
597 .iter()
598 .filter_map(|term| {
599 if let Term::Literal(lit) = term {
600 lit.value().parse::<f64>().ok()
601 } else {
602 None
603 }
604 })
605 .collect();
606
607 if nums.len() < 2 {
608 return 0.0;
609 }
610
611 let mean = nums.iter().sum::<f64>() / nums.len() as f64;
613
614 let squared_diffs: f64 = nums.iter().map(|x| (x - mean).powi(2)).sum();
616
617 squared_diffs / (nums.len() - 1) as f64
619}
620
621fn compute_percentile(values: &[Term], percentile: u8) -> f64 {
625 if values.is_empty() || percentile > 100 {
626 return 0.0;
627 }
628
629 let mut nums: Vec<f64> = values
630 .iter()
631 .filter_map(|term| {
632 if let Term::Literal(lit) = term {
633 lit.value().parse::<f64>().ok()
634 } else {
635 None
636 }
637 })
638 .collect();
639
640 if nums.is_empty() {
641 return 0.0;
642 }
643
644 nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
645
646 if percentile == 0 {
647 return nums[0];
648 }
649 if percentile == 100 {
650 return nums[nums.len() - 1];
651 }
652
653 let rank = (percentile as f64 / 100.0) * (nums.len() - 1) as f64;
655 let lower_index = rank.floor() as usize;
656 let upper_index = rank.ceil() as usize;
657
658 if lower_index == upper_index {
659 nums[lower_index]
660 } else {
661 let lower_value = nums[lower_index];
663 let upper_value = nums[upper_index];
664 let fraction = rank - lower_index as f64;
665 lower_value + fraction * (upper_value - lower_value)
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 fn create_test_binding(values: Vec<(&str, f64)>) -> VariableBinding {
674 let mut binding = VariableBinding::new();
675 for (var, val) in values {
676 binding.bind(var.to_string(), Term::from(Literal::new(val.to_string())));
677 }
678 binding
679 }
680
681 #[test]
682 fn test_count_aggregate() {
683 let results = vec![
684 create_test_binding(vec![("x", 1.0)]),
685 create_test_binding(vec![("x", 2.0)]),
686 create_test_binding(vec![("x", 3.0)]),
687 ];
688
689 let agg = AggregateExpression {
690 function: AggregateFunction::Count,
691 variable: Some("x".to_string()),
692 alias: "count".to_string(),
693 distinct: false,
694 };
695
696 let (result, vars) =
697 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
698 assert_eq!(result.len(), 1);
699 assert_eq!(vars, vec!["count"]);
700
701 if let Term::Literal(lit) = result[0].get("count").expect("binding should exist") {
702 assert_eq!(lit.value(), "3");
703 } else {
704 panic!("Expected literal");
705 }
706 }
707
708 #[test]
709 fn test_sum_aggregate() {
710 let results = vec![
711 create_test_binding(vec![("x", 10.0)]),
712 create_test_binding(vec![("x", 20.0)]),
713 create_test_binding(vec![("x", 30.0)]),
714 ];
715
716 let agg = AggregateExpression {
717 function: AggregateFunction::Sum,
718 variable: Some("x".to_string()),
719 alias: "sum".to_string(),
720 distinct: false,
721 };
722
723 let (result, _) =
724 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
725
726 if let Term::Literal(lit) = result[0].get("sum").expect("binding should exist") {
727 let sum: f64 = lit.value().parse().expect("parse should succeed");
728 assert!((sum - 60.0).abs() < 0.0001);
729 } else {
730 panic!("Expected literal");
731 }
732 }
733
734 #[test]
735 fn test_avg_aggregate() {
736 let results = vec![
737 create_test_binding(vec![("x", 10.0)]),
738 create_test_binding(vec![("x", 20.0)]),
739 create_test_binding(vec![("x", 30.0)]),
740 ];
741
742 let agg = AggregateExpression {
743 function: AggregateFunction::Avg,
744 variable: Some("x".to_string()),
745 alias: "avg".to_string(),
746 distinct: false,
747 };
748
749 let (result, _) =
750 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
751
752 if let Term::Literal(lit) = result[0].get("avg").expect("binding should exist") {
753 let avg: f64 = lit.value().parse().expect("parse should succeed");
754 assert!((avg - 20.0).abs() < 0.0001);
755 } else {
756 panic!("Expected literal");
757 }
758 }
759
760 #[test]
761 fn test_count_distinct() {
762 let results = vec![
763 create_test_binding(vec![("x", 1.0)]),
764 create_test_binding(vec![("x", 2.0)]),
765 create_test_binding(vec![("x", 1.0)]), create_test_binding(vec![("x", 3.0)]),
767 ];
768
769 let agg = AggregateExpression {
770 function: AggregateFunction::Count,
771 variable: Some("x".to_string()),
772 alias: "count".to_string(),
773 distinct: true, };
775
776 let (result, _) =
777 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
778
779 if let Term::Literal(lit) = result[0].get("count").expect("binding should exist") {
780 assert_eq!(lit.value(), "3"); } else {
782 panic!("Expected literal");
783 }
784 }
785
786 #[test]
787 fn test_group_concat() {
788 let mut binding1 = VariableBinding::new();
789 binding1.bind("x".to_string(), Term::from(Literal::new("apple")));
790 let mut binding2 = VariableBinding::new();
791 binding2.bind("x".to_string(), Term::from(Literal::new("banana")));
792 let mut binding3 = VariableBinding::new();
793 binding3.bind("x".to_string(), Term::from(Literal::new("cherry")));
794
795 let results = vec![binding1, binding2, binding3];
796
797 let agg = AggregateExpression {
798 function: AggregateFunction::GroupConcat {
799 separator: ", ".to_string(),
800 },
801 variable: Some("x".to_string()),
802 alias: "concat".to_string(),
803 distinct: false,
804 };
805
806 let (result, _) =
807 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
808
809 if let Term::Literal(lit) = result[0].get("concat").expect("binding should exist") {
810 assert_eq!(lit.value(), "apple, banana, cherry");
811 } else {
812 panic!("Expected literal");
813 }
814 }
815
816 #[test]
817 fn test_sample_aggregate() {
818 let results = vec![
819 create_test_binding(vec![("x", 10.0)]),
820 create_test_binding(vec![("x", 20.0)]),
821 create_test_binding(vec![("x", 30.0)]),
822 ];
823
824 let agg = AggregateExpression {
825 function: AggregateFunction::Sample,
826 variable: Some("x".to_string()),
827 alias: "sample".to_string(),
828 distinct: false,
829 };
830
831 let (result, _) =
832 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
833
834 assert!(result[0].get("sample").is_some());
836 }
837
838 #[test]
839 fn test_group_by_hash_based() {
840 let mut binding1 = VariableBinding::new();
842 binding1.bind("category".to_string(), Term::from(Literal::new("A")));
843 binding1.bind("value".to_string(), Term::from(Literal::new("10")));
844
845 let mut binding2 = VariableBinding::new();
846 binding2.bind("category".to_string(), Term::from(Literal::new("A")));
847 binding2.bind("value".to_string(), Term::from(Literal::new("20")));
848
849 let mut binding3 = VariableBinding::new();
850 binding3.bind("category".to_string(), Term::from(Literal::new("B")));
851 binding3.bind("value".to_string(), Term::from(Literal::new("30")));
852
853 let results = vec![binding1, binding2, binding3];
854
855 let agg = AggregateExpression {
856 function: AggregateFunction::Sum,
857 variable: Some("value".to_string()),
858 alias: "total".to_string(),
859 distinct: false,
860 };
861
862 let group_by = GroupBySpec {
863 variables: vec!["category".to_string()],
864 };
865
866 let (result, vars) = apply_aggregates_with_grouping(results, &[agg], &group_by)
867 .expect("aggregate operation should succeed");
868
869 assert_eq!(result.len(), 2);
871 assert_eq!(vars, vec!["category", "total"]);
872
873 for binding in &result {
875 if let Term::Literal(cat) = binding.get("category").expect("binding should exist") {
876 if let Term::Literal(total) = binding.get("total").expect("binding should exist") {
877 let total_val: f64 = total.value().parse().expect("parse should succeed");
878 if cat.value() == "A" {
879 assert!((total_val - 30.0).abs() < 0.0001); } else if cat.value() == "B" {
881 assert!((total_val - 30.0).abs() < 0.0001);
882 }
883 }
884 }
885 }
886 }
887
888 #[test]
889 fn test_multiple_aggregates() {
890 let results = vec![
891 create_test_binding(vec![("x", 10.0)]),
892 create_test_binding(vec![("x", 20.0)]),
893 create_test_binding(vec![("x", 30.0)]),
894 ];
895
896 let aggregates = vec![
897 AggregateExpression {
898 function: AggregateFunction::Count,
899 variable: Some("x".to_string()),
900 alias: "count".to_string(),
901 distinct: false,
902 },
903 AggregateExpression {
904 function: AggregateFunction::Sum,
905 variable: Some("x".to_string()),
906 alias: "sum".to_string(),
907 distinct: false,
908 },
909 AggregateExpression {
910 function: AggregateFunction::Avg,
911 variable: Some("x".to_string()),
912 alias: "avg".to_string(),
913 distinct: false,
914 },
915 ];
916
917 let (result, vars) =
918 apply_aggregates(results, &aggregates).expect("aggregate operation should succeed");
919 assert_eq!(result.len(), 1);
920 assert_eq!(vars, vec!["count", "sum", "avg"]);
921
922 assert!(result[0].get("count").is_some());
924 assert!(result[0].get("sum").is_some());
925 assert!(result[0].get("avg").is_some());
926 }
927
928 #[test]
929 fn test_median_aggregate() {
930 let results = vec![
932 create_test_binding(vec![("x", 1.0)]),
933 create_test_binding(vec![("x", 3.0)]),
934 create_test_binding(vec![("x", 5.0)]),
935 create_test_binding(vec![("x", 7.0)]),
936 create_test_binding(vec![("x", 9.0)]),
937 ];
938
939 let agg = AggregateExpression {
940 function: AggregateFunction::Median,
941 variable: Some("x".to_string()),
942 alias: "median".to_string(),
943 distinct: false,
944 };
945
946 let (result, _) =
947 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
948 if let Term::Literal(lit) = result[0].get("median").expect("binding should exist") {
949 let median: f64 = lit.value().parse().expect("parse should succeed");
950 assert!((median - 5.0).abs() < 0.001);
951 }
952
953 let results = vec![
955 create_test_binding(vec![("x", 2.0)]),
956 create_test_binding(vec![("x", 4.0)]),
957 create_test_binding(vec![("x", 6.0)]),
958 create_test_binding(vec![("x", 8.0)]),
959 ];
960
961 let agg = AggregateExpression {
962 function: AggregateFunction::Median,
963 variable: Some("x".to_string()),
964 alias: "median".to_string(),
965 distinct: false,
966 };
967
968 let (result, _) =
969 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
970 if let Term::Literal(lit) = result[0].get("median").expect("binding should exist") {
971 let median: f64 = lit.value().parse().expect("parse should succeed");
972 assert!((median - 5.0).abs() < 0.001); }
974 }
975
976 #[test]
977 fn test_variance_aggregate() {
978 let results = vec![
980 create_test_binding(vec![("x", 2.0)]),
981 create_test_binding(vec![("x", 4.0)]),
982 create_test_binding(vec![("x", 6.0)]),
983 create_test_binding(vec![("x", 8.0)]),
984 ];
985
986 let agg = AggregateExpression {
987 function: AggregateFunction::Variance,
988 variable: Some("x".to_string()),
989 alias: "variance".to_string(),
990 distinct: false,
991 };
992
993 let (result, _) =
994 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
995 if let Term::Literal(lit) = result[0].get("variance").expect("binding should exist") {
996 let variance: f64 = lit.value().parse().expect("parse should succeed");
997 assert!((variance - 6.666666666666667).abs() < 0.001);
999 }
1000 }
1001
1002 #[test]
1003 fn test_stddev_aggregate() {
1004 let results = vec![
1006 create_test_binding(vec![("x", 2.0)]),
1007 create_test_binding(vec![("x", 4.0)]),
1008 create_test_binding(vec![("x", 6.0)]),
1009 create_test_binding(vec![("x", 8.0)]),
1010 ];
1011
1012 let agg = AggregateExpression {
1013 function: AggregateFunction::StdDev,
1014 variable: Some("x".to_string()),
1015 alias: "stddev".to_string(),
1016 distinct: false,
1017 };
1018
1019 let (result, _) =
1020 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
1021 if let Term::Literal(lit) = result[0].get("stddev").expect("binding should exist") {
1022 let stddev: f64 = lit.value().parse().expect("parse should succeed");
1023 assert!((stddev - 2.581988897471611).abs() < 0.001);
1025 }
1026 }
1027
1028 #[test]
1029 fn test_percentile_aggregate() {
1030 let results = vec![
1031 create_test_binding(vec![("x", 1.0)]),
1032 create_test_binding(vec![("x", 2.0)]),
1033 create_test_binding(vec![("x", 3.0)]),
1034 create_test_binding(vec![("x", 4.0)]),
1035 create_test_binding(vec![("x", 5.0)]),
1036 create_test_binding(vec![("x", 6.0)]),
1037 create_test_binding(vec![("x", 7.0)]),
1038 create_test_binding(vec![("x", 8.0)]),
1039 create_test_binding(vec![("x", 9.0)]),
1040 create_test_binding(vec![("x", 10.0)]),
1041 ];
1042
1043 let agg = AggregateExpression {
1045 function: AggregateFunction::Percentile { percentile: 50 },
1046 variable: Some("x".to_string()),
1047 alias: "p50".to_string(),
1048 distinct: false,
1049 };
1050
1051 let (result, _) =
1052 apply_aggregates(results.clone(), &[agg]).expect("aggregate operation should succeed");
1053 if let Term::Literal(lit) = result[0].get("p50").expect("binding should exist") {
1054 let p50: f64 = lit.value().parse().expect("parse should succeed");
1055 assert!((p50 - 5.5).abs() < 0.001);
1056 }
1057
1058 let agg = AggregateExpression {
1060 function: AggregateFunction::Percentile { percentile: 95 },
1061 variable: Some("x".to_string()),
1062 alias: "p95".to_string(),
1063 distinct: false,
1064 };
1065
1066 let (result, _) =
1067 apply_aggregates(results.clone(), &[agg]).expect("aggregate operation should succeed");
1068 if let Term::Literal(lit) = result[0].get("p95").expect("binding should exist") {
1069 let p95: f64 = lit.value().parse().expect("parse should succeed");
1070 assert!((p95 - 9.55).abs() < 0.01);
1071 }
1072
1073 let agg = AggregateExpression {
1075 function: AggregateFunction::Percentile { percentile: 25 },
1076 variable: Some("x".to_string()),
1077 alias: "p25".to_string(),
1078 distinct: false,
1079 };
1080
1081 let (result, _) =
1082 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
1083 if let Term::Literal(lit) = result[0].get("p25").expect("binding should exist") {
1084 let p25: f64 = lit.value().parse().expect("parse should succeed");
1085 assert!((p25 - 3.25).abs() < 0.01);
1086 }
1087 }
1088
1089 #[test]
1090 fn test_statistical_aggregates_with_grouping() {
1091 let mut binding1 = VariableBinding::new();
1093 binding1.bind("category".to_string(), Term::from(Literal::new("A")));
1094 binding1.bind("value".to_string(), Term::from(Literal::new("10")));
1095
1096 let mut binding2 = VariableBinding::new();
1097 binding2.bind("category".to_string(), Term::from(Literal::new("A")));
1098 binding2.bind("value".to_string(), Term::from(Literal::new("20")));
1099
1100 let mut binding3 = VariableBinding::new();
1101 binding3.bind("category".to_string(), Term::from(Literal::new("A")));
1102 binding3.bind("value".to_string(), Term::from(Literal::new("30")));
1103
1104 let mut binding4 = VariableBinding::new();
1105 binding4.bind("category".to_string(), Term::from(Literal::new("B")));
1106 binding4.bind("value".to_string(), Term::from(Literal::new("5")));
1107
1108 let mut binding5 = VariableBinding::new();
1109 binding5.bind("category".to_string(), Term::from(Literal::new("B")));
1110 binding5.bind("value".to_string(), Term::from(Literal::new("15")));
1111
1112 let results = vec![binding1, binding2, binding3, binding4, binding5];
1113
1114 let agg = AggregateExpression {
1115 function: AggregateFunction::Median,
1116 variable: Some("value".to_string()),
1117 alias: "median".to_string(),
1118 distinct: false,
1119 };
1120
1121 let group_by = GroupBySpec {
1122 variables: vec!["category".to_string()],
1123 };
1124
1125 let (result, _) = apply_aggregates_with_grouping(results, &[agg], &group_by)
1126 .expect("aggregate operation should succeed");
1127
1128 assert_eq!(result.len(), 2);
1130
1131 for binding in &result {
1133 if let Term::Literal(cat) = binding.get("category").expect("binding should exist") {
1134 if let Term::Literal(median) = binding.get("median").expect("binding should exist")
1135 {
1136 let median_val: f64 = median.value().parse().expect("parse should succeed");
1137 if cat.value() == "A" {
1138 assert!((median_val - 20.0).abs() < 0.001);
1140 } else if cat.value() == "B" {
1141 assert!((median_val - 10.0).abs() < 0.001);
1143 }
1144 }
1145 }
1146 }
1147 }
1148
1149 #[test]
1150 fn test_statistical_aggregate_edge_cases() {
1151 let results: Vec<VariableBinding> = vec![];
1153
1154 let agg = AggregateExpression {
1155 function: AggregateFunction::Median,
1156 variable: Some("x".to_string()),
1157 alias: "median".to_string(),
1158 distinct: false,
1159 };
1160
1161 let (result, _) =
1162 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
1163 if let Term::Literal(lit) = result[0].get("median").expect("binding should exist") {
1165 let median: f64 = lit.value().parse().expect("parse should succeed");
1166 assert_eq!(median, 0.0);
1167 }
1168
1169 let results = vec![create_test_binding(vec![("x", 5.0)])];
1171
1172 let agg = AggregateExpression {
1173 function: AggregateFunction::Variance,
1174 variable: Some("x".to_string()),
1175 alias: "variance".to_string(),
1176 distinct: false,
1177 };
1178
1179 let (result, _) =
1180 apply_aggregates(results, &[agg]).expect("aggregate operation should succeed");
1181 if let Term::Literal(lit) = result[0].get("variance").expect("binding should exist") {
1183 let variance: f64 = lit.value().parse().expect("parse should succeed");
1184 assert_eq!(variance, 0.0);
1185 }
1186 }
1187}