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 use vortex_proto::expr as pb;
147
148 use crate::ArrayRef;
149 use crate::IntoArray;
150 use crate::VortexSessionExecute;
151 use crate::aggregate_fn::NumericalAggregateOpts;
152 use crate::array_session;
153 use crate::arrays::BoolArray;
154 use crate::arrays::ConstantArray;
155 use crate::arrays::FixedSizeListArray;
156 use crate::arrays::ListArray;
157 use crate::arrays::ListViewArray;
158 use crate::arrays::PrimitiveArray;
159 use crate::assert_arrays_eq;
160 use crate::dtype::DType;
161 use crate::dtype::Nullability;
162 use crate::dtype::PType;
163 use crate::expr::Expression;
164 use crate::expr::list_sum;
165 use crate::expr::list_sum_opts;
166 use crate::expr::proto::ExprSerializeProtoExt;
167 use crate::expr::root;
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 elements = create_list_elements();
191 let list = ListArray::try_new(elements, offsets, Validity::NonNullable)?.into_array();
192 let result = list.apply(&list_sum(root()))?;
193 let mut ctx = array_session().create_execution_ctx();
194 let expected =
196 PrimitiveArray::from_option_iter::<i64, _>([Some(3), Some(12), None, Some(6)]);
197 assert_arrays_eq!(result, expected, &mut ctx);
198 Ok(())
199 }
200
201 #[test]
202 fn test_nullable_list_sum() -> VortexResult<()> {
203 let elements = create_list_elements();
204 let list = ListArray::try_new(
205 elements,
206 buffer![0u32, 2, 5, 5, 7].into_array(),
207 Validity::Array(BoolArray::from_iter([true, false, true, false]).into_array()),
208 )?
209 .into_array();
210 let result = list.apply(&list_sum(root()))?;
211
212 let mut ctx = array_session().create_execution_ctx();
213 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(3), None, None, None]);
215 assert_arrays_eq!(result, expected, &mut ctx);
216 Ok(())
217 }
218
219 #[test]
220 fn test_all_null_elements_sum_to_null() -> VortexResult<()> {
221 let elements = PrimitiveArray::from_option_iter::<i32, _>([None, None, Some(1)]);
222 let list = ListArray::try_new(
223 elements.into_array(),
224 buffer![0u32, 2, 3].into_array(),
225 Validity::NonNullable,
226 )?
227 .into_array();
228 let result = list.apply(&list_sum(root()))?;
229
230 let mut ctx = array_session().create_execution_ctx();
231 let expected = PrimitiveArray::from_option_iter::<i64, _>([None, Some(1)]);
232 assert_arrays_eq!(result, expected, &mut ctx);
233 Ok(())
234 }
235
236 #[test]
237 fn test_overflow_sums_to_null() -> VortexResult<()> {
238 let elements = PrimitiveArray::from_iter([i64::MAX, 1, 1, 2]);
239 let list = ListArray::try_new(
240 elements.into_array(),
241 buffer![0u32, 2, 4].into_array(),
242 Validity::NonNullable,
243 )?
244 .into_array();
245 let result = list.apply(&list_sum(root()))?;
246
247 let mut ctx = array_session().create_execution_ctx();
248 let expected = PrimitiveArray::from_option_iter::<i64, _>([None, Some(3)]);
249 assert_arrays_eq!(result, expected, &mut ctx);
250 Ok(())
251 }
252
253 #[test]
254 fn test_unsigned_widening() -> VortexResult<()> {
255 let elements = PrimitiveArray::from_iter([1u8, 2, 3]);
256 let list = ListArray::try_new(
257 elements.into_array(),
258 buffer![0u32, 3].into_array(),
259 Validity::NonNullable,
260 )?
261 .into_array();
262 let result = list.apply(&list_sum(root()))?;
263
264 let mut ctx = array_session().create_execution_ctx();
265 let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(6)]);
266 assert_arrays_eq!(result, expected, &mut ctx);
267 Ok(())
268 }
269
270 #[test]
271 fn test_bool_elements() -> VortexResult<()> {
272 let elements = BoolArray::from_iter([true, true, false, true]);
273 let list = ListArray::try_new(
274 elements.into_array(),
275 buffer![0u32, 3, 4].into_array(),
276 Validity::NonNullable,
277 )?
278 .into_array();
279 let result = list.apply(&list_sum(root()))?;
280
281 let mut ctx = array_session().create_execution_ctx();
282 let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(2), Some(1)]);
283 assert_arrays_eq!(result, expected, &mut ctx);
284 Ok(())
285 }
286
287 #[test]
288 fn test_nan_skipped_by_default() -> VortexResult<()> {
289 let elements = PrimitiveArray::from_iter([1.0f64, f64::NAN, 2.0]);
290 let list = ListArray::try_new(
291 elements.into_array(),
292 buffer![0u32, 3].into_array(),
293 Validity::NonNullable,
294 )?
295 .into_array();
296 let result = list.apply(&list_sum(root()))?;
297
298 let mut ctx = array_session().create_execution_ctx();
299 let expected = PrimitiveArray::from_option_iter::<f64, _>([Some(3.0)]);
300 assert_arrays_eq!(result, expected, &mut ctx);
301 Ok(())
302 }
303
304 #[test]
305 fn test_all_nan_list_sums_to_zero() -> VortexResult<()> {
306 let elements = PrimitiveArray::from_iter([f64::NAN, f64::NAN]);
310 let list = ListArray::try_new(
311 elements.into_array(),
312 buffer![0u32, 2].into_array(),
313 Validity::NonNullable,
314 )?
315 .into_array();
316 let result = list.apply(&list_sum(root()))?;
317
318 let mut ctx = array_session().create_execution_ctx();
319 let expected = PrimitiveArray::from_option_iter::<f64, _>([Some(0.0)]);
320 assert_arrays_eq!(result, expected, &mut ctx);
321 Ok(())
322 }
323
324 #[test]
325 fn test_nan_poisons_with_include_nans() -> VortexResult<()> {
326 let elements = PrimitiveArray::from_iter([1.0f64, f64::NAN, 2.0]);
327 let list = ListArray::try_new(
328 elements.into_array(),
329 buffer![0u32, 3].into_array(),
330 Validity::NonNullable,
331 )?
332 .into_array();
333 let result = list.apply(&list_sum_opts(
334 root(),
335 NumericalAggregateOpts::include_nans(),
336 ))?;
337
338 let mut ctx = array_session().create_execution_ctx();
339 assert!(result.is_valid(0, &mut ctx)?);
340 let result = result.execute::<PrimitiveArray>(&mut ctx)?;
341 assert!(result.as_slice::<f64>()[0].is_nan());
342 Ok(())
343 }
344
345 #[test]
346 fn test_listview_sum() -> VortexResult<()> {
347 let elements = create_list_elements();
348 let lv = ListViewArray::new(
350 elements,
351 buffer![5u32, 0, 4, 1].into_array(),
352 buffer![2u32, 3, 0, 2].into_array(),
353 Validity::NonNullable,
354 )
355 .into_array();
356 let result = lv.apply(&list_sum(root()))?;
357
358 let mut ctx = array_session().create_execution_ctx();
359 let expected =
361 PrimitiveArray::from_option_iter::<i64, _>([Some(6), Some(6), None, Some(5)]);
362 assert_arrays_eq!(result, expected, &mut ctx);
363 Ok(())
364 }
365
366 fn create_fixed_size_list(validity: Validity) -> ArrayRef {
367 let elements = PrimitiveArray::from_iter([1i32, 2, 3, 4, 5, 6, 7, 8]).into_array();
369 FixedSizeListArray::new(elements, 2, validity, 4).into_array()
370 }
371
372 #[test]
373 fn test_fixed_size_list_sum() -> VortexResult<()> {
374 let fsl = create_fixed_size_list(Validity::NonNullable);
375 let result = fsl.apply(&list_sum(root()))?;
376
377 let mut ctx = array_session().create_execution_ctx();
378 let expected =
379 PrimitiveArray::from_option_iter::<i64, _>([Some(3), Some(7), Some(11), Some(15)]);
380 assert_arrays_eq!(result, expected, &mut ctx);
381 Ok(())
382 }
383
384 #[test]
385 fn test_fixed_size_list_sum_nullable() -> VortexResult<()> {
386 let fsl = create_fixed_size_list(Validity::Array(
387 BoolArray::from_iter([true, false, true, false]).into_array(),
388 ));
389 let result = fsl.apply(&list_sum(root()))?;
390
391 let mut ctx = array_session().create_execution_ctx();
392 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(3), None, Some(11), None]);
393 assert_arrays_eq!(result, expected, &mut ctx);
394 Ok(())
395 }
396
397 #[test]
398 fn test_list_sum_take() -> VortexResult<()> {
399 let elements = create_list_elements();
400 let list = ListArray::try_new(
401 elements,
402 buffer![0u32, 2, 5, 5, 7].into_array(),
403 Validity::NonNullable,
404 )?
405 .into_array();
406 let taken = list.take(buffer![3u64, 0, 2].into_array())?;
407
408 let result = taken.apply(&list_sum(root()))?;
409 let mut ctx = array_session().create_execution_ctx();
410 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(6), Some(3), None]);
411 assert_arrays_eq!(result, expected, &mut ctx);
412 Ok(())
413 }
414
415 #[test]
416 fn test_list_sum_slice() -> VortexResult<()> {
417 let elements = create_list_elements();
418 let list = ListArray::try_new(
419 elements,
420 buffer![0u32, 2, 5, 5, 7].into_array(),
421 Validity::NonNullable,
422 )?
423 .into_array();
424 let sliced = list.slice(1..4)?;
425
426 let result = sliced.apply(&list_sum(root()))?;
427 let mut ctx = array_session().create_execution_ctx();
428 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(12), None, Some(6)]);
429 assert_arrays_eq!(result, expected, &mut ctx);
430 Ok(())
431 }
432
433 #[test]
434 fn test_empty_array() -> VortexResult<()> {
435 let elements = PrimitiveArray::from_iter([0i32; 0]);
436 let list = ListArray::try_new(
437 elements.into_array(),
438 buffer![0u32].into_array(),
439 Validity::NonNullable,
440 )?
441 .into_array();
442 let result = list.apply(&list_sum(root()))?;
443
444 let mut ctx = array_session().create_execution_ctx();
445 let result = result.execute::<PrimitiveArray>(&mut ctx)?;
446 assert_eq!(result.len(), 0);
447 Ok(())
448 }
449
450 #[test]
451 fn test_constant_list_sum() -> VortexResult<()> {
452 let elements = create_list_elements();
453 let list = ListArray::try_new(
454 elements,
455 buffer![0u32, 2, 5, 5, 7].into_array(),
456 Validity::NonNullable,
457 )?
458 .into_array();
459 let mut ctx = array_session().create_execution_ctx();
460 let scalar = list.execute_scalar(1, &mut ctx)?;
461
462 let constant = ConstantArray::new(scalar, 3).into_array();
463 let result = constant.apply(&list_sum(root()))?;
464 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(12), Some(12), Some(12)]);
465 assert_arrays_eq!(result, expected, &mut ctx);
466 Ok(())
467 }
468
469 #[test]
470 fn test_null_scalar_list_sum() -> VortexResult<()> {
471 let null_scalar = Scalar::null(DType::List(
472 Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
473 Nullability::Nullable,
474 ));
475 let array = ConstantArray::new(null_scalar, 2).into_array();
476 let result = array.apply(&list_sum(root()))?;
477
478 let mut ctx = array_session().create_execution_ctx();
479 assert!(!result.is_valid(0, &mut ctx)?);
480 assert!(!result.is_valid(1, &mut ctx)?);
481 Ok(())
482 }
483
484 #[test]
485 fn test_unsupported_dtypes() {
486 let opts = NumericalAggregateOpts::default();
487 assert!(
489 ListSum
490 .return_dtype(
491 &opts,
492 &[DType::Primitive(PType::I32, Nullability::NonNullable)]
493 )
494 .is_err()
495 );
496 assert!(
498 ListSum
499 .return_dtype(
500 &opts,
501 &[DType::List(
502 Arc::new(DType::Utf8(Nullability::NonNullable)),
503 Nullability::NonNullable,
504 )],
505 )
506 .is_err()
507 );
508 }
509
510 #[test]
511 fn test_display() {
512 assert_eq!(list_sum(root()).to_string(), "vortex.list.sum($)");
513 assert_eq!(
514 list_sum_opts(root(), NumericalAggregateOpts::include_nans()).to_string(),
515 "vortex.list.sum($, opts=skip_nans=false)"
516 );
517 }
518
519 #[test]
520 fn test_proto_round_trip() -> VortexResult<()> {
521 for expr in [
522 list_sum(root()),
523 list_sum_opts(root(), NumericalAggregateOpts::include_nans()),
524 ] {
525 let proto = expr.serialize_proto()?;
526 let buf = proto.encode_to_vec();
527 let decoded = pb::Expr::decode(buf.as_slice())?;
528 let deser = Expression::from_proto(&decoded, &array_session())?;
529 assert_eq!(expr, deser);
530 }
531 Ok(())
532 }
533}