1use std::sync::Arc;
26
27use arrow::array::{
28 Array, ArrayRef, Decimal128Array, Float64Array, Int64Array, StringArray, UInt64Array,
29};
30use arrow::compute::kernels::aggregate as arrow_agg;
31use arrow::datatypes::{DataType, Field, Schema};
32use arrow::record_batch::RecordBatch;
33
34use crate::error::{QueryError, Result};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AggFn {
39 Count,
41 Sum,
43 Min,
45 Max,
47 Avg,
49 First,
65 Last,
81}
82
83pub const AVG_EXTRA_SCALE: u8 = 6;
94
95#[derive(Debug, Clone, Copy, PartialEq)]
103pub enum AggResult {
104 Null,
106 F64(f64),
108 Decimal {
110 mantissa: i128,
112 scale: u8,
114 },
115}
116
117impl AggResult {
118 #[must_use]
124 #[allow(clippy::cast_precision_loss)]
125 pub fn as_f64(self) -> Option<f64> {
126 match self {
127 Self::Null => None,
128 Self::F64(v) => Some(v),
129 Self::Decimal { mantissa, scale } => {
130 Some(mantissa as f64 / chronix_core::pow10(u32::from(scale)).unwrap_or(1) as f64)
131 }
132 }
133 }
134}
135
136pub fn agg_result_column(name: &str, results: &[AggResult]) -> Result<(Field, ArrayRef)> {
147 let scale = results.iter().find_map(|r| match r {
148 AggResult::Decimal { scale, .. } => Some(*scale),
149 _ => None,
150 });
151 match scale {
152 Some(scale) => {
153 let scale_i8 = i8::try_from(scale).map_err(|_| {
154 QueryError::Validation(format!("decimal scale {scale} is out of range"))
155 })?;
156 let mantissas: Vec<Option<i128>> = results
157 .iter()
158 .map(|r| match r {
159 AggResult::Decimal { mantissa, .. } => Some(*mantissa),
160 _ => None,
161 })
162 .collect();
163 let array = Decimal128Array::from(mantissas)
164 .with_precision_and_scale(chronix_core::DECIMAL_PRECISION, scale_i8)?;
165 let field = Field::new(
166 name,
167 DataType::Decimal128(chronix_core::DECIMAL_PRECISION, scale_i8),
168 true,
169 );
170 Ok((field, Arc::new(array)))
171 }
172 None => {
173 let values: Vec<Option<f64>> = results.iter().map(|r| r.as_f64()).collect();
174 Ok((
175 Field::new(name, DataType::Float64, true),
176 Arc::new(Float64Array::from(values)),
177 ))
178 }
179 }
180}
181
182#[derive(Clone, Copy, Debug)]
184pub enum Num {
185 F64(f64),
187 Dec(i128, u8),
189}
190
191#[derive(Debug, Clone)]
199pub struct DecimalTrack {
200 pub scale: u8,
202 pub sum: i128,
204 pub min: i128,
206 pub max: i128,
208 pub first: i128,
210 pub last: i128,
212 pub overflow: bool,
216}
217
218impl DecimalTrack {
219 #[must_use]
221 pub fn new(scale: u8) -> Self {
222 Self {
223 scale,
224 sum: 0,
225 min: i128::MAX,
226 max: i128::MIN,
227 first: 0,
228 last: 0,
229 overflow: false,
230 }
231 }
232
233 pub fn widen_to(&mut self, scale: u8) {
235 let Some(factor) = scale
236 .checked_sub(self.scale)
237 .and_then(|d| chronix_core::pow10(u32::from(d)))
238 else {
239 self.overflow = true;
240 return;
241 };
242 let mut scaled = |v: i128| match v.checked_mul(factor) {
243 Some(x) => x,
244 None => {
245 self.overflow = true;
246 v
247 }
248 };
249 self.sum = scaled(self.sum);
250 if self.min != i128::MAX {
253 self.min = scaled(self.min);
254 }
255 if self.max != i128::MIN {
256 self.max = scaled(self.max);
257 }
258 self.first = scaled(self.first);
259 self.last = scaled(self.last);
260 self.scale = scale;
261 }
262
263 pub fn align(&mut self, mantissa: i128, scale: u8) -> Option<i128> {
269 match scale.cmp(&self.scale) {
270 std::cmp::Ordering::Equal => Some(mantissa),
271 std::cmp::Ordering::Greater => {
272 self.widen_to(scale);
273 Some(mantissa)
274 }
275 std::cmp::Ordering::Less => chronix_core::pow10(u32::from(self.scale - scale))
276 .and_then(|f| mantissa.checked_mul(f)),
277 }
278 }
279
280 #[must_use]
282 pub fn sum_result(&self) -> AggResult {
283 if self.overflow || chronix_core::Decimal::new(self.sum, self.scale).is_err() {
284 return AggResult::Null;
285 }
286 AggResult::Decimal {
287 mantissa: self.sum,
288 scale: self.scale,
289 }
290 }
291
292 #[must_use]
296 pub fn avg_result(&self, count: u64) -> AggResult {
297 if self.overflow || count == 0 {
298 return AggResult::Null;
299 }
300 let target = self
301 .scale
302 .saturating_add(AVG_EXTRA_SCALE)
303 .min(chronix_core::MAX_DECIMAL_SCALE);
304 let extra = target - self.scale;
305 let Some(numerator) =
306 chronix_core::pow10(u32::from(extra)).and_then(|factor| self.sum.checked_mul(factor))
307 else {
308 return AggResult::Null;
309 };
310 let divisor = i128::from(count);
311 let quotient = numerator / divisor;
312 let remainder = numerator % divisor;
313 let rounded = match remainder.unsigned_abs().checked_mul(2) {
315 Some(twice) if twice >= divisor.unsigned_abs() => {
316 quotient + if numerator < 0 { -1 } else { 1 }
317 }
318 _ => quotient,
319 };
320 if chronix_core::Decimal::new(rounded, target).is_err() {
321 return AggResult::Null;
322 }
323 AggResult::Decimal {
324 mantissa: rounded,
325 scale: target,
326 }
327 }
328
329 #[must_use]
331 pub fn at(&self, mantissa: i128) -> AggResult {
332 AggResult::Decimal {
333 mantissa,
334 scale: self.scale,
335 }
336 }
337}
338
339macro_rules! impl_aggregate {
345 ($name:ident, $arr_ty:ty, $conv:expr) => {
346 #[allow(clippy::cast_precision_loss)]
347 fn $name(values: &$arr_ty, function: AggFn) -> Option<f64> {
348 if values.is_empty() {
350 return if function == AggFn::Count {
351 Some(0.0)
352 } else {
353 None
354 };
355 }
356 match function {
357 AggFn::Count => Some((values.len() - values.null_count()) as f64),
360 AggFn::Sum => arrow_agg::sum(values).map($conv),
361 AggFn::Min => arrow_agg::min(values).map($conv),
362 AggFn::Max => arrow_agg::max(values).map($conv),
363 AggFn::Avg => {
364 let sum = arrow_agg::sum(values).map($conv)?;
365 let count = values.len() - values.null_count();
366 if count == 0 {
367 None
368 } else {
369 Some(sum / count as f64)
370 }
371 }
372 AggFn::First => values.iter().flatten().next().map($conv),
373 AggFn::Last => values.iter().flatten().last().map($conv),
374 }
375 }
376 };
377}
378
379impl_aggregate!(aggregate_f64, Float64Array, |v| v);
380impl_aggregate!(aggregate_i64, Int64Array, |v| v as f64);
381impl_aggregate!(aggregate_u64, UInt64Array, |v| v as f64);
382
383pub fn aggregate_batch(
391 batch: &RecordBatch,
392 functions: &[AggFn],
393 field_columns: &[&str],
394 memory_tracker: Option<&crate::memory::MemoryTracker>,
395) -> Result<RecordBatch> {
396 let mut result_fields = Vec::new();
397 let mut result_columns: Vec<ArrayRef> = Vec::new();
398
399 let ts_col = batch
401 .column_by_name(chronix_core::TIME_COLUMN)
402 .and_then(|c| c.as_any().downcast_ref::<Int64Array>());
403
404 for &field_name in field_columns {
405 let col = batch
406 .column_by_name(field_name)
407 .ok_or_else(|| QueryError::Validation(format!("column '{field_name}' not found")))?;
408
409 for func in functions {
410 let func_name = format!("{field_name}_{}", agg_fn_name(*func));
411 let value = aggregate_column_ts(col, *func, ts_col);
412 let (field, array) = agg_result_column(&func_name, &[value])?;
413 result_fields.push(field);
414 result_columns.push(array);
415 }
416 }
417
418 if result_fields.is_empty() {
419 return Err(QueryError::Validation("no columns to aggregate".into()));
420 }
421
422 let schema = Arc::new(Schema::new(result_fields));
423 let batch = RecordBatch::try_new(schema, result_columns)?;
424 if let Some(tracker) = memory_tracker {
425 tracker.try_allocate(batch.get_array_memory_size())?;
426 }
427 Ok(batch)
428}
429
430pub fn aggregate_grouped(
455 batch: &RecordBatch,
456 functions: &[AggFn],
457 field_columns: &[&str],
458 group_by: &[&str],
459 memory_tracker: Option<&crate::memory::MemoryTracker>,
460) -> Result<RecordBatch> {
461 if group_by.is_empty() {
462 return aggregate_batch(batch, functions, field_columns, memory_tracker);
463 }
464
465 let group_cols: Vec<&StringArray> = group_by
467 .iter()
468 .map(|name| {
469 batch
470 .column_by_name(name)
471 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
472 .ok_or_else(|| {
473 QueryError::Validation(format!(
474 "group-by column '{name}' not found or not string"
475 ))
476 })
477 })
478 .collect::<Result<Vec<_>>>()?;
479
480 let field_cols: Vec<&dyn Array> = field_columns
482 .iter()
483 .map(|name| {
484 batch
485 .column_by_name(name)
486 .map(std::convert::AsRef::as_ref)
487 .ok_or_else(|| QueryError::Validation(format!("column '{name}' not found")))
488 })
489 .collect::<Result<Vec<_>>>()?;
490
491 let typed_cols: Vec<TypedColumn<'_>> = field_cols
493 .iter()
494 .map(|c| TypedColumn::from_array(*c))
495 .collect();
496
497 let ts_col = batch
498 .column_by_name(chronix_core::TIME_COLUMN)
499 .and_then(|c| c.as_any().downcast_ref::<Int64Array>());
500
501 let num_fields = field_columns.len();
505
506 let mut groups: Vec<(Vec<Option<&str>>, Vec<IncrementalAccumulator>)> = Vec::new();
508 let mut group_index: std::collections::HashMap<u64, Vec<usize>> =
509 std::collections::HashMap::new();
510
511 for row in 0..batch.num_rows() {
512 let ts: Option<i64> = ts_col.and_then(|t| {
513 if t.is_null(row) {
514 None
515 } else {
516 Some(t.value(row))
517 }
518 });
519
520 let mut hasher = fnv::FnvHasher::default();
522 for col in &group_cols {
523 if col.is_null(row) {
524 std::hash::Hash::hash(&0u8, &mut hasher);
525 } else {
526 std::hash::Hash::hash(&1u8, &mut hasher);
527 std::hash::Hash::hash(col.value(row), &mut hasher);
528 }
529 }
530 let hash = std::hash::Hasher::finish(&hasher);
531
532 let key: Vec<Option<&str>> = group_cols
534 .iter()
535 .map(|col| {
536 if col.is_null(row) {
537 None
538 } else {
539 Some(col.value(row))
540 }
541 })
542 .collect();
543
544 let bucket = group_index.entry(hash).or_default();
546 if bucket.len() >= 1024 {
548 return Err(QueryError::Validation(
549 "group-by hash chain length exceeded limit (possible collision attack)".into(),
550 ));
551 }
552 let gidx = {
553 let mut found = None;
554 for &idx in bucket.iter() {
555 if groups[idx].0 == key {
556 found = Some(idx);
557 break;
558 }
559 }
560 match found {
561 Some(idx) => idx,
562 None => {
563 if let Some(tracker) = memory_tracker {
565 let estimated_group_bytes = 24 + group_by.len() * 16 + num_fields * std::mem::size_of::<IncrementalAccumulator>()
567 + 64; tracker.try_allocate(estimated_group_bytes)?;
569 }
570 let idx = groups.len();
571 let accs = (0..num_fields)
572 .map(|_| IncrementalAccumulator::new())
573 .collect();
574 groups.push((key, accs));
575 bucket.push(idx);
576 idx
577 }
578 }
579 };
580
581 for (fi, typed) in typed_cols.iter().enumerate() {
585 if let Some(v) = typed.value_num(row) {
586 groups[gidx].1[fi].update(v, ts);
587 }
588 }
589 }
590
591 let mut result_fields: Vec<Field> = group_by
593 .iter()
594 .map(|name| Field::new(*name, DataType::Utf8, true))
595 .collect();
596
597 let num_groups = groups.len();
599 let mut result_columns: Vec<ArrayRef> = Vec::new();
600
601 for col_idx in 0..group_by.len() {
603 let vals: Vec<Option<&str>> = groups.iter().map(|(key, _)| key[col_idx]).collect();
604 let arr: StringArray = vals.into_iter().collect();
605 result_columns.push(Arc::new(arr));
606 }
607
608 for (fi, &field_name) in field_columns.iter().enumerate() {
613 for func in functions {
614 let func_name = format!("{field_name}_{}", agg_fn_name(*func));
615 let vals: Vec<AggResult> = (0..num_groups)
616 .map(|gi| groups[gi].1[fi].finalize(*func))
617 .collect();
618 let (field, array) = agg_result_column(&func_name, &vals)?;
619 result_fields.push(field);
620 result_columns.push(array);
621 }
622 }
623 debug_assert_eq!(num_fields, field_columns.len());
624
625 let schema = Arc::new(Schema::new(result_fields));
626 let batch = RecordBatch::try_new(schema, result_columns)?;
627 if let Some(tracker) = memory_tracker {
628 tracker.try_allocate(batch.get_array_memory_size())?;
629 }
630 Ok(batch)
631}
632
633#[allow(clippy::cast_precision_loss)]
637fn aggregate_column_ts(col: &dyn Array, func: AggFn, timestamps: Option<&Int64Array>) -> AggResult {
638 if let Some(ts) = timestamps {
641 if matches!(func, AggFn::First | AggFn::Last) {
642 return first_last_by_ts(col, ts, func);
643 }
644 }
645
646 if let TypedColumn::Decimal(a, scale) = TypedColumn::from_array(col) {
649 if matches!(func, AggFn::First | AggFn::Last) {
654 let mut present = (0..a.len()).filter(|&i| !a.is_null(i));
655 let idx = if func == AggFn::First {
656 present.next()
657 } else {
658 present.next_back()
659 };
660 return idx.map_or(AggResult::Null, |i| AggResult::Decimal {
661 mantissa: a.value(i),
662 scale,
663 });
664 }
665 let mut acc = IncrementalAccumulator::new();
666 for i in 0..a.len() {
667 if !a.is_null(i) {
668 acc.update(Num::Dec(a.value(i), scale), None);
669 }
670 }
671 return acc.finalize(func);
672 }
673
674 let value = if let Some(f64_col) = col.as_any().downcast_ref::<Float64Array>() {
675 aggregate_f64(f64_col, func)
676 } else if let Some(i64_col) = col.as_any().downcast_ref::<Int64Array>() {
677 aggregate_i64(i64_col, func)
678 } else if let Some(u64_col) = col.as_any().downcast_ref::<UInt64Array>() {
679 aggregate_u64(u64_col, func)
680 } else {
681 match func {
682 AggFn::Count => Some((col.len() - col.null_count()) as f64),
683 _ => None,
684 }
685 };
686 value.map_or(AggResult::Null, AggResult::F64)
687}
688
689#[allow(clippy::cast_precision_loss)]
692fn first_last_by_ts(col: &dyn Array, ts: &Int64Array, func: AggFn) -> AggResult {
693 let find_idx = |cmp: fn(i64, i64) -> bool| -> Option<usize> {
694 let mut best_idx: Option<usize> = None;
695 let mut best_ts: i64 = 0;
696 for i in 0..ts.len() {
697 if ts.is_null(i) || col.is_null(i) {
698 continue;
699 }
700 let t = ts.value(i);
701 if best_idx.is_none() || cmp(t, best_ts) {
702 best_idx = Some(i);
703 best_ts = t;
704 }
705 }
706 best_idx
707 };
708
709 let idx = match func {
710 AggFn::First => find_idx(|t, best| t < best),
711 AggFn::Last => find_idx(|t, best| t >= best),
715 _ => return AggResult::Null,
716 };
717 let Some(idx) = idx else {
718 return AggResult::Null;
719 };
720
721 match TypedColumn::from_array(col).value_num(idx) {
723 Some(Num::F64(v)) => AggResult::F64(v),
724 Some(Num::Dec(mantissa, scale)) => AggResult::Decimal { mantissa, scale },
725 None => AggResult::Null,
726 }
727}
728
729#[must_use]
735pub fn agg_fn_name(func: AggFn) -> &'static str {
736 match func {
737 AggFn::Count => "count",
738 AggFn::Sum => "sum",
739 AggFn::Min => "min",
740 AggFn::Max => "max",
741 AggFn::Avg => "avg",
742 AggFn::First => "first",
743 AggFn::Last => "last",
744 }
745}
746
747#[derive(Clone, Copy)]
752enum TypedColumn<'a> {
753 Float64(&'a Float64Array),
754 Int64(&'a Int64Array),
755 UInt64(&'a UInt64Array),
756 Decimal(&'a Decimal128Array, u8),
758 Other,
759}
760
761impl<'a> TypedColumn<'a> {
762 fn from_array(arr: &'a dyn Array) -> Self {
763 if let Some(a) = arr.as_any().downcast_ref::<Float64Array>() {
764 Self::Float64(a)
765 } else if let Some(a) = arr.as_any().downcast_ref::<Int64Array>() {
766 Self::Int64(a)
767 } else if let Some(a) = arr.as_any().downcast_ref::<UInt64Array>() {
768 Self::UInt64(a)
769 } else if let Some(a) = arr.as_any().downcast_ref::<Decimal128Array>() {
770 Self::Decimal(a, u8::try_from(a.scale()).unwrap_or(0))
773 } else {
774 Self::Other
775 }
776 }
777
778 #[allow(clippy::cast_precision_loss)]
780 fn value_num(&self, row: usize) -> Option<Num> {
781 match self {
782 Self::Decimal(a, scale) => {
783 if a.is_null(row) {
784 None
785 } else {
786 Some(Num::Dec(a.value(row), *scale))
787 }
788 }
789 _ => self.value_f64(row).map(Num::F64),
790 }
791 }
792
793 #[allow(clippy::cast_precision_loss)]
794 fn value_f64(&self, row: usize) -> Option<f64> {
795 match self {
796 Self::Float64(a) => {
797 if a.is_null(row) {
798 None
799 } else {
800 Some(a.value(row))
801 }
802 }
803 Self::Int64(a) => {
804 if a.is_null(row) {
805 None
806 } else {
807 Some(a.value(row) as f64)
808 }
809 }
810 Self::UInt64(a) => {
811 if a.is_null(row) {
812 None
813 } else {
814 Some(a.value(row) as f64)
815 }
816 }
817 Self::Decimal(a, scale) => {
821 if a.is_null(row) {
822 None
823 } else {
824 Some(
825 a.value(row) as f64
826 / chronix_core::pow10(u32::from(*scale)).unwrap_or(1) as f64,
827 )
828 }
829 }
830 Self::Other => None,
831 }
832 }
833}
834
835#[derive(Debug, Clone)]
838struct IncrementalAccumulator {
839 count: u64,
840 sum: f64,
841 min: f64,
842 max: f64,
843 first_ts: i64,
845 first_val: f64,
846 first_set: bool,
847 last_ts: i64,
849 last_val: f64,
850 last_set: bool,
851 decimal: Option<Box<DecimalTrack>>,
853}
854
855impl IncrementalAccumulator {
856 fn new() -> Self {
857 Self {
858 count: 0,
859 sum: 0.0,
860 min: f64::INFINITY,
861 max: f64::NEG_INFINITY,
862 first_ts: i64::MAX,
863 first_val: 0.0,
864 first_set: false,
865 last_ts: i64::MIN,
866 last_val: 0.0,
867 last_set: false,
868 decimal: None,
869 }
870 }
871
872 #[allow(clippy::cast_precision_loss)]
873 fn update(&mut self, value: Num, timestamp: Option<i64>) {
874 self.count += 1;
875 match value {
876 Num::F64(v) => {
877 self.sum += v;
878 if v < self.min {
879 self.min = v;
880 }
881 if v > self.max {
882 self.max = v;
883 }
884 }
885 Num::Dec(mantissa, scale) => {
886 let track = self
887 .decimal
888 .get_or_insert_with(|| Box::new(DecimalTrack::new(scale)));
889 match track.align(mantissa, scale) {
890 Some(m) => {
891 match track.sum.checked_add(m) {
892 Some(sum) => track.sum = sum,
893 None => track.overflow = true,
894 }
895 if m < track.min {
896 track.min = m;
897 }
898 if m > track.max {
899 track.max = m;
900 }
901 }
902 None => track.overflow = true,
903 }
904 }
905 }
906 if let Some(ts) = timestamp {
910 let first = ts < self.first_ts || !self.first_set;
911 let last = ts >= self.last_ts || !self.last_set;
914 if first {
915 self.first_ts = ts;
916 self.first_set = true;
917 }
918 if last {
919 self.last_ts = ts;
920 self.last_set = true;
921 }
922 match value {
923 Num::F64(v) => {
924 if first {
925 self.first_val = v;
926 }
927 if last {
928 self.last_val = v;
929 }
930 }
931 Num::Dec(mantissa, scale) => {
932 if let Some(track) = self.decimal.as_deref_mut() {
933 if let Some(m) = track.align(mantissa, scale) {
934 if first {
935 track.first = m;
936 }
937 if last {
938 track.last = m;
939 }
940 }
941 }
942 }
943 }
944 }
945 }
946
947 #[allow(clippy::cast_precision_loss)]
948 fn finalize(&self, func: AggFn) -> AggResult {
949 if self.count == 0 {
950 return if func == AggFn::Count {
951 AggResult::F64(0.0)
952 } else {
953 AggResult::Null
954 };
955 }
956 if func == AggFn::Count {
960 return AggResult::F64(self.count as f64);
961 }
962 if let Some(track) = self.decimal.as_deref() {
963 return match func {
964 AggFn::Sum => track.sum_result(),
965 AggFn::Avg => track.avg_result(self.count),
966 AggFn::Min => track.at(track.min),
967 AggFn::Max => track.at(track.max),
968 AggFn::First => track.at(track.first),
969 AggFn::Last => track.at(track.last),
970 AggFn::Count => unreachable!("handled above"),
971 };
972 }
973 match func {
974 AggFn::Sum => AggResult::F64(self.sum),
975 AggFn::Min => AggResult::F64(self.min),
976 AggFn::Max => AggResult::F64(self.max),
977 AggFn::Avg => AggResult::F64(self.sum / self.count as f64),
978 AggFn::First => AggResult::F64(self.first_val),
979 AggFn::Last => AggResult::F64(self.last_val),
980 AggFn::Count => unreachable!("handled above"),
981 }
982 }
983}
984
985#[derive(Debug, Clone, Copy, PartialEq, Eq)]
997pub enum AggregationStrategy {
998 Hash,
1000 Sort,
1002}
1003
1004const SORT_CARDINALITY_THRESHOLD: usize = 1024;
1009
1010#[must_use]
1016pub fn choose_strategy(estimated_cardinality: Option<usize>) -> AggregationStrategy {
1017 match estimated_cardinality {
1018 Some(c) if c <= SORT_CARDINALITY_THRESHOLD => AggregationStrategy::Sort,
1019 _ => AggregationStrategy::Hash,
1020 }
1021}
1022
1023pub fn aggregate_sorted(
1036 batch: &RecordBatch,
1037 functions: &[AggFn],
1038 field_columns: &[&str],
1039 group_by: &[&str],
1040 memory_tracker: Option<&crate::memory::MemoryTracker>,
1041) -> Result<RecordBatch> {
1042 use arrow::row::{RowConverter, SortField};
1043
1044 if group_by.is_empty() {
1045 return aggregate_batch(batch, functions, field_columns, memory_tracker);
1046 }
1047
1048 let group_arrays: Vec<ArrayRef> = group_by
1050 .iter()
1051 .map(|name| {
1052 batch.column_by_name(name).cloned().ok_or_else(|| {
1053 QueryError::Validation(format!("group-by column '{name}' not found"))
1054 })
1055 })
1056 .collect::<Result<Vec<_>>>()?;
1057
1058 let sort_fields: Vec<SortField> = group_arrays
1059 .iter()
1060 .map(|a| SortField::new(a.data_type().clone()))
1061 .collect();
1062
1063 let converter = RowConverter::new(sort_fields)?;
1064 let rows = converter.convert_columns(&group_arrays)?;
1065
1066 let mut indices: Vec<u32> = (0..batch.num_rows() as u32).collect();
1068 indices.sort_by(|&a, &b| rows.row(a as usize).cmp(&rows.row(b as usize)));
1069
1070 let field_cols: Vec<&dyn Array> = field_columns
1072 .iter()
1073 .map(|name| {
1074 batch
1075 .column_by_name(name)
1076 .map(std::convert::AsRef::as_ref)
1077 .ok_or_else(|| QueryError::Validation(format!("column '{name}' not found")))
1078 })
1079 .collect::<Result<Vec<_>>>()?;
1080 let typed_cols: Vec<TypedColumn<'_>> = field_cols
1081 .iter()
1082 .map(|c| TypedColumn::from_array(*c))
1083 .collect();
1084
1085 let ts_col = batch
1086 .column_by_name(chronix_core::TIME_COLUMN)
1087 .and_then(|c| c.as_any().downcast_ref::<Int64Array>());
1088
1089 let group_cols: Vec<&StringArray> = group_by
1090 .iter()
1091 .map(|name| {
1092 batch
1093 .column_by_name(name)
1094 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
1095 .ok_or_else(|| {
1096 QueryError::Validation(format!(
1097 "group-by column '{name}' not found or not string"
1098 ))
1099 })
1100 })
1101 .collect::<Result<Vec<_>>>()?;
1102
1103 let num_fields = field_columns.len();
1104 let mut groups: Vec<(Vec<Option<String>>, Vec<IncrementalAccumulator>)> = Vec::new();
1105
1106 let mut run_start = 0usize;
1108 while run_start < indices.len() {
1109 let anchor_row = rows.row(indices[run_start] as usize);
1110 let mut run_end = run_start + 1;
1111 while run_end < indices.len() && rows.row(indices[run_end] as usize) == anchor_row {
1112 run_end += 1;
1113 }
1114
1115 let first_idx = indices[run_start] as usize;
1117 let key: Vec<Option<String>> = group_cols
1118 .iter()
1119 .map(|col| {
1120 if col.is_null(first_idx) {
1121 None
1122 } else {
1123 Some(col.value(first_idx).to_owned())
1124 }
1125 })
1126 .collect();
1127
1128 if let Some(tracker) = memory_tracker {
1129 let est = 24
1130 + group_by.len() * 48
1131 + num_fields * std::mem::size_of::<IncrementalAccumulator>()
1132 + 64;
1133 tracker.try_allocate(est)?;
1134 }
1135
1136 let mut accs: Vec<IncrementalAccumulator> = (0..num_fields)
1137 .map(|_| IncrementalAccumulator::new())
1138 .collect();
1139
1140 for &idx in &indices[run_start..run_end] {
1141 let row = idx as usize;
1142 let ts: Option<i64> = ts_col.and_then(|t| {
1143 if t.is_null(row) {
1144 None
1145 } else {
1146 Some(t.value(row))
1147 }
1148 });
1149 for (fi, typed) in typed_cols.iter().enumerate() {
1150 if let Some(v) = typed.value_num(row) {
1151 accs[fi].update(v, ts);
1152 }
1153 }
1154 }
1155
1156 groups.push((key, accs));
1157 run_start = run_end;
1158 }
1159
1160 let mut result_fields: Vec<Field> = group_by
1162 .iter()
1163 .map(|name| Field::new(*name, DataType::Utf8, true))
1164 .collect();
1165 let num_groups = groups.len();
1166 let mut result_columns: Vec<ArrayRef> = Vec::new();
1167
1168 for col_idx in 0..group_by.len() {
1169 let vals: Vec<Option<&str>> = groups
1170 .iter()
1171 .map(|(key, _)| key[col_idx].as_deref())
1172 .collect();
1173 let arr: StringArray = vals.into_iter().collect();
1174 result_columns.push(Arc::new(arr));
1175 }
1176
1177 for (fi, &field_name) in field_columns.iter().enumerate() {
1178 for func in functions {
1179 let func_name = format!("{field_name}_{}", agg_fn_name(*func));
1180 let vals: Vec<AggResult> = (0..num_groups)
1181 .map(|gi| groups[gi].1[fi].finalize(*func))
1182 .collect();
1183 let (field, array) = agg_result_column(&func_name, &vals)?;
1184 result_fields.push(field);
1185 result_columns.push(array);
1186 }
1187 }
1188 debug_assert_eq!(num_fields, field_columns.len());
1189
1190 let schema = Arc::new(Schema::new(result_fields));
1191 let batch = RecordBatch::try_new(schema, result_columns)?;
1192 if let Some(tracker) = memory_tracker {
1193 tracker.try_allocate(batch.get_array_memory_size())?;
1194 }
1195 Ok(batch)
1196}
1197
1198pub fn aggregate_grouped_streaming(
1209 batches: &[RecordBatch],
1210 functions: &[AggFn],
1211 field_columns: &[&str],
1212 group_by: &[&str],
1213 memory_tracker: Option<&crate::memory::MemoryTracker>,
1214) -> Result<RecordBatch> {
1215 if batches.is_empty() {
1216 return Err(QueryError::Validation("no batches to aggregate".into()));
1217 }
1218 let mut agg = StreamingAggregator::new(functions, field_columns, group_by);
1219 for batch in batches {
1220 agg.push(batch, memory_tracker)?;
1221 }
1222 agg.finish(memory_tracker)
1223}
1224
1225pub struct StreamingAggregator {
1237 functions: Vec<AggFn>,
1238 field_columns: Vec<String>,
1239 group_by: Vec<String>,
1240 state: AggState,
1241}
1242
1243enum AggState {
1244 Grouped {
1246 groups: Vec<(Vec<Option<String>>, Vec<IncrementalAccumulator>)>,
1247 index: std::collections::HashMap<u64, Vec<usize>>,
1249 },
1250 Ungrouped(UngroupedState),
1252}
1253
1254struct UngroupedState {
1256 counts: Vec<u64>,
1257 sums: Vec<f64>,
1258 mins: Vec<f64>,
1259 maxs: Vec<f64>,
1260 first_ts: Vec<i64>,
1261 first_val: Vec<f64>,
1262 first_set: Vec<bool>,
1263 last_ts: Vec<i64>,
1264 last_val: Vec<f64>,
1265 last_set: Vec<bool>,
1266 needs_first_last: bool,
1267 decimals: Vec<Option<Box<DecimalTrack>>>,
1269}
1270
1271impl StreamingAggregator {
1272 #[must_use]
1275 pub fn new(functions: &[AggFn], field_columns: &[&str], group_by: &[&str]) -> Self {
1276 let num_fields = field_columns.len();
1277 let state = if group_by.is_empty() {
1278 AggState::Ungrouped(UngroupedState {
1279 counts: vec![0; num_fields],
1280 sums: vec![0.0; num_fields],
1281 mins: vec![f64::INFINITY; num_fields],
1282 maxs: vec![f64::NEG_INFINITY; num_fields],
1283 first_ts: vec![i64::MAX; num_fields],
1284 first_val: vec![0.0; num_fields],
1285 first_set: vec![false; num_fields],
1286 last_ts: vec![i64::MIN; num_fields],
1287 last_val: vec![0.0; num_fields],
1288 last_set: vec![false; num_fields],
1289 needs_first_last: functions
1290 .iter()
1291 .any(|f| matches!(f, AggFn::First | AggFn::Last)),
1292 decimals: (0..num_fields).map(|_| None).collect(),
1293 })
1294 } else {
1295 AggState::Grouped {
1296 groups: Vec::new(),
1297 index: std::collections::HashMap::new(),
1298 }
1299 };
1300 Self {
1301 functions: functions.to_vec(),
1302 field_columns: field_columns.iter().map(|s| (*s).to_string()).collect(),
1303 group_by: group_by.iter().map(|s| (*s).to_string()).collect(),
1304 state,
1305 }
1306 }
1307
1308 pub fn push(
1315 &mut self,
1316 batch: &RecordBatch,
1317 memory_tracker: Option<&crate::memory::MemoryTracker>,
1318 ) -> Result<()> {
1319 let field_columns: Vec<&str> = self.field_columns.iter().map(String::as_str).collect();
1320 match &mut self.state {
1321 AggState::Ungrouped(st) => {
1322 st.push(batch, &field_columns);
1323 Ok(())
1324 }
1325 AggState::Grouped { groups, index } => {
1326 let group_by: Vec<&str> = self.group_by.iter().map(String::as_str).collect();
1327 push_grouped(
1328 batch,
1329 &field_columns,
1330 &group_by,
1331 groups,
1332 index,
1333 memory_tracker,
1334 )
1335 }
1336 }
1337 }
1338
1339 pub fn finish(
1346 self,
1347 memory_tracker: Option<&crate::memory::MemoryTracker>,
1348 ) -> Result<RecordBatch> {
1349 let field_columns: Vec<&str> = self.field_columns.iter().map(String::as_str).collect();
1350 let batch = match self.state {
1351 AggState::Ungrouped(st) => st.finish(&self.functions, &field_columns)?,
1352 AggState::Grouped { groups, .. } => {
1353 let group_by: Vec<&str> = self.group_by.iter().map(String::as_str).collect();
1354 finish_grouped(&groups, &self.functions, &field_columns, &group_by)?
1355 }
1356 };
1357 if let Some(tracker) = memory_tracker {
1358 tracker.try_allocate(batch.get_array_memory_size())?;
1359 }
1360 Ok(batch)
1361 }
1362}
1363
1364fn push_grouped(
1366 batch: &RecordBatch,
1367 field_columns: &[&str],
1368 group_by: &[&str],
1369 groups: &mut Vec<(Vec<Option<String>>, Vec<IncrementalAccumulator>)>,
1370 group_index: &mut std::collections::HashMap<u64, Vec<usize>>,
1371 memory_tracker: Option<&crate::memory::MemoryTracker>,
1372) -> Result<()> {
1373 let num_fields = field_columns.len();
1374
1375 let group_cols: Vec<Option<&StringArray>> = group_by
1376 .iter()
1377 .map(|name| {
1378 batch
1379 .column_by_name(name)
1380 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
1381 })
1382 .collect();
1383
1384 let ts_col = batch
1385 .column_by_name(chronix_core::TIME_COLUMN)
1386 .and_then(|c| c.as_any().downcast_ref::<Int64Array>());
1387
1388 let typed_cols: Vec<Option<TypedColumn<'_>>> = field_columns
1390 .iter()
1391 .map(|name| {
1392 batch
1393 .column_by_name(name)
1394 .map(|c| TypedColumn::from_array(c.as_ref()))
1395 })
1396 .collect();
1397
1398 for row in 0..batch.num_rows() {
1399 let ts: Option<i64> = ts_col.and_then(|t| {
1400 if t.is_null(row) {
1401 None
1402 } else {
1403 Some(t.value(row))
1404 }
1405 });
1406
1407 let mut hasher = fnv::FnvHasher::default();
1409 for col in &group_cols {
1410 match col {
1411 Some(c) if !c.is_null(row) => {
1412 std::hash::Hash::hash(&1u8, &mut hasher);
1413 std::hash::Hash::hash(c.value(row), &mut hasher);
1414 }
1415 _ => std::hash::Hash::hash(&0u8, &mut hasher),
1416 }
1417 }
1418 let hash = std::hash::Hasher::finish(&hasher);
1419
1420 let key: Vec<Option<&str>> = group_cols
1421 .iter()
1422 .map(|col| match col {
1423 Some(c) if !c.is_null(row) => Some(c.value(row)),
1424 _ => None,
1425 })
1426 .collect();
1427
1428 let bucket = group_index.entry(hash).or_default();
1431 let gidx = {
1432 let mut found = None;
1433 for &idx in bucket.iter() {
1434 let existing: Vec<Option<&str>> =
1435 groups[idx].0.iter().map(|o| o.as_deref()).collect();
1436 if existing == key {
1437 found = Some(idx);
1438 break;
1439 }
1440 }
1441 match found {
1442 Some(idx) => idx,
1443 None => {
1444 if let Some(tracker) = memory_tracker {
1446 let estimated_bytes = group_by.len() * 48 + num_fields * std::mem::size_of::<IncrementalAccumulator>()
1448 + 48; tracker.try_allocate(estimated_bytes)?;
1450 }
1451 let idx = groups.len();
1452 let owned_key: Vec<Option<String>> =
1453 key.iter().map(|o| o.map(String::from)).collect();
1454 let accs = (0..num_fields)
1455 .map(|_| IncrementalAccumulator::new())
1456 .collect();
1457 groups.push((owned_key, accs));
1458 bucket.push(idx);
1459 idx
1460 }
1461 }
1462 };
1463
1464 for (fi, typed) in typed_cols.iter().enumerate() {
1467 if let Some(v) = typed.and_then(|tc| tc.value_num(row)) {
1468 groups[gidx].1[fi].update(v, ts);
1469 }
1470 }
1471 }
1472
1473 Ok(())
1474}
1475
1476fn finish_grouped(
1478 groups: &[(Vec<Option<String>>, Vec<IncrementalAccumulator>)],
1479 functions: &[AggFn],
1480 field_columns: &[&str],
1481 group_by: &[&str],
1482) -> Result<RecordBatch> {
1483 let num_groups = groups.len();
1484 let mut result_fields: Vec<Field> = group_by
1485 .iter()
1486 .map(|name| Field::new(*name, DataType::Utf8, true))
1487 .collect();
1488 let mut result_columns: Vec<ArrayRef> = Vec::new();
1489
1490 for col_idx in 0..group_by.len() {
1492 let vals: Vec<Option<&str>> = groups
1493 .iter()
1494 .map(|(key, _)| key[col_idx].as_deref())
1495 .collect();
1496 let arr: StringArray = vals.into_iter().collect();
1497 result_columns.push(Arc::new(arr));
1498 }
1499
1500 for (fi, &field_name) in field_columns.iter().enumerate() {
1502 for func in functions {
1503 let name = format!("{field_name}_{}", agg_fn_name(*func));
1504 let vals: Vec<AggResult> = (0..num_groups)
1505 .map(|gi| groups[gi].1[fi].finalize(*func))
1506 .collect();
1507 let (field, array) = agg_result_column(&name, &vals)?;
1508 result_fields.push(field);
1509 result_columns.push(array);
1510 }
1511 }
1512
1513 let schema = Arc::new(Schema::new(result_fields));
1514 RecordBatch::try_new(schema, result_columns).map_err(Into::into)
1515}
1516
1517impl UngroupedState {
1518 #[allow(clippy::cast_precision_loss)]
1522 fn push(&mut self, batch: &RecordBatch, field_columns: &[&str]) {
1523 let ts_col = batch
1524 .column_by_name(chronix_core::TIME_COLUMN)
1525 .and_then(|c| c.as_any().downcast_ref::<Int64Array>());
1526
1527 for (fi, &field_name) in field_columns.iter().enumerate() {
1528 let Some(col) = batch.column_by_name(field_name) else {
1529 continue;
1530 };
1531
1532 self.counts[fi] += (col.len() - col.null_count()) as u64;
1534
1535 let typed = TypedColumn::from_array(col.as_ref());
1537 match typed {
1538 TypedColumn::Float64(a) => {
1539 if let Some(s) = arrow_agg::sum(a) {
1540 self.sums[fi] += s;
1541 }
1542 if let Some(m) = arrow_agg::min(a) {
1543 if m < self.mins[fi] {
1544 self.mins[fi] = m;
1545 }
1546 }
1547 if let Some(m) = arrow_agg::max(a) {
1548 if m > self.maxs[fi] {
1549 self.maxs[fi] = m;
1550 }
1551 }
1552 }
1553 TypedColumn::Int64(a) => {
1554 if let Some(s) = arrow_agg::sum(a) {
1555 self.sums[fi] += s as f64;
1556 }
1557 if let Some(m) = arrow_agg::min(a) {
1558 if (m as f64) < self.mins[fi] {
1559 self.mins[fi] = m as f64;
1560 }
1561 }
1562 if let Some(m) = arrow_agg::max(a) {
1563 if (m as f64) > self.maxs[fi] {
1564 self.maxs[fi] = m as f64;
1565 }
1566 }
1567 }
1568 TypedColumn::UInt64(a) => {
1569 if let Some(s) = arrow_agg::sum(a) {
1570 self.sums[fi] += s as f64;
1571 }
1572 if let Some(m) = arrow_agg::min(a) {
1573 if (m as f64) < self.mins[fi] {
1574 self.mins[fi] = m as f64;
1575 }
1576 }
1577 if let Some(m) = arrow_agg::max(a) {
1578 if (m as f64) > self.maxs[fi] {
1579 self.maxs[fi] = m as f64;
1580 }
1581 }
1582 }
1583 TypedColumn::Decimal(a, scale) => {
1584 let track =
1588 self.decimals[fi].get_or_insert_with(|| Box::new(DecimalTrack::new(scale)));
1589 for i in 0..a.len() {
1590 if a.is_null(i) {
1591 continue;
1592 }
1593 match track.align(a.value(i), scale) {
1594 Some(m) => {
1595 match track.sum.checked_add(m) {
1596 Some(sum) => track.sum = sum,
1597 None => track.overflow = true,
1598 }
1599 if m < track.min {
1600 track.min = m;
1601 }
1602 if m > track.max {
1603 track.max = m;
1604 }
1605 }
1606 None => track.overflow = true,
1607 }
1608 }
1609 }
1610 TypedColumn::Other => {}
1611 }
1612
1613 if self.needs_first_last {
1617 if let Some(ts) = ts_col {
1618 let n = ts.len().min(col.len());
1619 for i in 0..n {
1620 if ts.is_null(i) || col.is_null(i) {
1621 continue;
1622 }
1623 let t = ts.value(i);
1624 if let Some(v) = typed.value_num(i) {
1625 let first = t < self.first_ts[fi] || !self.first_set[fi];
1626 let last = t >= self.last_ts[fi] || !self.last_set[fi];
1628 if first {
1629 self.first_ts[fi] = t;
1630 self.first_set[fi] = true;
1631 }
1632 if last {
1633 self.last_ts[fi] = t;
1634 self.last_set[fi] = true;
1635 }
1636 match v {
1637 Num::F64(x) => {
1638 if first {
1639 self.first_val[fi] = x;
1640 }
1641 if last {
1642 self.last_val[fi] = x;
1643 }
1644 }
1645 Num::Dec(mantissa, scale) => {
1646 let track = self.decimals[fi]
1647 .get_or_insert_with(|| Box::new(DecimalTrack::new(scale)));
1648 if let Some(m) = track.align(mantissa, scale) {
1649 if first {
1650 track.first = m;
1651 }
1652 if last {
1653 track.last = m;
1654 }
1655 }
1656 }
1657 }
1658 }
1659 }
1660 }
1661 }
1662 }
1663 }
1664
1665 #[allow(clippy::cast_precision_loss)]
1667 fn finalize(&self, fi: usize, func: AggFn) -> AggResult {
1668 if self.counts[fi] == 0 {
1669 return if func == AggFn::Count {
1670 AggResult::F64(0.0)
1671 } else {
1672 AggResult::Null
1673 };
1674 }
1675 if func == AggFn::Count {
1677 return AggResult::F64(self.counts[fi] as f64);
1678 }
1679 if let Some(track) = self.decimals[fi].as_deref() {
1680 return match func {
1681 AggFn::Sum => track.sum_result(),
1682 AggFn::Avg => track.avg_result(self.counts[fi]),
1683 AggFn::Min => track.at(track.min),
1684 AggFn::Max => track.at(track.max),
1685 AggFn::First => {
1686 if self.first_set[fi] {
1687 track.at(track.first)
1688 } else {
1689 AggResult::Null
1690 }
1691 }
1692 AggFn::Last => {
1693 if self.last_set[fi] {
1694 track.at(track.last)
1695 } else {
1696 AggResult::Null
1697 }
1698 }
1699 AggFn::Count => unreachable!("handled above"),
1700 };
1701 }
1702 match func {
1703 AggFn::Sum => AggResult::F64(self.sums[fi]),
1704 AggFn::Min => AggResult::F64(self.mins[fi]),
1705 AggFn::Max => AggResult::F64(self.maxs[fi]),
1706 AggFn::Avg => AggResult::F64(self.sums[fi] / self.counts[fi] as f64),
1707 AggFn::First => {
1708 if self.first_set[fi] {
1709 AggResult::F64(self.first_val[fi])
1710 } else {
1711 AggResult::Null
1712 }
1713 }
1714 AggFn::Last => {
1715 if self.last_set[fi] {
1716 AggResult::F64(self.last_val[fi])
1717 } else {
1718 AggResult::Null
1719 }
1720 }
1721 AggFn::Count => unreachable!("handled above"),
1722 }
1723 }
1724
1725 #[allow(clippy::cast_precision_loss)]
1726 fn finish(&self, functions: &[AggFn], field_columns: &[&str]) -> Result<RecordBatch> {
1727 let mut result_fields = Vec::new();
1728 let mut result_columns: Vec<ArrayRef> = Vec::new();
1729 for (fi, &field_name) in field_columns.iter().enumerate() {
1730 for func in functions {
1731 let name = format!("{field_name}_{}", agg_fn_name(*func));
1732 let value = self.finalize(fi, *func);
1733 let (field, array) = agg_result_column(&name, &[value])?;
1734 result_fields.push(field);
1735 result_columns.push(array);
1736 }
1737 }
1738
1739 let schema = Arc::new(Schema::new(result_fields));
1740 RecordBatch::try_new(schema, result_columns).map_err(Into::into)
1741 }
1742}
1743
1744#[cfg(test)]
1745mod tests {
1746 use super::*;
1747
1748 #[test]
1751 fn first_and_last_over_a_decimal_column_without_timestamps() {
1752 let col: ArrayRef = Arc::new(
1757 Decimal128Array::from(vec![Some(111_i128), None, Some(333)])
1758 .with_precision_and_scale(38, 2)
1759 .unwrap(),
1760 );
1761 assert_eq!(
1762 aggregate_column_ts(col.as_ref(), AggFn::First, None),
1763 AggResult::Decimal {
1764 mantissa: 111,
1765 scale: 2
1766 }
1767 );
1768 assert_eq!(
1769 aggregate_column_ts(col.as_ref(), AggFn::Last, None),
1770 AggResult::Decimal {
1771 mantissa: 333,
1772 scale: 2
1773 }
1774 );
1775 let empty: ArrayRef = Arc::new(
1777 Decimal128Array::from(vec![None::<i128>, None])
1778 .with_precision_and_scale(38, 2)
1779 .unwrap(),
1780 );
1781 assert_eq!(
1782 aggregate_column_ts(empty.as_ref(), AggFn::First, None),
1783 AggResult::Null
1784 );
1785 }
1786
1787 #[test]
1788 fn a_decimal_sum_that_overflows_38_digits_is_null_not_wrapped() {
1789 let widest = 99_999_999_999_999_999_999_999_999_999_999_999_999_i128;
1791 let col: ArrayRef = Arc::new(
1792 Decimal128Array::from(vec![Some(widest), Some(widest)])
1793 .with_precision_and_scale(38, 0)
1794 .unwrap(),
1795 );
1796 assert_eq!(
1797 aggregate_column_ts(col.as_ref(), AggFn::Sum, None),
1798 AggResult::Null
1799 );
1800 assert_eq!(
1802 aggregate_column_ts(col.as_ref(), AggFn::Max, None),
1803 AggResult::Decimal {
1804 mantissa: widest,
1805 scale: 0
1806 }
1807 );
1808 }
1809
1810 #[test]
1811 fn a_decimal_column_at_two_scales_folds_at_the_wider_one() {
1812 let mut acc = IncrementalAccumulator::new();
1816 acc.update(Num::Dec(150, 2), Some(1)); acc.update(Num::Dec(15_000, 4), Some(2)); assert_eq!(
1819 acc.finalize(AggFn::Sum),
1820 AggResult::Decimal {
1821 mantissa: 30_000,
1822 scale: 4
1823 }
1824 );
1825 let mut acc = IncrementalAccumulator::new();
1827 acc.update(Num::Dec(15_000, 4), Some(1));
1828 acc.update(Num::Dec(150, 2), Some(2));
1829 assert_eq!(
1830 acc.finalize(AggFn::Sum),
1831 AggResult::Decimal {
1832 mantissa: 30_000,
1833 scale: 4
1834 }
1835 );
1836 }
1837
1838 #[test]
1839 fn the_average_of_decimals_rounds_half_away_from_zero() {
1840 let mut acc = IncrementalAccumulator::new();
1841 for m in [100_i128, 100, 200] {
1842 acc.update(Num::Dec(m, 2), Some(1));
1843 }
1844 assert_eq!(
1846 acc.finalize(AggFn::Avg),
1847 AggResult::Decimal {
1848 mantissa: 133_333_333,
1849 scale: 8
1850 }
1851 );
1852 let mut acc = IncrementalAccumulator::new();
1854 for m in [-100_i128, -100, -200] {
1855 acc.update(Num::Dec(m, 2), Some(1));
1856 }
1857 assert_eq!(
1858 acc.finalize(AggFn::Avg),
1859 AggResult::Decimal {
1860 mantissa: -133_333_333,
1861 scale: 8
1862 }
1863 );
1864 }
1865
1866 #[test]
1867 fn a_count_of_decimals_is_a_float_column_not_a_decimal_one() {
1868 let results = [AggResult::F64(3.0)];
1869 let (field, _) = agg_result_column("v_count", &results).unwrap();
1870 assert_eq!(field.data_type(), &DataType::Float64);
1871 let results = [AggResult::Decimal {
1873 mantissa: 30,
1874 scale: 2,
1875 }];
1876 let (field, array) = agg_result_column("v_sum", &results).unwrap();
1877 assert_eq!(field.data_type(), &DataType::Decimal128(38, 2));
1878 assert_eq!(array.len(), 1);
1879 }
1880
1881 fn test_batch() -> RecordBatch {
1882 let schema = Arc::new(Schema::new(vec![
1883 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
1884 Field::new("host", DataType::Utf8, false),
1885 Field::new("value", DataType::Float64, false),
1886 ]));
1887
1888 let timestamps = Arc::new(Int64Array::from(vec![100, 200, 300, 400, 500]));
1889 let hosts = Arc::new(StringArray::from(vec!["a", "a", "b", "b", "a"]));
1890 let values = Arc::new(Float64Array::from(vec![10.0, 20.0, 30.0, 40.0, 50.0]));
1891
1892 RecordBatch::try_new(schema, vec![timestamps, hosts, values]).unwrap()
1893 }
1894
1895 #[test]
1896 fn aggregate_count() {
1897 let batch = test_batch();
1898 let result = aggregate_batch(&batch, &[AggFn::Count], &["value"], None).unwrap();
1899 assert_eq!(result.num_rows(), 1);
1900 let col = result
1901 .column(0)
1902 .as_any()
1903 .downcast_ref::<Float64Array>()
1904 .unwrap();
1905 assert!((col.value(0) - 5.0).abs() < f64::EPSILON);
1906 }
1907
1908 #[test]
1909 fn aggregate_sum() {
1910 let batch = test_batch();
1911 let result = aggregate_batch(&batch, &[AggFn::Sum], &["value"], None).unwrap();
1912 let col = result
1913 .column(0)
1914 .as_any()
1915 .downcast_ref::<Float64Array>()
1916 .unwrap();
1917 assert!((col.value(0) - 150.0).abs() < f64::EPSILON);
1918 }
1919
1920 #[test]
1921 fn aggregate_min_max() {
1922 let batch = test_batch();
1923 let result = aggregate_batch(&batch, &[AggFn::Min, AggFn::Max], &["value"], None).unwrap();
1924 let min_col = result
1925 .column(0)
1926 .as_any()
1927 .downcast_ref::<Float64Array>()
1928 .unwrap();
1929 let max_col = result
1930 .column(1)
1931 .as_any()
1932 .downcast_ref::<Float64Array>()
1933 .unwrap();
1934 assert!((min_col.value(0) - 10.0).abs() < f64::EPSILON);
1935 assert!((max_col.value(0) - 50.0).abs() < f64::EPSILON);
1936 }
1937
1938 #[test]
1939 fn aggregate_avg() {
1940 let batch = test_batch();
1941 let result = aggregate_batch(&batch, &[AggFn::Avg], &["value"], None).unwrap();
1942 let col = result
1943 .column(0)
1944 .as_any()
1945 .downcast_ref::<Float64Array>()
1946 .unwrap();
1947 assert!((col.value(0) - 30.0).abs() < f64::EPSILON);
1948 }
1949
1950 #[test]
1951 fn aggregate_first_last() {
1952 let batch = test_batch();
1953 let result =
1954 aggregate_batch(&batch, &[AggFn::First, AggFn::Last], &["value"], None).unwrap();
1955 let first = result
1956 .column(0)
1957 .as_any()
1958 .downcast_ref::<Float64Array>()
1959 .unwrap();
1960 let last = result
1961 .column(1)
1962 .as_any()
1963 .downcast_ref::<Float64Array>()
1964 .unwrap();
1965 assert!((first.value(0) - 10.0).abs() < f64::EPSILON);
1966 assert!((last.value(0) - 50.0).abs() < f64::EPSILON);
1967 }
1968
1969 #[test]
1970 fn aggregate_grouped_by_host() {
1971 let batch = test_batch();
1972 let result = aggregate_grouped(
1973 &batch,
1974 &[AggFn::Sum, AggFn::Count],
1975 &["value"],
1976 &["host"],
1977 None,
1978 )
1979 .unwrap();
1980
1981 assert_eq!(result.num_rows(), 2); let hosts = result
1984 .column(0)
1985 .as_any()
1986 .downcast_ref::<StringArray>()
1987 .unwrap();
1988 let sums = result
1989 .column(1)
1990 .as_any()
1991 .downcast_ref::<Float64Array>()
1992 .unwrap();
1993 let counts = result
1994 .column(2)
1995 .as_any()
1996 .downcast_ref::<Float64Array>()
1997 .unwrap();
1998
1999 assert_eq!(hosts.value(0), "a");
2001 assert!((sums.value(0) - 80.0).abs() < f64::EPSILON); assert!((counts.value(0) - 3.0).abs() < f64::EPSILON);
2003
2004 assert_eq!(hosts.value(1), "b");
2005 assert!((sums.value(1) - 70.0).abs() < f64::EPSILON); assert!((counts.value(1) - 2.0).abs() < f64::EPSILON);
2007 }
2008
2009 #[test]
2010 fn all_functions_on_known_data() {
2011 let batch = test_batch();
2012 let result = aggregate_batch(
2013 &batch,
2014 &[
2015 AggFn::Count,
2016 AggFn::Sum,
2017 AggFn::Min,
2018 AggFn::Max,
2019 AggFn::Avg,
2020 AggFn::First,
2021 AggFn::Last,
2022 ],
2023 &["value"],
2024 None,
2025 )
2026 .unwrap();
2027
2028 assert_eq!(result.num_columns(), 7);
2029 let cols: Vec<f64> = (0..7)
2030 .map(|i| {
2031 result
2032 .column(i)
2033 .as_any()
2034 .downcast_ref::<Float64Array>()
2035 .unwrap()
2036 .value(0)
2037 })
2038 .collect();
2039
2040 assert!((cols[0] - 5.0).abs() < f64::EPSILON); assert!((cols[1] - 150.0).abs() < f64::EPSILON); assert!((cols[2] - 10.0).abs() < f64::EPSILON); assert!((cols[3] - 50.0).abs() < f64::EPSILON); assert!((cols[4] - 30.0).abs() < f64::EPSILON); assert!((cols[5] - 10.0).abs() < f64::EPSILON); assert!((cols[6] - 50.0).abs() < f64::EPSILON); }
2048
2049 fn u64_batch() -> RecordBatch {
2052 let schema = Arc::new(Schema::new(vec![
2053 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2054 Field::new("count", DataType::UInt64, false),
2055 ]));
2056 let timestamps = Arc::new(Int64Array::from(vec![100, 200, 300, 400, 500]));
2057 let counts = Arc::new(UInt64Array::from(vec![10u64, 20, 30, 40, 50]));
2058 RecordBatch::try_new(schema, vec![timestamps, counts]).unwrap()
2059 }
2060
2061 #[test]
2062 fn aggregate_u64_sum() {
2063 let batch = u64_batch();
2064 let result = aggregate_batch(&batch, &[AggFn::Sum], &["count"], None).unwrap();
2065 let col = result
2066 .column(0)
2067 .as_any()
2068 .downcast_ref::<Float64Array>()
2069 .unwrap();
2070 assert!((col.value(0) - 150.0).abs() < f64::EPSILON);
2071 }
2072
2073 #[test]
2074 fn aggregate_u64_min_max() {
2075 let batch = u64_batch();
2076 let result = aggregate_batch(&batch, &[AggFn::Min, AggFn::Max], &["count"], None).unwrap();
2077 let min_col = result
2078 .column(0)
2079 .as_any()
2080 .downcast_ref::<Float64Array>()
2081 .unwrap();
2082 let max_col = result
2083 .column(1)
2084 .as_any()
2085 .downcast_ref::<Float64Array>()
2086 .unwrap();
2087 assert!((min_col.value(0) - 10.0).abs() < f64::EPSILON);
2088 assert!((max_col.value(0) - 50.0).abs() < f64::EPSILON);
2089 }
2090
2091 #[test]
2092 fn aggregate_u64_avg() {
2093 let batch = u64_batch();
2094 let result = aggregate_batch(&batch, &[AggFn::Avg], &["count"], None).unwrap();
2095 let col = result
2096 .column(0)
2097 .as_any()
2098 .downcast_ref::<Float64Array>()
2099 .unwrap();
2100 assert!((col.value(0) - 30.0).abs() < f64::EPSILON);
2101 }
2102
2103 #[test]
2104 fn aggregate_u64_first_last() {
2105 let batch = u64_batch();
2106 let result =
2107 aggregate_batch(&batch, &[AggFn::First, AggFn::Last], &["count"], None).unwrap();
2108 let first = result
2109 .column(0)
2110 .as_any()
2111 .downcast_ref::<Float64Array>()
2112 .unwrap();
2113 let last = result
2114 .column(1)
2115 .as_any()
2116 .downcast_ref::<Float64Array>()
2117 .unwrap();
2118 assert!((first.value(0) - 10.0).abs() < f64::EPSILON);
2119 assert!((last.value(0) - 50.0).abs() < f64::EPSILON);
2120 }
2121
2122 #[test]
2123 fn aggregate_u64_count() {
2124 let batch = u64_batch();
2125 let result = aggregate_batch(&batch, &[AggFn::Count], &["count"], None).unwrap();
2126 let col = result
2127 .column(0)
2128 .as_any()
2129 .downcast_ref::<Float64Array>()
2130 .unwrap();
2131 assert!((col.value(0) - 5.0).abs() < f64::EPSILON);
2132 }
2133
2134 #[test]
2135 fn aggregate_respects_memory_tracker() {
2136 let batch = test_batch();
2137 let tracker = crate::memory::MemoryTracker::new(1_000_000);
2138 let result = aggregate_batch(&batch, &[AggFn::Sum], &["value"], Some(&tracker)).unwrap();
2139 assert!(tracker.allocated() > 0);
2140 let col = result
2141 .column(0)
2142 .as_any()
2143 .downcast_ref::<Float64Array>()
2144 .unwrap();
2145 assert!((col.value(0) - 150.0).abs() < f64::EPSILON);
2146 }
2147
2148 #[test]
2149 fn aggregate_exceeds_memory_budget() {
2150 let batch = test_batch();
2151 let tracker = crate::memory::MemoryTracker::new(1); let err = aggregate_batch(&batch, &[AggFn::Sum], &["value"], Some(&tracker)).unwrap_err();
2153 assert!(matches!(
2154 err,
2155 crate::error::QueryError::QueryMemoryExceeded { .. }
2156 ));
2157 }
2158
2159 #[test]
2160 fn grouped_aggregate_respects_memory_tracker() {
2161 let batch = test_batch();
2162 let tracker = crate::memory::MemoryTracker::new(1_000_000);
2163 let result =
2164 aggregate_grouped(&batch, &[AggFn::Sum], &["value"], &["host"], Some(&tracker))
2165 .unwrap();
2166 assert!(tracker.allocated() > 0);
2167 assert_eq!(result.num_rows(), 2);
2168 }
2169
2170 #[test]
2173 fn first_last_out_of_order_timestamps() {
2174 let schema = Arc::new(Schema::new(vec![
2175 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2176 Field::new("host", DataType::Utf8, false),
2177 Field::new("value", DataType::Float64, false),
2178 ]));
2179 let ts = Int64Array::from(vec![300, 100, 500, 200, 400]);
2180 let host = StringArray::from(vec!["a", "a", "a", "a", "a"]);
2181 let value = Float64Array::from(vec![3.0, 1.0, 5.0, 2.0, 4.0]);
2182 let batch =
2183 RecordBatch::try_new(schema, vec![Arc::new(ts), Arc::new(host), Arc::new(value)])
2184 .unwrap();
2185
2186 let result =
2187 aggregate_batch(&batch, &[AggFn::First, AggFn::Last], &["value"], None).unwrap();
2188 let first = result
2189 .column(0)
2190 .as_any()
2191 .downcast_ref::<Float64Array>()
2192 .unwrap();
2193 let last = result
2194 .column(1)
2195 .as_any()
2196 .downcast_ref::<Float64Array>()
2197 .unwrap();
2198 assert!(
2200 (first.value(0) - 1.0).abs() < f64::EPSILON,
2201 "First should be 1.0, got {}",
2202 first.value(0)
2203 );
2204 assert!(
2206 (last.value(0) - 5.0).abs() < f64::EPSILON,
2207 "Last should be 5.0, got {}",
2208 last.value(0)
2209 );
2210 }
2211
2212 #[test]
2214 fn first_last_timestamp_ties() {
2215 let schema = Arc::new(Schema::new(vec![
2216 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2217 Field::new("host", DataType::Utf8, false),
2218 Field::new("value", DataType::Float64, false),
2219 ]));
2220 let ts = Int64Array::from(vec![100, 200, 100, 200]);
2222 let host = StringArray::from(vec!["a", "a", "a", "a"]);
2223 let value = Float64Array::from(vec![1.0, 2.0, 3.0, 4.0]);
2224 let batch =
2225 RecordBatch::try_new(schema, vec![Arc::new(ts), Arc::new(host), Arc::new(value)])
2226 .unwrap();
2227
2228 let result =
2229 aggregate_batch(&batch, &[AggFn::First, AggFn::Last], &["value"], None).unwrap();
2230 let first = result
2231 .column(0)
2232 .as_any()
2233 .downcast_ref::<Float64Array>()
2234 .unwrap();
2235 let last = result
2236 .column(1)
2237 .as_any()
2238 .downcast_ref::<Float64Array>()
2239 .unwrap();
2240 assert!((first.value(0) - 1.0).abs() < f64::EPSILON);
2242 assert!((last.value(0) - 4.0).abs() < f64::EPSILON);
2244 }
2245
2246 #[test]
2248 fn grouped_first_last_by_timestamp() {
2249 let schema = Arc::new(Schema::new(vec![
2250 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2251 Field::new("host", DataType::Utf8, false),
2252 Field::new("value", DataType::Float64, false),
2253 ]));
2254 let ts = Int64Array::from(vec![200, 100, 300, 100, 200]);
2257 let host = StringArray::from(vec!["a", "a", "b", "b", "b"]);
2258 let value = Float64Array::from(vec![20.0, 10.0, 30.0, 10.0, 20.0]);
2259 let batch =
2260 RecordBatch::try_new(schema, vec![Arc::new(ts), Arc::new(host), Arc::new(value)])
2261 .unwrap();
2262
2263 let result = aggregate_grouped(
2264 &batch,
2265 &[AggFn::First, AggFn::Last],
2266 &["value"],
2267 &["host"],
2268 None,
2269 )
2270 .unwrap();
2271
2272 let host_col = result.column_by_name("host").unwrap();
2274 let host_arr = host_col.as_any().downcast_ref::<StringArray>().unwrap();
2275 let first_col = result.column_by_name("value_first").unwrap();
2276 let first_arr = first_col.as_any().downcast_ref::<Float64Array>().unwrap();
2277 let last_col = result.column_by_name("value_last").unwrap();
2278 let last_arr = last_col.as_any().downcast_ref::<Float64Array>().unwrap();
2279
2280 for i in 0..result.num_rows() {
2281 match host_arr.value(i) {
2282 "a" => {
2283 assert!(
2284 (first_arr.value(i) - 10.0).abs() < f64::EPSILON,
2285 "host=a first"
2286 );
2287 assert!(
2288 (last_arr.value(i) - 20.0).abs() < f64::EPSILON,
2289 "host=a last"
2290 );
2291 }
2292 "b" => {
2293 assert!(
2294 (first_arr.value(i) - 10.0).abs() < f64::EPSILON,
2295 "host=b first"
2296 );
2297 assert!(
2298 (last_arr.value(i) - 30.0).abs() < f64::EPSILON,
2299 "host=b last"
2300 );
2301 }
2302 other => panic!("unexpected host: {other}"),
2303 }
2304 }
2305 }
2306
2307 #[test]
2309 fn grouped_last_timestamp_tie_breaking() {
2310 let schema = Arc::new(Schema::new(vec![
2311 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2312 Field::new("host", DataType::Utf8, false),
2313 Field::new("value", DataType::Float64, false),
2314 ]));
2315 let ts = Int64Array::from(vec![100, 100]);
2317 let host = StringArray::from(vec!["a", "a"]);
2318 let value = Float64Array::from(vec![1.0, 2.0]);
2319 let batch =
2320 RecordBatch::try_new(schema, vec![Arc::new(ts), Arc::new(host), Arc::new(value)])
2321 .unwrap();
2322
2323 let result =
2324 aggregate_grouped(&batch, &[AggFn::Last], &["value"], &["host"], None).unwrap();
2325
2326 let last_col = result.column_by_name("value_last").unwrap();
2327 let last_arr = last_col.as_any().downcast_ref::<Float64Array>().unwrap();
2328 assert!(
2329 (last_arr.value(0) - 2.0).abs() < f64::EPSILON,
2330 "Last with tie should keep last-encountered (2.0), got {}",
2331 last_arr.value(0)
2332 );
2333 }
2334
2335 #[test]
2337 fn first_last_single_row() {
2338 let schema = Arc::new(Schema::new(vec![
2339 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2340 Field::new("host", DataType::Utf8, false),
2341 Field::new("value", DataType::Float64, false),
2342 ]));
2343 let ts = Int64Array::from(vec![100]);
2344 let host = StringArray::from(vec!["a"]);
2345 let value = Float64Array::from(vec![42.0]);
2346 let batch =
2347 RecordBatch::try_new(schema, vec![Arc::new(ts), Arc::new(host), Arc::new(value)])
2348 .unwrap();
2349
2350 let result =
2351 aggregate_batch(&batch, &[AggFn::First, AggFn::Last], &["value"], None).unwrap();
2352 let first = result
2353 .column(0)
2354 .as_any()
2355 .downcast_ref::<Float64Array>()
2356 .unwrap();
2357 let last = result
2358 .column(1)
2359 .as_any()
2360 .downcast_ref::<Float64Array>()
2361 .unwrap();
2362 assert!((first.value(0) - 42.0).abs() < f64::EPSILON);
2363 assert!((last.value(0) - 42.0).abs() < f64::EPSILON);
2364 }
2365
2366 #[test]
2369 fn sort_aggregate_matches_hash() {
2370 let batch = test_batch();
2371 let hash_result = aggregate_grouped(
2372 &batch,
2373 &[AggFn::Sum, AggFn::Count, AggFn::Min, AggFn::Max, AggFn::Avg],
2374 &["value"],
2375 &["host"],
2376 None,
2377 )
2378 .unwrap();
2379 let sort_result = aggregate_sorted(
2380 &batch,
2381 &[AggFn::Sum, AggFn::Count, AggFn::Min, AggFn::Max, AggFn::Avg],
2382 &["value"],
2383 &["host"],
2384 None,
2385 )
2386 .unwrap();
2387
2388 assert_eq!(hash_result.num_rows(), sort_result.num_rows());
2390
2391 let hash_hosts = hash_result
2393 .column_by_name("host")
2394 .unwrap()
2395 .as_any()
2396 .downcast_ref::<StringArray>()
2397 .unwrap();
2398 let sort_hosts = sort_result
2399 .column_by_name("host")
2400 .unwrap()
2401 .as_any()
2402 .downcast_ref::<StringArray>()
2403 .unwrap();
2404
2405 for host in &["a", "b"] {
2406 let hi = (0..hash_result.num_rows())
2407 .find(|&i| hash_hosts.value(i) == *host)
2408 .unwrap();
2409 let si = (0..sort_result.num_rows())
2410 .find(|&i| sort_hosts.value(i) == *host)
2411 .unwrap();
2412
2413 for col_name in &[
2414 "value_sum",
2415 "value_count",
2416 "value_min",
2417 "value_max",
2418 "value_avg",
2419 ] {
2420 let hv = hash_result
2421 .column_by_name(col_name)
2422 .unwrap()
2423 .as_any()
2424 .downcast_ref::<Float64Array>()
2425 .unwrap()
2426 .value(hi);
2427 let sv = sort_result
2428 .column_by_name(col_name)
2429 .unwrap()
2430 .as_any()
2431 .downcast_ref::<Float64Array>()
2432 .unwrap()
2433 .value(si);
2434 assert!(
2435 (hv - sv).abs() < f64::EPSILON,
2436 "mismatch for host={host} col={col_name}: hash={hv}, sort={sv}"
2437 );
2438 }
2439 }
2440 }
2441
2442 #[test]
2443 fn sort_aggregate_first_last() {
2444 let batch = test_batch();
2445 let result = aggregate_sorted(
2446 &batch,
2447 &[AggFn::First, AggFn::Last],
2448 &["value"],
2449 &["host"],
2450 None,
2451 )
2452 .unwrap();
2453
2454 let host_col = result
2455 .column_by_name("host")
2456 .unwrap()
2457 .as_any()
2458 .downcast_ref::<StringArray>()
2459 .unwrap();
2460 let first_col = result
2461 .column_by_name("value_first")
2462 .unwrap()
2463 .as_any()
2464 .downcast_ref::<Float64Array>()
2465 .unwrap();
2466 let last_col = result
2467 .column_by_name("value_last")
2468 .unwrap()
2469 .as_any()
2470 .downcast_ref::<Float64Array>()
2471 .unwrap();
2472
2473 for i in 0..result.num_rows() {
2474 match host_col.value(i) {
2475 "a" => {
2476 assert!((first_col.value(i) - 10.0).abs() < f64::EPSILON);
2478 assert!((last_col.value(i) - 50.0).abs() < f64::EPSILON);
2479 }
2480 "b" => {
2481 assert!((first_col.value(i) - 30.0).abs() < f64::EPSILON);
2483 assert!((last_col.value(i) - 40.0).abs() < f64::EPSILON);
2484 }
2485 other => panic!("unexpected host: {other}"),
2486 }
2487 }
2488 }
2489
2490 #[test]
2491 fn sort_aggregate_produces_sorted_output() {
2492 let schema = Arc::new(Schema::new(vec![
2493 Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
2494 Field::new("region", DataType::Utf8, false),
2495 Field::new("value", DataType::Float64, false),
2496 ]));
2497 let ts = Int64Array::from(vec![100, 200, 300]);
2498 let regions = StringArray::from(vec!["eu", "us", "ap"]);
2499 let values = Float64Array::from(vec![1.0, 2.0, 3.0]);
2500 let batch = RecordBatch::try_new(
2501 schema,
2502 vec![Arc::new(ts), Arc::new(regions), Arc::new(values)],
2503 )
2504 .unwrap();
2505
2506 let result =
2507 aggregate_sorted(&batch, &[AggFn::Sum], &["value"], &["region"], None).unwrap();
2508
2509 let region_col = result
2510 .column_by_name("region")
2511 .unwrap()
2512 .as_any()
2513 .downcast_ref::<StringArray>()
2514 .unwrap();
2515 let regions: Vec<&str> = (0..result.num_rows())
2517 .map(|i| region_col.value(i))
2518 .collect();
2519 assert_eq!(
2520 regions,
2521 vec!["ap", "eu", "us"],
2522 "output should be sorted by group key"
2523 );
2524 }
2525
2526 #[test]
2527 fn choose_strategy_defaults_to_hash() {
2528 assert_eq!(choose_strategy(None), AggregationStrategy::Hash);
2529 assert_eq!(choose_strategy(Some(2000)), AggregationStrategy::Hash);
2530 }
2531
2532 #[test]
2533 fn choose_strategy_low_cardinality_uses_sort() {
2534 assert_eq!(choose_strategy(Some(5)), AggregationStrategy::Sort);
2535 assert_eq!(choose_strategy(Some(1024)), AggregationStrategy::Sort);
2536 }
2537
2538 #[test]
2539 fn sort_aggregate_no_group_by_delegates() {
2540 let batch = test_batch();
2541 let result = aggregate_sorted(&batch, &[AggFn::Sum], &["value"], &[], None).unwrap();
2542 let col = result
2543 .column(0)
2544 .as_any()
2545 .downcast_ref::<Float64Array>()
2546 .unwrap();
2547 assert!((col.value(0) - 150.0).abs() < f64::EPSILON);
2548 }
2549}