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