Skip to main content

vortex_array/scalar_fn/fns/
list_sum.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexResult;
5use vortex_error::vortex_bail;
6use vortex_error::vortex_err;
7use vortex_session::VortexSession;
8use vortex_session::registry::CachedId;
9
10use crate::ArrayRef;
11use crate::Canonical;
12use crate::Columnar;
13use crate::ExecutionCtx;
14use crate::IntoArray;
15use crate::aggregate_fn::AggregateFnVTable;
16use crate::aggregate_fn::DynGroupedAccumulator;
17use crate::aggregate_fn::GroupedAccumulator;
18use crate::aggregate_fn::NumericalAggregateOpts;
19use crate::aggregate_fn::fns::sum_v2::SumV2;
20use crate::arrays::ConstantArray;
21use crate::dtype::DType;
22use crate::scalar_fn::Arity;
23use crate::scalar_fn::ChildName;
24use crate::scalar_fn::ExecutionArgs;
25use crate::scalar_fn::ScalarFnId;
26use crate::scalar_fn::ScalarFnVTable;
27
28/// Sum of the elements in each list of a `List` or `FixedSizeList` typed array.
29///
30/// Follows SQL `SUM` semantics per list, matching DuckDB's `list_sum`: null lists, empty
31/// lists, and lists whose elements are all null yield a null sum; null elements are skipped.
32/// Integer and decimal overflow yields a null sum value, matching [`SumV2`]. The result dtype
33/// follows [`SumV2`]'s widening rules and is always nullable.
34///
35/// NaN handling for float elements is controlled by [`NumericalAggregateOpts`]: with
36/// `skip_nans` (the default) NaN values contribute nothing, otherwise any NaN poisons the
37/// list's sum to NaN.
38#[derive(Clone)]
39pub struct ListSum;
40
41impl ScalarFnVTable for ListSum {
42    type Options = NumericalAggregateOpts;
43
44    fn id(&self) -> ScalarFnId {
45        static ID: CachedId = CachedId::new("vortex.list.sum");
46        *ID
47    }
48
49    fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
50        Ok(Some(options.serialize()))
51    }
52
53    fn deserialize(
54        &self,
55        metadata: &[u8],
56        _session: &VortexSession,
57    ) -> VortexResult<Self::Options> {
58        NumericalAggregateOpts::deserialize(metadata)
59    }
60
61    fn arity(&self, _options: &Self::Options) -> Arity {
62        Arity::Exact(1)
63    }
64
65    fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
66        match child_idx {
67            0 => ChildName::from("input"),
68            _ => unreachable!("Invalid child index {child_idx} for list_sum()"),
69        }
70    }
71
72    fn return_dtype(&self, options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
73        let elem_dtype = match &arg_dtypes[0] {
74            DType::List(elem, _) | DType::FixedSizeList(elem, ..) => elem.as_ref(),
75            other => vortex_bail!("list_sum() requires List or FixedSizeList, got {other}"),
76        };
77        SumV2
78            .return_dtype(options, elem_dtype)
79            .ok_or_else(|| vortex_err!("list_sum() cannot sum elements of type {elem_dtype}"))
80    }
81
82    fn execute(
83        &self,
84        options: &Self::Options,
85        args: &dyn ExecutionArgs,
86        ctx: &mut ExecutionCtx,
87    ) -> VortexResult<ArrayRef> {
88        let input = args.get(0)?;
89
90        let elem_dtype = match input.dtype() {
91            DType::List(elem, _) | DType::FixedSizeList(elem, ..) => elem.as_ref().clone(),
92            other => vortex_bail!("list_sum() requires List or FixedSizeList, got {other}"),
93        };
94
95        let columnar = input.execute::<Columnar>(ctx)?;
96
97        match columnar {
98            Columnar::Constant(constant) => {
99                // Canonicalize one row of the constant and broadcast its sum.
100                let one_row = ConstantArray::new(constant.scalar().clone(), 1)
101                    .into_array()
102                    .execute::<Canonical>(ctx)?
103                    .into_array();
104                let sum =
105                    list_sum_impl(one_row, elem_dtype, options, ctx)?.execute_scalar(0, ctx)?;
106                Ok(ConstantArray::new(sum, constant.len()).into_array())
107            }
108            Columnar::Canonical(canonical) => {
109                list_sum_impl(canonical.into_array(), elem_dtype, options, ctx)
110            }
111        }
112    }
113
114    fn is_strict(&self, _options: &Self::Options) -> bool {
115        // A null list sums to null, which is all that strictness requires. Element nulls are part
116        // of the list value rather than row-level nulls, and a valid list may still sum to null
117        // (empty, all-null, or overflow) since strictness is only one-directional.
118        true
119    }
120
121    fn is_infallible(&self, _options: &Self::Options) -> bool {
122        true
123    }
124}
125
126/// Sum each list of a canonical `array` into one value per list.
127fn list_sum_impl(
128    canonical: ArrayRef,
129    elem_dtype: DType,
130    options: &NumericalAggregateOpts,
131    ctx: &mut ExecutionCtx,
132) -> VortexResult<ArrayRef> {
133    let mut acc = GroupedAccumulator::try_new(SumV2, *options, elem_dtype)?;
134    acc.accumulate_list(&canonical, ctx)?;
135    acc.finish()
136}
137
138#[cfg(test)]
139mod tests {
140    use std::sync::Arc;
141
142    use prost::Message;
143    use rstest::rstest;
144    use vortex_buffer::buffer;
145    use vortex_error::VortexResult;
146
147    use crate::ArrayRef;
148    use crate::IntoArray;
149    use crate::VortexSessionExecute;
150    use crate::aggregate_fn::NumericalAggregateOpts;
151    use crate::array_session;
152    use crate::arrays::BoolArray;
153    use crate::arrays::ConstantArray;
154    use crate::arrays::FixedSizeListArray;
155    use crate::arrays::ListArray;
156    use crate::arrays::ListViewArray;
157    use crate::arrays::PrimitiveArray;
158    use crate::assert_arrays_eq;
159    use crate::dtype::DType;
160    use crate::dtype::Nullability;
161    use crate::dtype::PType;
162    use crate::expr::Expression;
163    use crate::expr::list_sum;
164    use crate::expr::list_sum_opts;
165    use crate::expr::proto::ExprSerializeProtoExt;
166    use crate::expr::root;
167    use crate::proto::expr as pb;
168    use crate::scalar::Scalar;
169    use crate::scalar_fn::ScalarFnVTable;
170    use crate::scalar_fn::fns::list_sum::ListSum;
171    use crate::validity::Validity;
172
173    fn create_list_elements() -> ArrayRef {
174        PrimitiveArray::from_option_iter::<i32, _>([
175            Some(1),
176            Some(2),
177            Some(3),
178            Some(4),
179            Some(5),
180            Some(6),
181            None,
182        ])
183        .into_array()
184    }
185
186    #[rstest]
187    #[case(buffer![0u32, 2, 5, 5, 7].into_array())]
188    #[case(buffer![0u64, 2, 5, 5, 7].into_array())]
189    fn test_list_sum(#[case] offsets: ArrayRef) -> VortexResult<()> {
190        let mut ctx = array_session().create_execution_ctx();
191        let elements = create_list_elements();
192        let list =
193            ListArray::try_new(elements, offsets, Validity::NonNullable, &mut ctx)?.into_array();
194        let result = list.apply(&list_sum(root()))?;
195        let mut ctx = array_session().create_execution_ctx();
196        // [1, 2] = 3; [3, 4, 5] = 12; [] = null; [6, null] = 6 (nulls skipped).
197        let expected =
198            PrimitiveArray::from_option_iter::<i64, _>([Some(3), Some(12), None, Some(6)]);
199        assert_arrays_eq!(result, expected, &mut ctx);
200        Ok(())
201    }
202
203    #[test]
204    fn test_nullable_list_sum() -> VortexResult<()> {
205        let mut ctx = array_session().create_execution_ctx();
206        let elements = create_list_elements();
207        let list = ListArray::try_new(
208            elements,
209            buffer![0u32, 2, 5, 5, 7].into_array(),
210            Validity::Array(BoolArray::from_iter([true, false, true, false]).into_array()),
211            &mut ctx,
212        )?
213        .into_array();
214        let result = list.apply(&list_sum(root()))?;
215
216        let mut ctx = array_session().create_execution_ctx();
217        // Row 1 and 3 are null lists; row 2 is a valid but empty list.
218        let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(3), None, None, None]);
219        assert_arrays_eq!(result, expected, &mut ctx);
220        Ok(())
221    }
222
223    #[test]
224    fn test_all_null_elements_sum_to_null() -> VortexResult<()> {
225        let mut ctx = array_session().create_execution_ctx();
226        let elements = PrimitiveArray::from_option_iter::<i32, _>([None, None, Some(1)]);
227        let list = ListArray::try_new(
228            elements.into_array(),
229            buffer![0u32, 2, 3].into_array(),
230            Validity::NonNullable,
231            &mut ctx,
232        )?
233        .into_array();
234        let result = list.apply(&list_sum(root()))?;
235
236        let mut ctx = array_session().create_execution_ctx();
237        let expected = PrimitiveArray::from_option_iter::<i64, _>([None, Some(1)]);
238        assert_arrays_eq!(result, expected, &mut ctx);
239        Ok(())
240    }
241
242    #[test]
243    fn test_overflow_sums_to_null() -> VortexResult<()> {
244        let mut ctx = array_session().create_execution_ctx();
245        let elements = PrimitiveArray::from_iter([i64::MAX, 1, 1, 2]);
246        let list = ListArray::try_new(
247            elements.into_array(),
248            buffer![0u32, 2, 4].into_array(),
249            Validity::NonNullable,
250            &mut ctx,
251        )?
252        .into_array();
253        let result = list.apply(&list_sum(root()))?;
254
255        let mut ctx = array_session().create_execution_ctx();
256        let expected = PrimitiveArray::from_option_iter::<i64, _>([None, Some(3)]);
257        assert_arrays_eq!(result, expected, &mut ctx);
258        Ok(())
259    }
260
261    #[test]
262    fn test_unsigned_widening() -> VortexResult<()> {
263        let mut ctx = array_session().create_execution_ctx();
264        let elements = PrimitiveArray::from_iter([1u8, 2, 3]);
265        let list = ListArray::try_new(
266            elements.into_array(),
267            buffer![0u32, 3].into_array(),
268            Validity::NonNullable,
269            &mut ctx,
270        )?
271        .into_array();
272        let result = list.apply(&list_sum(root()))?;
273
274        let mut ctx = array_session().create_execution_ctx();
275        let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(6)]);
276        assert_arrays_eq!(result, expected, &mut ctx);
277        Ok(())
278    }
279
280    #[test]
281    fn test_bool_elements() -> VortexResult<()> {
282        let mut ctx = array_session().create_execution_ctx();
283        let elements = BoolArray::from_iter([true, true, false, true]);
284        let list = ListArray::try_new(
285            elements.into_array(),
286            buffer![0u32, 3, 4].into_array(),
287            Validity::NonNullable,
288            &mut ctx,
289        )?
290        .into_array();
291        let result = list.apply(&list_sum(root()))?;
292
293        let mut ctx = array_session().create_execution_ctx();
294        let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(2), Some(1)]);
295        assert_arrays_eq!(result, expected, &mut ctx);
296        Ok(())
297    }
298
299    #[test]
300    fn test_nan_skipped_by_default() -> VortexResult<()> {
301        let mut ctx = array_session().create_execution_ctx();
302        let elements = PrimitiveArray::from_iter([1.0f64, f64::NAN, 2.0]);
303        let list = ListArray::try_new(
304            elements.into_array(),
305            buffer![0u32, 3].into_array(),
306            Validity::NonNullable,
307            &mut ctx,
308        )?
309        .into_array();
310        let result = list.apply(&list_sum(root()))?;
311
312        let mut ctx = array_session().create_execution_ctx();
313        let expected = PrimitiveArray::from_option_iter::<f64, _>([Some(3.0)]);
314        assert_arrays_eq!(result, expected, &mut ctx);
315        Ok(())
316    }
317
318    #[test]
319    fn test_all_nan_list_sums_to_zero() -> VortexResult<()> {
320        let mut ctx = array_session().create_execution_ctx();
321        // NaN elements are *valid*: with the default `skip_nans` they contribute nothing, but
322        // the list still has valid elements, so the sum is `0.0` rather than null (unlike an
323        // all-null list).
324        let elements = PrimitiveArray::from_iter([f64::NAN, f64::NAN]);
325        let list = ListArray::try_new(
326            elements.into_array(),
327            buffer![0u32, 2].into_array(),
328            Validity::NonNullable,
329            &mut ctx,
330        )?
331        .into_array();
332        let result = list.apply(&list_sum(root()))?;
333
334        let mut ctx = array_session().create_execution_ctx();
335        let expected = PrimitiveArray::from_option_iter::<f64, _>([Some(0.0)]);
336        assert_arrays_eq!(result, expected, &mut ctx);
337        Ok(())
338    }
339
340    #[test]
341    fn test_nan_poisons_with_include_nans() -> VortexResult<()> {
342        let mut ctx = array_session().create_execution_ctx();
343        let elements = PrimitiveArray::from_iter([1.0f64, f64::NAN, 2.0]);
344        let list = ListArray::try_new(
345            elements.into_array(),
346            buffer![0u32, 3].into_array(),
347            Validity::NonNullable,
348            &mut ctx,
349        )?
350        .into_array();
351        let result = list.apply(&list_sum_opts(
352            root(),
353            NumericalAggregateOpts::include_nans(),
354        ))?;
355
356        let mut ctx = array_session().create_execution_ctx();
357        assert!(result.is_valid(0, &mut ctx)?);
358        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
359        assert!(result.as_slice::<f64>()[0].is_nan());
360        Ok(())
361    }
362
363    #[test]
364    fn test_listview_sum() -> VortexResult<()> {
365        let mut ctx = array_session().create_execution_ctx();
366        let elements = create_list_elements();
367        // Overlapping and out-of-order views over the shared elements.
368        let lv = ListViewArray::new(
369            elements,
370            buffer![5u32, 0, 4, 1].into_array(),
371            buffer![2u32, 3, 0, 2].into_array(),
372            Validity::NonNullable,
373            &mut ctx,
374        )
375        .into_array();
376        let result = lv.apply(&list_sum(root()))?;
377
378        let mut ctx = array_session().create_execution_ctx();
379        // [6, null] = 6; [1, 2, 3] = 6; [] = null; [2, 3] = 5.
380        let expected =
381            PrimitiveArray::from_option_iter::<i64, _>([Some(6), Some(6), None, Some(5)]);
382        assert_arrays_eq!(result, expected, &mut ctx);
383        Ok(())
384    }
385
386    fn create_fixed_size_list(validity: Validity) -> ArrayRef {
387        // 4 lists of size 2 over 8 primitive elements.
388        let elements = PrimitiveArray::from_iter([1i32, 2, 3, 4, 5, 6, 7, 8]).into_array();
389        FixedSizeListArray::new(elements, 2, validity, 4).into_array()
390    }
391
392    #[test]
393    fn test_fixed_size_list_sum() -> VortexResult<()> {
394        let fsl = create_fixed_size_list(Validity::NonNullable);
395        let result = fsl.apply(&list_sum(root()))?;
396
397        let mut ctx = array_session().create_execution_ctx();
398        let expected =
399            PrimitiveArray::from_option_iter::<i64, _>([Some(3), Some(7), Some(11), Some(15)]);
400        assert_arrays_eq!(result, expected, &mut ctx);
401        Ok(())
402    }
403
404    #[test]
405    fn test_fixed_size_list_sum_nullable() -> VortexResult<()> {
406        let fsl = create_fixed_size_list(Validity::Array(
407            BoolArray::from_iter([true, false, true, false]).into_array(),
408        ));
409        let result = fsl.apply(&list_sum(root()))?;
410
411        let mut ctx = array_session().create_execution_ctx();
412        let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(3), None, Some(11), None]);
413        assert_arrays_eq!(result, expected, &mut ctx);
414        Ok(())
415    }
416
417    #[test]
418    fn test_list_sum_take() -> VortexResult<()> {
419        let mut ctx = array_session().create_execution_ctx();
420        let elements = create_list_elements();
421        let list = ListArray::try_new(
422            elements,
423            buffer![0u32, 2, 5, 5, 7].into_array(),
424            Validity::NonNullable,
425            &mut ctx,
426        )?
427        .into_array();
428        let taken = list.take(buffer![3u64, 0, 2].into_array())?;
429
430        let result = taken.apply(&list_sum(root()))?;
431        let mut ctx = array_session().create_execution_ctx();
432        let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(6), Some(3), None]);
433        assert_arrays_eq!(result, expected, &mut ctx);
434        Ok(())
435    }
436
437    #[test]
438    fn test_list_sum_slice() -> VortexResult<()> {
439        let mut ctx = array_session().create_execution_ctx();
440        let elements = create_list_elements();
441        let list = ListArray::try_new(
442            elements,
443            buffer![0u32, 2, 5, 5, 7].into_array(),
444            Validity::NonNullable,
445            &mut ctx,
446        )?
447        .into_array();
448        let sliced = list.slice(1..4)?;
449
450        let result = sliced.apply(&list_sum(root()))?;
451        let mut ctx = array_session().create_execution_ctx();
452        let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(12), None, Some(6)]);
453        assert_arrays_eq!(result, expected, &mut ctx);
454        Ok(())
455    }
456
457    #[test]
458    fn test_empty_array() -> VortexResult<()> {
459        let mut ctx = array_session().create_execution_ctx();
460        let elements = PrimitiveArray::from_iter([0i32; 0]);
461        let list = ListArray::try_new(
462            elements.into_array(),
463            buffer![0u32].into_array(),
464            Validity::NonNullable,
465            &mut ctx,
466        )?
467        .into_array();
468        let result = list.apply(&list_sum(root()))?;
469
470        let mut ctx = array_session().create_execution_ctx();
471        assert_arrays_eq!(
472            result,
473            PrimitiveArray::from_option_iter::<i64, _>([]),
474            &mut ctx
475        );
476        Ok(())
477    }
478
479    #[test]
480    fn test_constant_list_sum() -> VortexResult<()> {
481        let mut ctx = array_session().create_execution_ctx();
482        let elements = create_list_elements();
483        let list = ListArray::try_new(
484            elements,
485            buffer![0u32, 2, 5, 5, 7].into_array(),
486            Validity::NonNullable,
487            &mut ctx,
488        )?
489        .into_array();
490        let mut ctx = array_session().create_execution_ctx();
491        let scalar = list.execute_scalar(1, &mut ctx)?;
492
493        let constant = ConstantArray::new(scalar, 3).into_array();
494        let result = constant.apply(&list_sum(root()))?;
495        let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(12), Some(12), Some(12)]);
496        assert_arrays_eq!(result, expected, &mut ctx);
497        Ok(())
498    }
499
500    #[test]
501    fn test_null_scalar_list_sum() -> VortexResult<()> {
502        let null_scalar = Scalar::null(DType::List(
503            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
504            Nullability::Nullable,
505        ));
506        let array = ConstantArray::new(null_scalar, 2).into_array();
507        let result = array.apply(&list_sum(root()))?;
508
509        let mut ctx = array_session().create_execution_ctx();
510        assert!(!result.is_valid(0, &mut ctx)?);
511        assert!(!result.is_valid(1, &mut ctx)?);
512        assert_arrays_eq!(
513            result,
514            PrimitiveArray::from_option_iter([None::<i64>, None]),
515            &mut ctx
516        );
517        Ok(())
518    }
519
520    #[test]
521    fn test_unsupported_dtypes() {
522        let opts = NumericalAggregateOpts::default();
523        // Non-list inputs are rejected.
524        assert!(
525            ListSum
526                .return_dtype(
527                    &opts,
528                    &[DType::Primitive(PType::I32, Nullability::NonNullable)]
529                )
530                .is_err()
531        );
532        // Non-numeric element types are rejected.
533        assert!(
534            ListSum
535                .return_dtype(
536                    &opts,
537                    &[DType::List(
538                        Arc::new(DType::Utf8(Nullability::NonNullable)),
539                        Nullability::NonNullable,
540                    )],
541                )
542                .is_err()
543        );
544    }
545
546    #[test]
547    fn test_display() {
548        assert_eq!(list_sum(root()).to_string(), "vortex.list.sum($)");
549        assert_eq!(
550            list_sum_opts(root(), NumericalAggregateOpts::include_nans()).to_string(),
551            "vortex.list.sum($, opts=skip_nans=false)"
552        );
553    }
554
555    #[test]
556    fn test_proto_round_trip() -> VortexResult<()> {
557        for expr in [
558            list_sum(root()),
559            list_sum_opts(root(), NumericalAggregateOpts::include_nans()),
560        ] {
561            let proto = expr.serialize_proto()?;
562            let buf = proto.encode_to_vec();
563            let decoded = pb::Expr::decode(buf.as_slice())?;
564            let deser = Expression::from_proto(&decoded, &array_session())?;
565            assert_eq!(expr, deser);
566        }
567        Ok(())
568    }
569}