1use 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#[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 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 true
119 }
120
121 fn is_infallible(&self, _options: &Self::Options) -> bool {
122 true
123 }
124}
125
126fn 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 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 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 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 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 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 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 assert!(
525 ListSum
526 .return_dtype(
527 &opts,
528 &[DType::Primitive(PType::I32, Nullability::NonNullable)]
529 )
530 .is_err()
531 );
532 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}