1mod bool;
5mod constant;
6mod decimal;
7mod grouped;
8mod primitive;
9pub(crate) use grouped::PrimitiveGroupedSumEncodingKernel;
10use vortex_error::VortexExpect;
11use vortex_error::VortexResult;
12use vortex_error::vortex_bail;
13use vortex_error::vortex_ensure;
14use vortex_error::vortex_panic;
15use vortex_session::VortexSession;
16use vortex_session::registry::CachedId;
17
18pub(crate) use self::bool::accumulate_bool;
19pub(crate) use self::constant::multiply_constant;
20pub(crate) use self::decimal::accumulate_decimal;
21pub(crate) use self::primitive::accumulate_primitive;
22pub(crate) use self::primitive::sum_float_all;
23pub(crate) use self::primitive::sum_signed_all;
24pub(crate) use self::primitive::sum_unsigned_all;
25use crate::ArrayRef;
26use crate::Canonical;
27use crate::Columnar;
28use crate::ExecutionCtx;
29use crate::aggregate_fn::Accumulator;
30use crate::aggregate_fn::AggregateArgs;
31use crate::aggregate_fn::AggregateFnId;
32use crate::aggregate_fn::AggregateFnVTable;
33use crate::aggregate_fn::DynAccumulator;
34use crate::aggregate_fn::NumericalAggregateOpts;
35use crate::dtype::DType;
36use crate::dtype::DecimalDType;
37use crate::dtype::MAX_PRECISION;
38use crate::dtype::Nullability;
39use crate::dtype::PType;
40use crate::expr::stats::Precision;
41use crate::expr::stats::Stat;
42use crate::expr::stats::StatsProvider;
43use crate::expr::stats::StatsProviderExt;
44use crate::scalar::DecimalValue;
45use crate::scalar::Scalar;
46
47pub fn sum(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Scalar> {
51 if let Precision::Exact(sum_scalar) = array.statistics().get(Stat::Sum) {
53 return Ok(sum_scalar);
54 }
55
56 let mut acc = Accumulator::try_new(
59 Sum,
60 NumericalAggregateOpts::default(),
61 array.dtype().clone(),
62 )?;
63 acc.accumulate(array, ctx)?;
64 acc.finish()
66}
67
68#[derive(Clone, Debug)]
76pub struct Sum;
77
78pub(crate) fn sum_decimal_dtype(input: &DecimalDType) -> DecimalDType {
82 DecimalDType::new(
83 u8::min(MAX_PRECISION, input.precision() + 10),
84 input.scale(),
85 )
86}
87
88impl AggregateFnVTable for Sum {
89 type Options = NumericalAggregateOpts;
90 type Partial = SumPartial;
91
92 fn id(&self) -> AggregateFnId {
93 static ID: CachedId = CachedId::new("vortex.sum");
94 *ID
95 }
96
97 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
98 Ok(Some(options.serialize()))
99 }
100
101 fn deserialize(
102 &self,
103 metadata: &[u8],
104 _session: &VortexSession,
105 ) -> VortexResult<Self::Options> {
106 NumericalAggregateOpts::deserialize(metadata)
107 }
108
109 fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
110 use Nullability::Nullable;
113
114 Some(match input_dtype {
115 DType::Bool(_) => DType::Primitive(PType::U64, Nullable),
116 DType::Primitive(ptype, _) => match ptype {
117 PType::U8 | PType::U16 | PType::U32 | PType::U64 => {
118 DType::Primitive(PType::U64, Nullable)
119 }
120 PType::I8 | PType::I16 | PType::I32 | PType::I64 => {
121 DType::Primitive(PType::I64, Nullable)
122 }
123 PType::F16 | PType::F32 | PType::F64 => {
124 DType::Primitive(PType::F64, Nullable)
126 }
127 },
128 DType::Decimal(decimal_dtype, _) => {
129 DType::Decimal(sum_decimal_dtype(decimal_dtype), Nullable)
130 }
131 _ => return None,
133 })
134 }
135
136 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
137 self.return_dtype(options, input_dtype)
138 }
139
140 fn empty_partial(&self, args: AggregateArgs<'_, Self::Options>) -> VortexResult<Self::Partial> {
141 Ok(SumPartial {
142 current: Some(make_zero_state(args.return_dtype)),
143 })
144 }
145
146 fn partial_from_scalar(
147 &self,
148 args: AggregateArgs<'_, Self::Options>,
149 scalar: &Scalar,
150 ) -> VortexResult<Self::Partial> {
151 vortex_ensure!(
152 scalar.dtype().eq_ignore_nullability(args.return_dtype),
153 "Sum partial has dtype {}, expected {}",
154 scalar.dtype(),
155 args.return_dtype
156 );
157 let current = if scalar.is_null() {
159 None
160 } else {
161 Some(sum_state_from_scalar(scalar, args.return_dtype)?)
162 };
163 Ok(SumPartial { current })
164 }
165
166 fn merge_partials(
167 &self,
168 args: AggregateArgs<'_, Self::Options>,
169 mut first: Self::Partial,
170 second: Self::Partial,
171 ) -> VortexResult<Self::Partial> {
172 let overflow = match (first.current.as_mut(), second.current) {
173 (None, _) => false,
175 (Some(_), None) => true,
177 (Some(acc), Some(state)) => checked_add_sum_states(acc, args.return_dtype, &state)?,
178 };
179 if overflow {
180 first.current = None;
181 }
182 Ok(first)
183 }
184
185 fn to_scalar(
186 &self,
187 args: AggregateArgs<'_, Self::Options>,
188 partial: &Self::Partial,
189 ) -> VortexResult<Scalar> {
190 Ok(match &partial.current {
191 None => Scalar::null(args.return_dtype.as_nullable()),
192 Some(SumState::Unsigned(v)) => Scalar::primitive(*v, Nullability::Nullable),
193 Some(SumState::Signed(v)) => Scalar::primitive(*v, Nullability::Nullable),
194 Some(SumState::Float(v)) => Scalar::primitive(*v, Nullability::Nullable),
195 Some(SumState::Decimal(value)) => {
196 let decimal_dtype = *args
197 .return_dtype
198 .as_decimal_opt()
199 .vortex_expect("return dtype must be decimal");
200 Scalar::decimal(*value, decimal_dtype, Nullability::Nullable)
201 }
202 })
203 }
204
205 #[inline]
206 fn is_saturated(
207 &self,
208 _args: AggregateArgs<'_, Self::Options>,
209 partial: &Self::Partial,
210 ) -> bool {
211 match partial.current.as_ref() {
212 None => true,
213 Some(SumState::Float(v)) => v.is_nan(),
214 Some(_) => false,
215 }
216 }
217
218 fn try_accumulate(
219 &self,
220 args: AggregateArgs<'_, Self::Options>,
221 partial: &mut Self::Partial,
222 batch: &ArrayRef,
223 _ctx: &mut ExecutionCtx,
224 ) -> VortexResult<bool> {
225 if args.options.skip_nans || !matches!(partial.current, Some(SumState::Float(_))) {
228 return Ok(false);
229 }
230 match batch.statistics().get_as::<u64>(Stat::NaNCount) {
231 Precision::Exact(0) => {
232 if let Precision::Exact(sum) = batch.statistics().get(Stat::Sum) {
235 let sum = if sum.dtype() == args.return_dtype {
236 sum
237 } else {
238 sum.cast(args.return_dtype)?
239 };
240 merge_sum_result(partial, args.return_dtype, sum)?;
241 return Ok(true);
242 }
243 Ok(false)
244 }
245 Precision::Exact(_) => {
246 if let Some(SumState::Float(acc)) = partial.current.as_mut() {
248 *acc = f64::NAN;
249 }
250 Ok(true)
251 }
252 _ => Ok(false),
253 }
254 }
255
256 fn accumulate(
257 &self,
258 args: AggregateArgs<'_, Self::Options>,
259 partial: &mut Self::Partial,
260 batch: &Columnar,
261 ctx: &mut ExecutionCtx,
262 ) -> VortexResult<()> {
263 if let Columnar::Constant(c) = batch {
264 if args.options.skip_nans && c.scalar().as_primitive_opt().is_some_and(|p| p.is_nan()) {
266 return Ok(());
267 }
268 if let Some(product) = multiply_constant(c.scalar(), c.len(), args.return_dtype)? {
269 merge_sum_result(partial, args.return_dtype, product)?;
270 }
271 return Ok(());
272 }
273
274 let mut inner = match partial.current.take() {
275 Some(inner) => inner,
276 None => return Ok(()),
277 };
278
279 let result = match batch {
280 Columnar::Canonical(c) => match c {
281 Canonical::Primitive(p) => {
282 accumulate_primitive(&mut inner, p, ctx, args.options.skip_nans)
283 }
284 Canonical::Bool(b) => accumulate_bool(&mut inner, b, ctx),
285 Canonical::Decimal(d) => accumulate_decimal(&mut inner, args.return_dtype, d, ctx),
286 _ => vortex_bail!("Unsupported canonical type for sum: {}", batch.dtype()),
287 },
288 Columnar::Constant(_) => unreachable!(),
289 };
290
291 match result {
292 Ok(false) => partial.current = Some(inner),
293 Ok(true) => {} Err(e) => {
295 partial.current = Some(inner);
296 return Err(e);
297 }
298 }
299 Ok(())
300 }
301
302 fn finalize(
303 &self,
304 _args: AggregateArgs<'_, Self::Options>,
305 partials: ArrayRef,
306 ) -> VortexResult<ArrayRef> {
307 Ok(partials)
308 }
309
310 fn finalize_scalar(
311 &self,
312 args: AggregateArgs<'_, Self::Options>,
313 partial: &Self::Partial,
314 ) -> VortexResult<Scalar> {
315 self.to_scalar(args, partial)
316 }
317}
318
319pub struct SumPartial {
321 current: Option<SumState>,
323}
324
325pub enum SumState {
332 Unsigned(u64),
333 Signed(i64),
334 Float(f64),
335 Decimal(DecimalValue),
336}
337
338pub(crate) fn make_zero_state(return_dtype: &DType) -> SumState {
339 match return_dtype {
340 DType::Primitive(ptype, _) => match ptype {
341 PType::U8 | PType::U16 | PType::U32 | PType::U64 => SumState::Unsigned(0),
342 PType::I8 | PType::I16 | PType::I32 | PType::I64 => SumState::Signed(0),
343 PType::F16 | PType::F32 | PType::F64 => SumState::Float(0.0),
344 },
345 DType::Decimal(decimal, _) => SumState::Decimal(DecimalValue::zero(decimal)),
346 _ => vortex_panic!("Unsupported sum type"),
347 }
348}
349
350fn sum_state_from_scalar(scalar: &Scalar, return_dtype: &DType) -> VortexResult<SumState> {
352 Ok(match return_dtype {
353 DType::Primitive(ptype, _) if ptype.is_unsigned_int() => {
354 SumState::Unsigned(u64::try_from(scalar)?)
355 }
356 DType::Primitive(ptype, _) if ptype.is_signed_int() => {
357 SumState::Signed(i64::try_from(scalar)?)
358 }
359 DType::Primitive(..) => SumState::Float(f64::try_from(scalar)?),
360 DType::Decimal(..) => SumState::Decimal(DecimalValue::try_from(scalar)?),
361 _ => vortex_bail!("Unsupported sum type {}", return_dtype),
362 })
363}
364
365pub(crate) fn checked_add_sum_states(
369 state: &mut SumState,
370 return_dtype: &DType,
371 other: &SumState,
372) -> VortexResult<bool> {
373 Ok(match (state, other) {
374 (SumState::Unsigned(acc), SumState::Unsigned(other)) => checked_add_u64(acc, *other),
375 (SumState::Signed(acc), SumState::Signed(other)) => checked_add_i64(acc, *other),
376 (SumState::Float(acc), SumState::Float(other)) => {
377 *acc += *other;
378 false
379 }
380 (SumState::Decimal(value), SumState::Decimal(other)) => {
381 let dtype = return_dtype
382 .as_decimal_opt()
383 .vortex_expect("decimal sum result dtype");
384 match value.checked_add(other) {
385 Some(result) if result.fits_in_precision(*dtype) => {
386 *value = result;
387 false
388 }
389 Some(_) | None => true,
390 }
391 }
392 _ => vortex_bail!("Mismatched sum partial states"),
393 })
394}
395
396fn merge_sum_result(
398 partial: &mut SumPartial,
399 return_dtype: &DType,
400 result: Scalar,
401) -> VortexResult<()> {
402 let overflow = match partial.current.as_mut() {
403 None => return Ok(()),
404 Some(_) if result.is_null() => true,
405 Some(acc) => checked_add_sum_states(
406 acc,
407 return_dtype,
408 &sum_state_from_scalar(&result, return_dtype)?,
409 )?,
410 };
411 if overflow {
412 partial.current = None;
413 }
414 Ok(())
415}
416
417#[allow(clippy::inline_always)]
419#[inline(always)]
420fn checked_add_u64(acc: &mut u64, val: u64) -> bool {
421 match acc.checked_add(val) {
422 Some(r) => {
423 *acc = r;
424 false
425 }
426 None => true,
427 }
428}
429
430#[allow(clippy::inline_always)]
432#[inline(always)]
433fn checked_add_i64(acc: &mut i64, val: i64) -> bool {
434 match acc.checked_add(val) {
435 Some(r) => {
436 *acc = r;
437 false
438 }
439 None => true,
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use vortex_buffer::buffer;
446 use vortex_error::VortexExpect;
447 use vortex_error::VortexResult;
448
449 use crate::ArrayRef;
450 use crate::IntoArray;
451 use crate::VortexSessionExecute;
452 use crate::aggregate_fn::Accumulator;
453 use crate::aggregate_fn::AggregateDTypes;
454 use crate::aggregate_fn::AggregateFnVTable;
455 use crate::aggregate_fn::DynAccumulator;
456 use crate::aggregate_fn::DynGroupedAccumulator;
457 use crate::aggregate_fn::GroupedAccumulator;
458 use crate::aggregate_fn::NumericalAggregateOpts;
459 use crate::aggregate_fn::fns::sum::Sum;
460 use crate::aggregate_fn::fns::sum::SumPartial;
461 use crate::aggregate_fn::fns::sum::SumState;
462 use crate::aggregate_fn::fns::sum::sum;
463 use crate::array_session;
464 use crate::arrays::BoolArray;
465 use crate::arrays::ChunkedArray;
466 use crate::arrays::ConstantArray;
467 use crate::arrays::DecimalArray;
468 use crate::arrays::FixedSizeListArray;
469 use crate::arrays::ListViewArray;
470 use crate::arrays::PrimitiveArray;
471 use crate::assert_arrays_eq;
472 use crate::dtype::DType;
473 use crate::dtype::DecimalDType;
474 use crate::dtype::Nullability;
475 use crate::dtype::Nullability::Nullable;
476 use crate::dtype::PType;
477 use crate::dtype::i256;
478 use crate::expr::stats::Precision;
479 use crate::expr::stats::Stat;
480 use crate::expr::stats::StatsProvider;
481 use crate::scalar::DecimalValue;
482 use crate::scalar::Scalar;
483 use crate::validity::Validity;
484
485 #[test]
488 fn sum_multi_batch() -> VortexResult<()> {
489 let mut ctx = array_session().create_execution_ctx();
490 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
491 let mut acc = Accumulator::try_new(Sum, NumericalAggregateOpts::default(), dtype)?;
492
493 let batch1 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
494 acc.accumulate(&batch1, &mut ctx)?;
495
496 let batch2 = PrimitiveArray::new(buffer![3i32, 6, 9], Validity::NonNullable).into_array();
497 acc.accumulate(&batch2, &mut ctx)?;
498
499 let result = acc.finish()?;
500 assert_eq!(result.as_primitive().typed_value::<i64>(), Some(48));
501 Ok(())
502 }
503
504 #[test]
505 fn sum_finish_resets_state() -> VortexResult<()> {
506 let mut ctx = array_session().create_execution_ctx();
507 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
508 let mut acc = Accumulator::try_new(Sum, NumericalAggregateOpts::default(), dtype)?;
509
510 let batch1 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
511 acc.accumulate(&batch1, &mut ctx)?;
512 let result1 = acc.finish()?;
513 assert_eq!(result1.as_primitive().typed_value::<i64>(), Some(30));
514
515 let batch2 = PrimitiveArray::new(buffer![3i32, 6, 9], Validity::NonNullable).into_array();
516 acc.accumulate(&batch2, &mut ctx)?;
517 let result2 = acc.finish()?;
518 assert_eq!(result2.as_primitive().typed_value::<i64>(), Some(18));
519 Ok(())
520 }
521
522 #[test]
525 fn sum_state_merge() -> VortexResult<()> {
526 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
527 let options = NumericalAggregateOpts::default();
528 let owned = AggregateDTypes::try_new(&Sum, &options, dtype)?;
529 let args = owned.args(&options);
530
531 let partial_of = |value: i64| SumPartial {
532 current: Some(SumState::Signed(value)),
533 };
534 let state = Sum.merge_partials(args, partial_of(100), partial_of(50))?;
535
536 let result = Sum.to_scalar(args, &state)?;
537 assert_eq!(result.as_primitive().typed_value::<i64>(), Some(150));
538 Ok(())
539 }
540
541 #[test]
542 fn sum_overflowed_partial_poisons_reduction() -> VortexResult<()> {
543 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
544 let options = NumericalAggregateOpts::default();
545 let owned = AggregateDTypes::try_new(&Sum, &options, dtype)?;
546 let args = owned.args(&options);
547
548 let overflowed =
549 Sum.partial_from_scalar(args, &Scalar::null(DType::Primitive(PType::I64, Nullable)))?;
550 let five = Sum.partial_from_scalar(args, &Scalar::primitive(5i64, Nullable))?;
551 let state = Sum.merge_partials(args, five, overflowed)?;
552
553 assert!(Sum.is_saturated(args, &state));
554 assert!(Sum.to_scalar(args, &state)?.is_null());
555 Ok(())
556 }
557
558 #[test]
561 fn sum_stats() -> VortexResult<()> {
562 let array = ChunkedArray::try_new(
563 vec![
564 PrimitiveArray::from_iter([1, 1, 1]).into_array(),
565 PrimitiveArray::from_iter([2, 2, 2]).into_array(),
566 ],
567 DType::Primitive(PType::I32, Nullability::NonNullable),
568 )
569 .vortex_expect("operation should succeed in test");
570 let array = array.into_array();
571 let mut ctx = array_session().create_execution_ctx();
572 let mut acc = Accumulator::try_new(
573 Sum,
574 NumericalAggregateOpts::default(),
575 array.dtype().clone(),
576 )?;
577 acc.combine_partials(&Scalar::primitive(2i64, Nullable))?;
578 acc.accumulate(&array, &mut ctx)?;
579 assert_eq!(acc.finish()?, Scalar::primitive(11i64, Nullable));
580 assert_eq!(array.statistics().get(Stat::Sum), Precision::exact(9i64));
581
582 let sum_without_acc = sum(&array, &mut array_session().create_execution_ctx())?;
583 assert_eq!(sum_without_acc, Scalar::primitive(9i64, Nullable));
584 Ok(())
585 }
586
587 #[test]
590 fn sum_constant_float_non_multiply() -> VortexResult<()> {
591 let acc = -2048669276050936500000000000f64;
592 let array = ConstantArray::new(6.1811675e16f64, 25);
593 let array = array.into_array();
594 let mut accumulator = Accumulator::try_new(
595 Sum,
596 NumericalAggregateOpts::default(),
597 array.dtype().clone(),
598 )?;
599 accumulator.combine_partials(&Scalar::primitive(acc, Nullable))?;
600 accumulator.accumulate(&array, &mut array_session().create_execution_ctx())?;
601 let result = accumulator.finish()?;
602 assert_eq!(
603 f64::try_from(&result).vortex_expect("operation should succeed in test"),
604 -2048669274505644600000000000f64
605 );
606 Ok(())
607 }
608
609 fn run_grouped_sum(groups: &ArrayRef, elem_dtype: &DType) -> VortexResult<ArrayRef> {
612 let mut acc = GroupedAccumulator::try_new(
613 Sum,
614 NumericalAggregateOpts::default(),
615 elem_dtype.clone(),
616 )?;
617 acc.accumulate_list(groups, &mut array_session().create_execution_ctx())?;
618 acc.finish()
619 }
620
621 #[test]
622 fn grouped_sum_fixed_size_list() -> VortexResult<()> {
623 let mut ctx = array_session().create_execution_ctx();
624 let elements =
625 PrimitiveArray::new(buffer![1i32, 2, 3, 4, 5, 6], Validity::NonNullable).into_array();
626 let groups = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 2)?;
627
628 let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
629 let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
630
631 let expected = PrimitiveArray::from_option_iter([Some(6i64), Some(15i64)]).into_array();
632 assert_arrays_eq!(&result, &expected, &mut ctx);
633 Ok(())
634 }
635
636 #[test]
637 fn grouped_sum_with_null_elements() -> VortexResult<()> {
638 let mut ctx = array_session().create_execution_ctx();
639 let elements =
640 PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None, Some(5), Some(6)])
641 .into_array();
642 let groups = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 2)?;
643
644 let elem_dtype = DType::Primitive(PType::I32, Nullable);
645 let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
646
647 let expected = PrimitiveArray::from_option_iter([Some(4i64), Some(11i64)]).into_array();
648 assert_arrays_eq!(&result, &expected, &mut ctx);
649 Ok(())
650 }
651
652 #[test]
653 fn grouped_sum_with_null_group() -> VortexResult<()> {
654 let mut ctx = array_session().create_execution_ctx();
655 let elements =
656 PrimitiveArray::new(buffer![1i32, 2, 3, 4, 5, 6, 7, 8, 9], Validity::NonNullable)
657 .into_array();
658 let validity = Validity::from_iter([true, false, true]);
659 let groups = FixedSizeListArray::try_new(elements, 3, validity, 3)?;
660
661 let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
662 let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
663
664 let expected =
665 PrimitiveArray::from_option_iter([Some(6i64), None, Some(24i64)]).into_array();
666 assert_arrays_eq!(&result, &expected, &mut ctx);
667 Ok(())
668 }
669
670 #[test]
671 fn grouped_sum_all_null_elements_in_group() -> VortexResult<()> {
672 let mut ctx = array_session().create_execution_ctx();
673 let elements =
674 PrimitiveArray::from_option_iter([None::<i32>, None, Some(3), Some(4)]).into_array();
675 let groups = FixedSizeListArray::try_new(elements, 2, Validity::NonNullable, 2)?;
676
677 let elem_dtype = DType::Primitive(PType::I32, Nullable);
678 let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
679
680 let expected = PrimitiveArray::from_option_iter([Some(0i64), Some(7i64)]).into_array();
681 assert_arrays_eq!(&result, &expected, &mut ctx);
682 Ok(())
683 }
684
685 #[test]
686 fn grouped_sum_bool() -> VortexResult<()> {
687 let mut ctx = array_session().create_execution_ctx();
688 let elements: BoolArray = [true, false, true, true, true, true].into_iter().collect();
689 let groups =
690 FixedSizeListArray::try_new(elements.into_array(), 3, Validity::NonNullable, 2)?;
691
692 let elem_dtype = DType::Bool(Nullability::NonNullable);
693 let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
694
695 let expected = PrimitiveArray::from_option_iter([Some(2u64), Some(3u64)]).into_array();
696 assert_arrays_eq!(&result, &expected, &mut ctx);
697 Ok(())
698 }
699
700 #[test]
701 fn grouped_sum_finish_resets() -> VortexResult<()> {
702 let mut ctx = array_session().create_execution_ctx();
703 let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
704 let mut acc =
705 GroupedAccumulator::try_new(Sum, NumericalAggregateOpts::default(), elem_dtype)?;
706
707 let elements1 =
708 PrimitiveArray::new(buffer![1i32, 2, 3, 4], Validity::NonNullable).into_array();
709 let groups1 = FixedSizeListArray::try_new(elements1, 2, Validity::NonNullable, 2)?;
710 acc.accumulate_list(&groups1.into_array(), &mut ctx)?;
711 let result1 = acc.finish()?;
712
713 let expected1 = PrimitiveArray::from_option_iter([Some(3i64), Some(7i64)]).into_array();
714 assert_arrays_eq!(&result1, &expected1, &mut ctx);
715
716 let elements2 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
717 let groups2 = FixedSizeListArray::try_new(elements2, 2, Validity::NonNullable, 1)?;
718 acc.accumulate_list(&groups2.into_array(), &mut ctx)?;
719 let result2 = acc.finish()?;
720
721 let expected2 = PrimitiveArray::from_option_iter([Some(30i64)]).into_array();
722 assert_arrays_eq!(&result2, &expected2, &mut ctx);
723 Ok(())
724 }
725
726 #[test]
727 fn grouped_sum_listview_out_of_order_offsets_with_null_group() -> VortexResult<()> {
728 let mut ctx = array_session().create_execution_ctx();
729 let elements =
730 PrimitiveArray::new(buffer![100i32, 200, 300], Validity::NonNullable).into_array();
731 let offsets = PrimitiveArray::new(buffer![2i32, 0, 1], Validity::NonNullable).into_array();
732 let sizes = PrimitiveArray::new(buffer![1i32, 1, 1], Validity::NonNullable).into_array();
733 let validity = Validity::from_iter([true, false, true]);
734 let groups = ListViewArray::try_new(elements, offsets, sizes, validity)?.into_array();
735
736 let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
737 let result = run_grouped_sum(&groups, &elem_dtype)?;
738
739 let expected =
741 PrimitiveArray::from_option_iter([Some(300i64), None, Some(200i64)]).into_array();
742 assert_arrays_eq!(&result, &expected, &mut ctx);
743 Ok(())
744 }
745
746 #[test]
749 fn sum_chunked_floats_with_nulls() -> VortexResult<()> {
750 let chunk1 =
751 PrimitiveArray::from_option_iter(vec![Some(1.5f64), None, Some(3.2), Some(4.8)]);
752 let chunk2 = PrimitiveArray::from_option_iter(vec![Some(2.1f64), Some(5.7), None]);
753 let chunk3 = PrimitiveArray::from_option_iter(vec![None, Some(1.0f64), Some(2.5), None]);
754 let dtype = chunk1.dtype().clone();
755 let chunked = ChunkedArray::try_new(
756 vec![
757 chunk1.into_array(),
758 chunk2.into_array(),
759 chunk3.into_array(),
760 ],
761 dtype,
762 )?;
763
764 let result = sum(
765 &chunked.into_array(),
766 &mut array_session().create_execution_ctx(),
767 )?;
768 assert_eq!(result.as_primitive().as_::<f64>(), Some(20.8));
769 Ok(())
770 }
771
772 #[test]
773 fn sum_chunked_floats_all_nulls_is_zero() -> VortexResult<()> {
774 let chunk1 = PrimitiveArray::from_option_iter::<f32, _>(vec![None, None, None]);
775 let chunk2 = PrimitiveArray::from_option_iter::<f32, _>(vec![None, None]);
776 let dtype = chunk1.dtype().clone();
777 let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
778 let result = sum(
779 &chunked.into_array(),
780 &mut array_session().create_execution_ctx(),
781 )?;
782 assert_eq!(result, Scalar::primitive(0f64, Nullable));
783 Ok(())
784 }
785
786 #[test]
787 fn sum_chunked_floats_empty_chunks() -> VortexResult<()> {
788 let chunk1 = PrimitiveArray::from_option_iter(vec![Some(10.5f64), Some(20.3)]);
789 let chunk2 = ConstantArray::new(Scalar::primitive(0f64, Nullable), 0);
790 let chunk3 = PrimitiveArray::from_option_iter(vec![Some(5.2f64)]);
791 let dtype = chunk1.dtype().clone();
792 let chunked = ChunkedArray::try_new(
793 vec![
794 chunk1.into_array(),
795 chunk2.into_array(),
796 chunk3.into_array(),
797 ],
798 dtype,
799 )?;
800
801 let result = sum(
802 &chunked.into_array(),
803 &mut array_session().create_execution_ctx(),
804 )?;
805 assert_eq!(result.as_primitive().as_::<f64>(), Some(36.0));
806 Ok(())
807 }
808
809 #[test]
810 fn sum_chunked_int_almost_all_null() -> VortexResult<()> {
811 let chunk1 = PrimitiveArray::from_option_iter::<u32, _>(vec![Some(1)]);
812 let chunk2 = PrimitiveArray::from_option_iter::<u32, _>(vec![None]);
813 let dtype = chunk1.dtype().clone();
814 let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
815
816 let result = sum(
817 &chunked.into_array(),
818 &mut array_session().create_execution_ctx(),
819 )?;
820 assert_eq!(result.as_primitive().as_::<u64>(), Some(1));
821 Ok(())
822 }
823
824 #[test]
825 fn sum_chunked_decimals() -> VortexResult<()> {
826 let decimal_dtype = DecimalDType::new(10, 2);
827 let chunk1 = DecimalArray::new(
828 buffer![100i32, 100i32, 100i32, 100i32, 100i32],
829 decimal_dtype,
830 Validity::AllValid,
831 );
832 let chunk2 = DecimalArray::new(
833 buffer![200i32, 200i32, 200i32],
834 decimal_dtype,
835 Validity::AllValid,
836 );
837 let chunk3 = DecimalArray::new(buffer![300i32, 300i32], decimal_dtype, Validity::AllValid);
838 let dtype = chunk1.dtype().clone();
839 let chunked = ChunkedArray::try_new(
840 vec![
841 chunk1.into_array(),
842 chunk2.into_array(),
843 chunk3.into_array(),
844 ],
845 dtype,
846 )?;
847
848 let result = sum(
849 &chunked.into_array(),
850 &mut array_session().create_execution_ctx(),
851 )?;
852 let decimal_result = result.as_decimal();
853 assert_eq!(
854 decimal_result.decimal_value(),
855 Some(DecimalValue::I256(i256::from_i128(1700)))
856 );
857 Ok(())
858 }
859
860 #[test]
861 fn sum_chunked_decimals_with_nulls() -> VortexResult<()> {
862 let decimal_dtype = DecimalDType::new(10, 2);
863 let chunk1 = DecimalArray::new(
864 buffer![100i32, 100i32, 100i32],
865 decimal_dtype,
866 Validity::AllValid,
867 );
868 let chunk2 = DecimalArray::new(
869 buffer![0i32, 0i32],
870 decimal_dtype,
871 Validity::from_iter([false, false]),
872 );
873 let chunk3 = DecimalArray::new(buffer![200i32, 200i32], decimal_dtype, Validity::AllValid);
874 let dtype = chunk1.dtype().clone();
875 let chunked = ChunkedArray::try_new(
876 vec![
877 chunk1.into_array(),
878 chunk2.into_array(),
879 chunk3.into_array(),
880 ],
881 dtype,
882 )?;
883
884 let result = sum(
885 &chunked.into_array(),
886 &mut array_session().create_execution_ctx(),
887 )?;
888 let decimal_result = result.as_decimal();
889 assert_eq!(
890 decimal_result.decimal_value(),
891 Some(DecimalValue::I256(i256::from_i128(700)))
892 );
893 Ok(())
894 }
895
896 #[test]
897 fn sum_chunked_decimals_large() -> VortexResult<()> {
898 let decimal_dtype = DecimalDType::new(3, 0);
899 let chunk1 = ConstantArray::new(
900 Scalar::decimal(
901 DecimalValue::I16(500),
902 decimal_dtype,
903 Nullability::NonNullable,
904 ),
905 1,
906 );
907 let chunk2 = ConstantArray::new(
908 Scalar::decimal(
909 DecimalValue::I16(600),
910 decimal_dtype,
911 Nullability::NonNullable,
912 ),
913 1,
914 );
915 let dtype = chunk1.dtype().clone();
916 let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
917
918 let result = sum(
919 &chunked.into_array(),
920 &mut array_session().create_execution_ctx(),
921 )?;
922 let decimal_result = result.as_decimal();
923 assert_eq!(
924 decimal_result.decimal_value(),
925 Some(DecimalValue::I256(i256::from_i128(1100)))
926 );
927 assert_eq!(
928 result.dtype(),
929 &DType::Decimal(DecimalDType::new(13, 0), Nullable)
930 );
931 Ok(())
932 }
933}