Skip to main content

vortex_array/aggregate_fn/fns/sum/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod 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_err;
14use vortex_error::vortex_panic;
15use vortex_session::VortexSession;
16use vortex_session::registry::CachedId;
17
18use self::bool::accumulate_bool;
19use self::constant::multiply_constant;
20use self::decimal::accumulate_decimal;
21use self::primitive::accumulate_primitive;
22use crate::ArrayRef;
23use crate::Canonical;
24use crate::Columnar;
25use crate::ExecutionCtx;
26use crate::aggregate_fn::Accumulator;
27use crate::aggregate_fn::AggregateFnId;
28use crate::aggregate_fn::AggregateFnVTable;
29use crate::aggregate_fn::DynAccumulator;
30use crate::aggregate_fn::NumericalAggregateOpts;
31use crate::dtype::DType;
32use crate::dtype::DecimalDType;
33use crate::dtype::MAX_PRECISION;
34use crate::dtype::Nullability;
35use crate::dtype::PType;
36use crate::expr::stats::Precision;
37use crate::expr::stats::Stat;
38use crate::expr::stats::StatsProvider;
39use crate::expr::stats::StatsProviderExt;
40use crate::scalar::DecimalValue;
41use crate::scalar::Scalar;
42
43/// Return the sum of an array.
44///
45/// See [`Sum`] for details.
46pub fn sum(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Scalar> {
47    // Short-circuit using cached array statistics.
48    if let Precision::Exact(sum_scalar) = array.statistics().get(Stat::Sum) {
49        return Ok(sum_scalar);
50    }
51
52    // Compute using Accumulator<Sum>.
53    // TODO(ngates): we may want to wrap this three-step dance up into an extension crate maybe.
54    let mut acc = Accumulator::try_new(
55        Sum,
56        NumericalAggregateOpts::default(),
57        array.dtype().clone(),
58    )?;
59    acc.accumulate(array, ctx)?;
60    let result = acc.finish()?;
61
62    // Cache the computed sum as a statistic (only if non-null, i.e. no overflow).
63    if let Some(val) = result.value().cloned() {
64        array.statistics().set(Stat::Sum, Precision::Exact(val));
65    }
66
67    Ok(result)
68}
69
70/// Sum an array, starting from zero.
71///
72/// If the sum overflows, a null scalar will be returned.
73/// If the array is all-invalid, the sum will be zero.
74///
75/// NaN handling for float inputs is controlled by [`NumericalAggregateOpts`]: with `skip_nans` (the
76/// default) NaN values contribute nothing, otherwise any NaN value poisons the sum to NaN.
77#[derive(Clone, Debug)]
78pub struct Sum;
79
80// Both Spark and DataFusion use this heuristic.
81// - https://github.com/apache/spark/blob/fcf636d9eb8d645c24be3db2d599aba2d7e2955a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/Sum.scala#L66
82// - https://github.com/apache/datafusion/blob/4153adf2c0f6e317ef476febfdc834208bd46622/datafusion/functions-aggregate/src/sum.rs#L188
83pub(crate) fn sum_decimal_dtype(input: &DecimalDType) -> DecimalDType {
84    DecimalDType::new(
85        u8::min(MAX_PRECISION, input.precision() + 10),
86        input.scale(),
87    )
88}
89
90impl AggregateFnVTable for Sum {
91    type Options = NumericalAggregateOpts;
92    type Partial = SumPartial;
93
94    fn id(&self) -> AggregateFnId {
95        static ID: CachedId = CachedId::new("vortex.sum");
96        *ID
97    }
98
99    fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
100        Ok(Some(options.serialize()))
101    }
102
103    fn deserialize(
104        &self,
105        metadata: &[u8],
106        _session: &VortexSession,
107    ) -> VortexResult<Self::Options> {
108        NumericalAggregateOpts::deserialize(metadata)
109    }
110
111    fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
112        // When a sum overflows, we return a sum _value_ of null. Therefore, we all return dtypes
113        // are nullable.
114        use Nullability::Nullable;
115
116        Some(match input_dtype {
117            DType::Bool(_) => DType::Primitive(PType::U64, Nullable),
118            DType::Primitive(ptype, _) => match ptype {
119                PType::U8 | PType::U16 | PType::U32 | PType::U64 => {
120                    DType::Primitive(PType::U64, Nullable)
121                }
122                PType::I8 | PType::I16 | PType::I32 | PType::I64 => {
123                    DType::Primitive(PType::I64, Nullable)
124                }
125                PType::F16 | PType::F32 | PType::F64 => {
126                    // Float sums cannot overflow, but all null floats still end up as null
127                    DType::Primitive(PType::F64, Nullable)
128                }
129            },
130            DType::Decimal(decimal_dtype, _) => {
131                DType::Decimal(sum_decimal_dtype(decimal_dtype), Nullable)
132            }
133            // Unsupported types
134            _ => return None,
135        })
136    }
137
138    fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
139        self.return_dtype(options, input_dtype)
140    }
141
142    fn empty_partial(
143        &self,
144        options: &Self::Options,
145        input_dtype: &DType,
146    ) -> VortexResult<Self::Partial> {
147        let return_dtype = self
148            .return_dtype(options, input_dtype)
149            .ok_or_else(|| vortex_err!("Unsupported sum dtype: {}", input_dtype))?;
150        let initial = make_zero_state(&return_dtype);
151
152        Ok(SumPartial {
153            return_dtype,
154            current: Some(initial),
155            skip_nans: options.skip_nans,
156        })
157    }
158
159    fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
160        if other.is_null() {
161            // A null partial means the sub-accumulator saturated (overflow).
162            partial.current = None;
163            return Ok(());
164        }
165        let Some(ref mut inner) = partial.current else {
166            return Ok(());
167        };
168        let saturated = match inner {
169            SumState::Unsigned(acc) => {
170                let val = other
171                    .as_primitive()
172                    .typed_value::<u64>()
173                    .vortex_expect("checked non-null");
174                checked_add_u64(acc, val)
175            }
176            SumState::Signed(acc) => {
177                let val = other
178                    .as_primitive()
179                    .typed_value::<i64>()
180                    .vortex_expect("checked non-null");
181                checked_add_i64(acc, val)
182            }
183            SumState::Float(acc) => {
184                let val = other
185                    .as_primitive()
186                    .typed_value::<f64>()
187                    .vortex_expect("checked non-null");
188                *acc += val;
189                false
190            }
191            SumState::Decimal { value, dtype } => {
192                let val = other
193                    .as_decimal()
194                    .decimal_value()
195                    .vortex_expect("checked non-null");
196                match value.checked_add(&val) {
197                    Some(r) => {
198                        *value = r;
199                        !value.fits_in_precision(*dtype)
200                    }
201                    None => true,
202                }
203            }
204        };
205        if saturated {
206            partial.current = None;
207        }
208        Ok(())
209    }
210
211    fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
212        Ok(match &partial.current {
213            None => Scalar::null(partial.return_dtype.as_nullable()),
214            Some(SumState::Unsigned(v)) => Scalar::primitive(*v, Nullability::Nullable),
215            Some(SumState::Signed(v)) => Scalar::primitive(*v, Nullability::Nullable),
216            Some(SumState::Float(v)) => Scalar::primitive(*v, Nullability::Nullable),
217            Some(SumState::Decimal { value, .. }) => {
218                let decimal_dtype = *partial
219                    .return_dtype
220                    .as_decimal_opt()
221                    .vortex_expect("return dtype must be decimal");
222                Scalar::decimal(*value, decimal_dtype, Nullability::Nullable)
223            }
224        })
225    }
226
227    fn reset(&self, partial: &mut Self::Partial) {
228        partial.current = Some(make_zero_state(&partial.return_dtype));
229    }
230
231    #[inline]
232    fn is_saturated(&self, partial: &Self::Partial) -> bool {
233        match partial.current.as_ref() {
234            None => true,
235            Some(SumState::Float(v)) => v.is_nan(),
236            Some(_) => false,
237        }
238    }
239
240    fn try_accumulate(
241        &self,
242        partial: &mut Self::Partial,
243        batch: &ArrayRef,
244        _ctx: &mut ExecutionCtx,
245    ) -> VortexResult<bool> {
246        // NaN-aware shortcircuits only apply to NaN-including float sums; everything else takes
247        // the default dispatch path.
248        if partial.skip_nans || !matches!(partial.current, Some(SumState::Float(_))) {
249            return Ok(false);
250        }
251        match batch.statistics().get_as::<u64>(Stat::NaNCount) {
252            Precision::Exact(0) => {
253                // NaN-free batch: the cached NaN-skipping sum (if any) equals the
254                // NaN-including sum.
255                if let Precision::Exact(sum) = batch.statistics().get(Stat::Sum) {
256                    let sum = if sum.dtype() == &partial.return_dtype {
257                        sum
258                    } else {
259                        sum.cast(&partial.return_dtype)?
260                    };
261                    self.combine_partials(partial, sum)?;
262                    return Ok(true);
263                }
264                Ok(false)
265            }
266            Precision::Exact(_) => {
267                // At least one NaN value: the sum is NaN without scanning the batch.
268                if let Some(SumState::Float(acc)) = partial.current.as_mut() {
269                    *acc = f64::NAN;
270                }
271                Ok(true)
272            }
273            _ => Ok(false),
274        }
275    }
276
277    fn accumulate(
278        &self,
279        partial: &mut Self::Partial,
280        batch: &Columnar,
281        ctx: &mut ExecutionCtx,
282    ) -> VortexResult<()> {
283        // Constants compute scalar * len and combine via combine_partials.
284        if let Columnar::Constant(c) = batch {
285            // NaN constants are treated as missing when skipping NaNs.
286            if partial.skip_nans && c.scalar().as_primitive_opt().is_some_and(|p| p.is_nan()) {
287                return Ok(());
288            }
289            if let Some(product) = multiply_constant(c.scalar(), c.len(), &partial.return_dtype)? {
290                self.combine_partials(partial, product)?;
291            }
292            return Ok(());
293        }
294
295        let skip_nans = partial.skip_nans;
296        let mut inner = match partial.current.take() {
297            Some(inner) => inner,
298            None => return Ok(()),
299        };
300
301        let result = match batch {
302            Columnar::Canonical(c) => match c {
303                Canonical::Primitive(p) => accumulate_primitive(&mut inner, p, ctx, skip_nans),
304                Canonical::Bool(b) => accumulate_bool(&mut inner, b, ctx),
305                Canonical::Decimal(d) => accumulate_decimal(&mut inner, d, ctx),
306                _ => vortex_bail!("Unsupported canonical type for sum: {}", batch.dtype()),
307            },
308            Columnar::Constant(_) => unreachable!(),
309        };
310
311        match result {
312            Ok(false) => partial.current = Some(inner),
313            Ok(true) => {} // saturated: current stays None
314            Err(e) => {
315                partial.current = Some(inner);
316                return Err(e);
317            }
318        }
319        Ok(())
320    }
321
322    fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
323        Ok(partials)
324    }
325
326    fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
327        self.to_scalar(partial)
328    }
329}
330
331/// The group state for a sum aggregate, containing the accumulated value and configuration
332/// needed for reset/result without external context.
333pub struct SumPartial {
334    return_dtype: DType,
335    /// The current accumulated state, or `None` if saturated (checked overflow).
336    current: Option<SumState>,
337    /// Whether NaN values in float inputs are skipped.
338    skip_nans: bool,
339}
340
341/// The accumulated sum value.
342// TODO(ngates): instead of an enum, we should use a Box<dyn State> to avoid dispatcher over the
343//  input type every time? Perhaps?
344pub enum SumState {
345    Unsigned(u64),
346    Signed(i64),
347    Float(f64),
348    Decimal {
349        value: DecimalValue,
350        dtype: DecimalDType,
351    },
352}
353
354fn make_zero_state(return_dtype: &DType) -> SumState {
355    match return_dtype {
356        DType::Primitive(ptype, _) => match ptype {
357            PType::U8 | PType::U16 | PType::U32 | PType::U64 => SumState::Unsigned(0),
358            PType::I8 | PType::I16 | PType::I32 | PType::I64 => SumState::Signed(0),
359            PType::F16 | PType::F32 | PType::F64 => SumState::Float(0.0),
360        },
361        DType::Decimal(decimal, _) => SumState::Decimal {
362            value: DecimalValue::zero(decimal),
363            dtype: *decimal,
364        },
365        _ => vortex_panic!("Unsupported sum type"),
366    }
367}
368
369/// Checked add for u64, returning true if overflow occurred.
370#[inline(always)]
371fn checked_add_u64(acc: &mut u64, val: u64) -> bool {
372    match acc.checked_add(val) {
373        Some(r) => {
374            *acc = r;
375            false
376        }
377        None => true,
378    }
379}
380
381/// Checked add for i64, returning true if overflow occurred.
382#[inline(always)]
383fn checked_add_i64(acc: &mut i64, val: i64) -> bool {
384    match acc.checked_add(val) {
385        Some(r) => {
386            *acc = r;
387            false
388        }
389        None => true,
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use num_traits::CheckedAdd;
396    use vortex_buffer::buffer;
397    use vortex_error::VortexExpect;
398    use vortex_error::VortexResult;
399
400    use crate::ArrayRef;
401    use crate::IntoArray;
402    use crate::VortexSessionExecute;
403    use crate::aggregate_fn::Accumulator;
404    use crate::aggregate_fn::AggregateFnVTable;
405    use crate::aggregate_fn::DynAccumulator;
406    use crate::aggregate_fn::DynGroupedAccumulator;
407    use crate::aggregate_fn::GroupedAccumulator;
408    use crate::aggregate_fn::NumericalAggregateOpts;
409    use crate::aggregate_fn::fns::sum::Sum;
410    use crate::aggregate_fn::fns::sum::sum;
411    use crate::array_session;
412    use crate::arrays::BoolArray;
413    use crate::arrays::ChunkedArray;
414    use crate::arrays::ConstantArray;
415    use crate::arrays::DecimalArray;
416    use crate::arrays::FixedSizeListArray;
417    use crate::arrays::ListViewArray;
418    use crate::arrays::PrimitiveArray;
419    use crate::assert_arrays_eq;
420    use crate::dtype::DType;
421    use crate::dtype::DecimalDType;
422    use crate::dtype::Nullability;
423    use crate::dtype::Nullability::Nullable;
424    use crate::dtype::PType;
425    use crate::dtype::i256;
426    use crate::expr::stats::Precision;
427    use crate::expr::stats::Stat;
428    use crate::expr::stats::StatsProvider;
429    use crate::scalar::DecimalValue;
430    use crate::scalar::NumericOperator;
431    use crate::scalar::Scalar;
432    use crate::validity::Validity;
433
434    /// Sum an array with an initial value (test-only helper).
435    fn sum_with_accumulator(array: &ArrayRef, accumulator: &Scalar) -> VortexResult<Scalar> {
436        let mut ctx = array_session().create_execution_ctx();
437        if accumulator.is_null() {
438            return Ok(accumulator.clone());
439        }
440        if accumulator.is_zero() == Some(true) {
441            return sum(array, &mut ctx);
442        }
443
444        let sum_dtype = Stat::Sum.dtype(array.dtype()).ok_or_else(|| {
445            vortex_error::vortex_err!("Sum not supported for dtype: {}", array.dtype())
446        })?;
447
448        // For non-float types, try statistics short-circuit with accumulator.
449        if !matches!(&sum_dtype, DType::Primitive(p, _) if p.is_float())
450            && let Precision::Exact(sum_scalar) = array.statistics().get(Stat::Sum)
451        {
452            return add_scalars(&sum_dtype, &sum_scalar, accumulator);
453        }
454
455        // Compute array sum from zero (also caches stats).
456        let array_sum = sum(array, &mut ctx)?;
457
458        // Combine with the accumulator.
459        add_scalars(&sum_dtype, &array_sum, accumulator)
460    }
461
462    /// Add two sum scalars with overflow checking.
463    fn add_scalars(sum_dtype: &DType, lhs: &Scalar, rhs: &Scalar) -> VortexResult<Scalar> {
464        if lhs.is_null() || rhs.is_null() {
465            return Ok(Scalar::null(sum_dtype.as_nullable()));
466        }
467
468        Ok(match sum_dtype {
469            DType::Primitive(ptype, _) if ptype.is_float() => {
470                let lhs_val = f64::try_from(lhs)?;
471                let rhs_val = f64::try_from(rhs)?;
472                Scalar::primitive(lhs_val + rhs_val, Nullable)
473            }
474            DType::Primitive(..) => lhs
475                .as_primitive()
476                .checked_add(&rhs.as_primitive())
477                .map(Scalar::from)
478                .unwrap_or_else(|| Scalar::null(sum_dtype.as_nullable())),
479            DType::Decimal(..) => lhs
480                .as_decimal()
481                .checked_binary_numeric(&rhs.as_decimal(), NumericOperator::Add)
482                .map(Scalar::from)
483                .unwrap_or_else(|| Scalar::null(sum_dtype.as_nullable())),
484            _ => unreachable!("Sum will always be a decimal or a primitive dtype"),
485        })
486    }
487
488    // Multi-batch and reset tests
489
490    #[test]
491    fn sum_multi_batch() -> VortexResult<()> {
492        let mut ctx = array_session().create_execution_ctx();
493        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
494        let mut acc = Accumulator::try_new(Sum, NumericalAggregateOpts::default(), dtype)?;
495
496        let batch1 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
497        acc.accumulate(&batch1, &mut ctx)?;
498
499        let batch2 = PrimitiveArray::new(buffer![3i32, 6, 9], Validity::NonNullable).into_array();
500        acc.accumulate(&batch2, &mut ctx)?;
501
502        let result = acc.finish()?;
503        assert_eq!(result.as_primitive().typed_value::<i64>(), Some(48));
504        Ok(())
505    }
506
507    #[test]
508    fn sum_finish_resets_state() -> VortexResult<()> {
509        let mut ctx = array_session().create_execution_ctx();
510        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
511        let mut acc = Accumulator::try_new(Sum, NumericalAggregateOpts::default(), dtype)?;
512
513        let batch1 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
514        acc.accumulate(&batch1, &mut ctx)?;
515        let result1 = acc.finish()?;
516        assert_eq!(result1.as_primitive().typed_value::<i64>(), Some(30));
517
518        let batch2 = PrimitiveArray::new(buffer![3i32, 6, 9], Validity::NonNullable).into_array();
519        acc.accumulate(&batch2, &mut ctx)?;
520        let result2 = acc.finish()?;
521        assert_eq!(result2.as_primitive().typed_value::<i64>(), Some(18));
522        Ok(())
523    }
524
525    // State merge tests (vtable-level)
526
527    #[test]
528    fn sum_state_merge() -> VortexResult<()> {
529        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
530        let mut state = Sum.empty_partial(&NumericalAggregateOpts::default(), &dtype)?;
531
532        let scalar1 = Scalar::primitive(100i64, Nullable);
533        Sum.combine_partials(&mut state, scalar1)?;
534
535        let scalar2 = Scalar::primitive(50i64, Nullable);
536        Sum.combine_partials(&mut state, scalar2)?;
537
538        let result = Sum.to_scalar(&state)?;
539        Sum.reset(&mut state);
540        assert_eq!(result.as_primitive().typed_value::<i64>(), Some(150));
541        Ok(())
542    }
543
544    // Stats caching test
545
546    #[test]
547    fn sum_stats() -> VortexResult<()> {
548        let array = ChunkedArray::try_new(
549            vec![
550                PrimitiveArray::from_iter([1, 1, 1]).into_array(),
551                PrimitiveArray::from_iter([2, 2, 2]).into_array(),
552            ],
553            DType::Primitive(PType::I32, Nullability::NonNullable),
554        )
555        .vortex_expect("operation should succeed in test");
556        let array = array.into_array();
557        // compute sum with accumulator to populate stats
558        sum_with_accumulator(&array, &Scalar::primitive(2i64, Nullable))?;
559
560        let sum_without_acc = sum(&array, &mut array_session().create_execution_ctx())?;
561        assert_eq!(sum_without_acc, Scalar::primitive(9i64, Nullable));
562        Ok(())
563    }
564
565    // Constant float non-multiply test
566
567    #[test]
568    fn sum_constant_float_non_multiply() -> VortexResult<()> {
569        let acc = -2048669276050936500000000000f64;
570        let array = ConstantArray::new(6.1811675e16f64, 25);
571        let result = sum_with_accumulator(&array.into_array(), &Scalar::primitive(acc, Nullable))
572            .vortex_expect("operation should succeed in test");
573        assert_eq!(
574            f64::try_from(&result).vortex_expect("operation should succeed in test"),
575            -2048669274505644600000000000f64
576        );
577        Ok(())
578    }
579
580    // Grouped sum tests
581
582    fn run_grouped_sum(groups: &ArrayRef, elem_dtype: &DType) -> VortexResult<ArrayRef> {
583        let mut acc = GroupedAccumulator::try_new(
584            Sum,
585            NumericalAggregateOpts::default(),
586            elem_dtype.clone(),
587        )?;
588        acc.accumulate_list(groups, &mut array_session().create_execution_ctx())?;
589        acc.finish()
590    }
591
592    #[test]
593    fn grouped_sum_fixed_size_list() -> VortexResult<()> {
594        let mut ctx = array_session().create_execution_ctx();
595        let elements =
596            PrimitiveArray::new(buffer![1i32, 2, 3, 4, 5, 6], Validity::NonNullable).into_array();
597        let groups = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 2)?;
598
599        let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
600        let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
601
602        let expected = PrimitiveArray::from_option_iter([Some(6i64), Some(15i64)]).into_array();
603        assert_arrays_eq!(&result, &expected, &mut ctx);
604        Ok(())
605    }
606
607    #[test]
608    fn grouped_sum_with_null_elements() -> VortexResult<()> {
609        let mut ctx = array_session().create_execution_ctx();
610        let elements =
611            PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None, Some(5), Some(6)])
612                .into_array();
613        let groups = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 2)?;
614
615        let elem_dtype = DType::Primitive(PType::I32, Nullable);
616        let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
617
618        let expected = PrimitiveArray::from_option_iter([Some(4i64), Some(11i64)]).into_array();
619        assert_arrays_eq!(&result, &expected, &mut ctx);
620        Ok(())
621    }
622
623    #[test]
624    fn grouped_sum_with_null_group() -> VortexResult<()> {
625        let mut ctx = array_session().create_execution_ctx();
626        let elements =
627            PrimitiveArray::new(buffer![1i32, 2, 3, 4, 5, 6, 7, 8, 9], Validity::NonNullable)
628                .into_array();
629        let validity = Validity::from_iter([true, false, true]);
630        let groups = FixedSizeListArray::try_new(elements, 3, validity, 3)?;
631
632        let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
633        let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
634
635        let expected =
636            PrimitiveArray::from_option_iter([Some(6i64), None, Some(24i64)]).into_array();
637        assert_arrays_eq!(&result, &expected, &mut ctx);
638        Ok(())
639    }
640
641    #[test]
642    fn grouped_sum_all_null_elements_in_group() -> VortexResult<()> {
643        let mut ctx = array_session().create_execution_ctx();
644        let elements =
645            PrimitiveArray::from_option_iter([None::<i32>, None, Some(3), Some(4)]).into_array();
646        let groups = FixedSizeListArray::try_new(elements, 2, Validity::NonNullable, 2)?;
647
648        let elem_dtype = DType::Primitive(PType::I32, Nullable);
649        let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
650
651        let expected = PrimitiveArray::from_option_iter([Some(0i64), Some(7i64)]).into_array();
652        assert_arrays_eq!(&result, &expected, &mut ctx);
653        Ok(())
654    }
655
656    #[test]
657    fn grouped_sum_bool() -> VortexResult<()> {
658        let mut ctx = array_session().create_execution_ctx();
659        let elements: BoolArray = [true, false, true, true, true, true].into_iter().collect();
660        let groups =
661            FixedSizeListArray::try_new(elements.into_array(), 3, Validity::NonNullable, 2)?;
662
663        let elem_dtype = DType::Bool(Nullability::NonNullable);
664        let result = run_grouped_sum(&groups.into_array(), &elem_dtype)?;
665
666        let expected = PrimitiveArray::from_option_iter([Some(2u64), Some(3u64)]).into_array();
667        assert_arrays_eq!(&result, &expected, &mut ctx);
668        Ok(())
669    }
670
671    #[test]
672    fn grouped_sum_finish_resets() -> VortexResult<()> {
673        let mut ctx = array_session().create_execution_ctx();
674        let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
675        let mut acc =
676            GroupedAccumulator::try_new(Sum, NumericalAggregateOpts::default(), elem_dtype)?;
677
678        let elements1 =
679            PrimitiveArray::new(buffer![1i32, 2, 3, 4], Validity::NonNullable).into_array();
680        let groups1 = FixedSizeListArray::try_new(elements1, 2, Validity::NonNullable, 2)?;
681        acc.accumulate_list(&groups1.into_array(), &mut ctx)?;
682        let result1 = acc.finish()?;
683
684        let expected1 = PrimitiveArray::from_option_iter([Some(3i64), Some(7i64)]).into_array();
685        assert_arrays_eq!(&result1, &expected1, &mut ctx);
686
687        let elements2 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
688        let groups2 = FixedSizeListArray::try_new(elements2, 2, Validity::NonNullable, 1)?;
689        acc.accumulate_list(&groups2.into_array(), &mut ctx)?;
690        let result2 = acc.finish()?;
691
692        let expected2 = PrimitiveArray::from_option_iter([Some(30i64)]).into_array();
693        assert_arrays_eq!(&result2, &expected2, &mut ctx);
694        Ok(())
695    }
696
697    #[test]
698    fn grouped_sum_listview_out_of_order_offsets_with_null_group() -> VortexResult<()> {
699        let mut ctx = array_session().create_execution_ctx();
700        let elements =
701            PrimitiveArray::new(buffer![100i32, 200, 300], Validity::NonNullable).into_array();
702        let offsets = PrimitiveArray::new(buffer![2i32, 0, 1], Validity::NonNullable).into_array();
703        let sizes = PrimitiveArray::new(buffer![1i32, 1, 1], Validity::NonNullable).into_array();
704        let validity = Validity::from_iter([true, false, true]);
705        let groups = ListViewArray::try_new(elements, offsets, sizes, validity)?.into_array();
706
707        let elem_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
708        let result = run_grouped_sum(&groups, &elem_dtype)?;
709
710        // group 0 -> elements[2..3] = 300; group 1 -> null; group 2 -> elements[1..2] = 200.
711        let expected =
712            PrimitiveArray::from_option_iter([Some(300i64), None, Some(200i64)]).into_array();
713        assert_arrays_eq!(&result, &expected, &mut ctx);
714        Ok(())
715    }
716
717    // Chunked array tests
718
719    #[test]
720    fn sum_chunked_floats_with_nulls() -> VortexResult<()> {
721        let chunk1 =
722            PrimitiveArray::from_option_iter(vec![Some(1.5f64), None, Some(3.2), Some(4.8)]);
723        let chunk2 = PrimitiveArray::from_option_iter(vec![Some(2.1f64), Some(5.7), None]);
724        let chunk3 = PrimitiveArray::from_option_iter(vec![None, Some(1.0f64), Some(2.5), None]);
725        let dtype = chunk1.dtype().clone();
726        let chunked = ChunkedArray::try_new(
727            vec![
728                chunk1.into_array(),
729                chunk2.into_array(),
730                chunk3.into_array(),
731            ],
732            dtype,
733        )?;
734
735        let result = sum(
736            &chunked.into_array(),
737            &mut array_session().create_execution_ctx(),
738        )?;
739        assert_eq!(result.as_primitive().as_::<f64>(), Some(20.8));
740        Ok(())
741    }
742
743    #[test]
744    fn sum_chunked_floats_all_nulls_is_zero() -> VortexResult<()> {
745        let chunk1 = PrimitiveArray::from_option_iter::<f32, _>(vec![None, None, None]);
746        let chunk2 = PrimitiveArray::from_option_iter::<f32, _>(vec![None, None]);
747        let dtype = chunk1.dtype().clone();
748        let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
749        let result = sum(
750            &chunked.into_array(),
751            &mut array_session().create_execution_ctx(),
752        )?;
753        assert_eq!(result, Scalar::primitive(0f64, Nullable));
754        Ok(())
755    }
756
757    #[test]
758    fn sum_chunked_floats_empty_chunks() -> VortexResult<()> {
759        let chunk1 = PrimitiveArray::from_option_iter(vec![Some(10.5f64), Some(20.3)]);
760        let chunk2 = ConstantArray::new(Scalar::primitive(0f64, Nullable), 0);
761        let chunk3 = PrimitiveArray::from_option_iter(vec![Some(5.2f64)]);
762        let dtype = chunk1.dtype().clone();
763        let chunked = ChunkedArray::try_new(
764            vec![
765                chunk1.into_array(),
766                chunk2.into_array(),
767                chunk3.into_array(),
768            ],
769            dtype,
770        )?;
771
772        let result = sum(
773            &chunked.into_array(),
774            &mut array_session().create_execution_ctx(),
775        )?;
776        assert_eq!(result.as_primitive().as_::<f64>(), Some(36.0));
777        Ok(())
778    }
779
780    #[test]
781    fn sum_chunked_int_almost_all_null() -> VortexResult<()> {
782        let chunk1 = PrimitiveArray::from_option_iter::<u32, _>(vec![Some(1)]);
783        let chunk2 = PrimitiveArray::from_option_iter::<u32, _>(vec![None]);
784        let dtype = chunk1.dtype().clone();
785        let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
786
787        let result = sum(
788            &chunked.into_array(),
789            &mut array_session().create_execution_ctx(),
790        )?;
791        assert_eq!(result.as_primitive().as_::<u64>(), Some(1));
792        Ok(())
793    }
794
795    #[test]
796    fn sum_chunked_decimals() -> VortexResult<()> {
797        let decimal_dtype = DecimalDType::new(10, 2);
798        let chunk1 = DecimalArray::new(
799            buffer![100i32, 100i32, 100i32, 100i32, 100i32],
800            decimal_dtype,
801            Validity::AllValid,
802        );
803        let chunk2 = DecimalArray::new(
804            buffer![200i32, 200i32, 200i32],
805            decimal_dtype,
806            Validity::AllValid,
807        );
808        let chunk3 = DecimalArray::new(buffer![300i32, 300i32], decimal_dtype, Validity::AllValid);
809        let dtype = chunk1.dtype().clone();
810        let chunked = ChunkedArray::try_new(
811            vec![
812                chunk1.into_array(),
813                chunk2.into_array(),
814                chunk3.into_array(),
815            ],
816            dtype,
817        )?;
818
819        let result = sum(
820            &chunked.into_array(),
821            &mut array_session().create_execution_ctx(),
822        )?;
823        let decimal_result = result.as_decimal();
824        assert_eq!(
825            decimal_result.decimal_value(),
826            Some(DecimalValue::I256(i256::from_i128(1700)))
827        );
828        Ok(())
829    }
830
831    #[test]
832    fn sum_chunked_decimals_with_nulls() -> VortexResult<()> {
833        let decimal_dtype = DecimalDType::new(10, 2);
834        let chunk1 = DecimalArray::new(
835            buffer![100i32, 100i32, 100i32],
836            decimal_dtype,
837            Validity::AllValid,
838        );
839        let chunk2 = DecimalArray::new(
840            buffer![0i32, 0i32],
841            decimal_dtype,
842            Validity::from_iter([false, false]),
843        );
844        let chunk3 = DecimalArray::new(buffer![200i32, 200i32], decimal_dtype, Validity::AllValid);
845        let dtype = chunk1.dtype().clone();
846        let chunked = ChunkedArray::try_new(
847            vec![
848                chunk1.into_array(),
849                chunk2.into_array(),
850                chunk3.into_array(),
851            ],
852            dtype,
853        )?;
854
855        let result = sum(
856            &chunked.into_array(),
857            &mut array_session().create_execution_ctx(),
858        )?;
859        let decimal_result = result.as_decimal();
860        assert_eq!(
861            decimal_result.decimal_value(),
862            Some(DecimalValue::I256(i256::from_i128(700)))
863        );
864        Ok(())
865    }
866
867    #[test]
868    fn sum_chunked_decimals_large() -> VortexResult<()> {
869        let decimal_dtype = DecimalDType::new(3, 0);
870        let chunk1 = ConstantArray::new(
871            Scalar::decimal(
872                DecimalValue::I16(500),
873                decimal_dtype,
874                Nullability::NonNullable,
875            ),
876            1,
877        );
878        let chunk2 = ConstantArray::new(
879            Scalar::decimal(
880                DecimalValue::I16(600),
881                decimal_dtype,
882                Nullability::NonNullable,
883            ),
884            1,
885        );
886        let dtype = chunk1.dtype().clone();
887        let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
888
889        let result = sum(
890            &chunked.into_array(),
891            &mut array_session().create_execution_ctx(),
892        )?;
893        let decimal_result = result.as_decimal();
894        assert_eq!(
895            decimal_result.decimal_value(),
896            Some(DecimalValue::I256(i256::from_i128(1100)))
897        );
898        assert_eq!(
899            result.dtype(),
900            &DType::Decimal(DecimalDType::new(13, 0), Nullable)
901        );
902        Ok(())
903    }
904}