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_null_sensitive(&self, _options: &Self::Options) -> bool {
124 false
125 }
126
127 fn is_fallible(&self, _options: &Self::Options) -> bool {
128 false
129 }
130}
131
132fn list_sum_impl(
137 canonical: ArrayRef,
138 elem_dtype: DType,
139 options: &NumericalAggregateOpts,
140 ctx: &mut ExecutionCtx,
141) -> VortexResult<ArrayRef> {
142 let mut acc = GroupedAccumulator::try_new(Sum, *options, elem_dtype)?;
143 acc.accumulate_list(&canonical, ctx)?;
144 let sums = acc.finish()?;
145
146 let grouped: GroupedArray = if let Some(fsl) = canonical.as_opt::<FixedSizeList>() {
147 fsl.into_owned().into()
148 } else if let Some(lv) = canonical.as_opt::<ListView>() {
149 lv.into_owned().into()
150 } else {
151 let dtype = canonical.dtype();
152 vortex_bail!("list_sum() requires List or FixedSizeList but got {dtype}")
153 };
154
155 mask_empty_lists(grouped, sums, ctx)
156}
157
158fn mask_empty_lists(
162 grouped: GroupedArray,
163 sums: ArrayRef,
164 ctx: &mut ExecutionCtx,
165) -> VortexResult<ArrayRef> {
166 let elements = grouped.elements();
167 let elem_mask = elements.validity()?.execute_mask(elements.len(), ctx)?;
168 let ranges = grouped.group_ranges(ctx)?;
169
170 let has_valid_element: BitBuffer = match elem_mask.bit_buffer() {
171 AllOr::All => match &ranges {
172 GroupRanges::FixedSizeList { size, .. } if *size > 0 => return Ok(sums),
174 GroupRanges::FixedSizeList { len, .. } => BitBuffer::full(false, *len),
175 GroupRanges::ListView { ranges } => ranges.iter().map(|&(_, size)| size > 0).collect(),
176 },
177 AllOr::None => BitBuffer::full(false, ranges.len()),
178 AllOr::Some(bits) => ranges
179 .iter()
180 .map(|(offset, size)| size > 0 && bits.count_range(offset, offset + size) > 0)
181 .collect(),
182 };
183 if has_valid_element.true_count() == has_valid_element.len() {
184 return Ok(sums);
185 }
186
187 sums.mask(BoolArray::new(has_valid_element, Validity::NonNullable).into_array())
188}
189
190#[cfg(test)]
191mod tests {
192 use std::sync::Arc;
193
194 use prost::Message;
195 use rstest::rstest;
196 use vortex_buffer::buffer;
197 use vortex_error::VortexResult;
198 use vortex_proto::expr as pb;
199
200 use crate::ArrayRef;
201 use crate::IntoArray;
202 use crate::VortexSessionExecute;
203 use crate::aggregate_fn::NumericalAggregateOpts;
204 use crate::array_session;
205 use crate::arrays::BoolArray;
206 use crate::arrays::ConstantArray;
207 use crate::arrays::FixedSizeListArray;
208 use crate::arrays::ListArray;
209 use crate::arrays::ListViewArray;
210 use crate::arrays::PrimitiveArray;
211 use crate::assert_arrays_eq;
212 use crate::dtype::DType;
213 use crate::dtype::Nullability;
214 use crate::dtype::PType;
215 use crate::expr::Expression;
216 use crate::expr::list_sum;
217 use crate::expr::list_sum_opts;
218 use crate::expr::proto::ExprSerializeProtoExt;
219 use crate::expr::root;
220 use crate::scalar::Scalar;
221 use crate::scalar_fn::ScalarFnVTable;
222 use crate::scalar_fn::fns::list_sum::ListSum;
223 use crate::validity::Validity;
224
225 fn create_list_elements() -> ArrayRef {
226 PrimitiveArray::from_option_iter::<i32, _>([
227 Some(1),
228 Some(2),
229 Some(3),
230 Some(4),
231 Some(5),
232 Some(6),
233 None,
234 ])
235 .into_array()
236 }
237
238 #[rstest]
239 #[case(buffer![0u32, 2, 5, 5, 7].into_array())]
240 #[case(buffer![0u64, 2, 5, 5, 7].into_array())]
241 fn test_list_sum(#[case] offsets: ArrayRef) -> VortexResult<()> {
242 let elements = create_list_elements();
243 let list = ListArray::try_new(elements, offsets, Validity::NonNullable)?.into_array();
244 let result = list.apply(&list_sum(root()))?;
245 let mut ctx = array_session().create_execution_ctx();
246 let expected =
248 PrimitiveArray::from_option_iter::<i64, _>([Some(3), Some(12), None, Some(6)]);
249 assert_arrays_eq!(result, expected, &mut ctx);
250 Ok(())
251 }
252
253 #[test]
254 fn test_nullable_list_sum() -> VortexResult<()> {
255 let elements = create_list_elements();
256 let list = ListArray::try_new(
257 elements,
258 buffer![0u32, 2, 5, 5, 7].into_array(),
259 Validity::Array(BoolArray::from_iter([true, false, true, false]).into_array()),
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::<i64, _>([Some(3), None, None, None]);
267 assert_arrays_eq!(result, expected, &mut ctx);
268 Ok(())
269 }
270
271 #[test]
272 fn test_all_null_elements_sum_to_null() -> VortexResult<()> {
273 let elements = PrimitiveArray::from_option_iter::<i32, _>([None, None, Some(1)]);
274 let list = ListArray::try_new(
275 elements.into_array(),
276 buffer![0u32, 2, 3].into_array(),
277 Validity::NonNullable,
278 )?
279 .into_array();
280 let result = list.apply(&list_sum(root()))?;
281
282 let mut ctx = array_session().create_execution_ctx();
283 let expected = PrimitiveArray::from_option_iter::<i64, _>([None, Some(1)]);
284 assert_arrays_eq!(result, expected, &mut ctx);
285 Ok(())
286 }
287
288 #[test]
289 fn test_overflow_sums_to_null() -> VortexResult<()> {
290 let elements = PrimitiveArray::from_iter([i64::MAX, 1, 1, 2]);
291 let list = ListArray::try_new(
292 elements.into_array(),
293 buffer![0u32, 2, 4].into_array(),
294 Validity::NonNullable,
295 )?
296 .into_array();
297 let result = list.apply(&list_sum(root()))?;
298
299 let mut ctx = array_session().create_execution_ctx();
300 let expected = PrimitiveArray::from_option_iter::<i64, _>([None, Some(3)]);
301 assert_arrays_eq!(result, expected, &mut ctx);
302 Ok(())
303 }
304
305 #[test]
306 fn test_unsigned_widening() -> VortexResult<()> {
307 let elements = PrimitiveArray::from_iter([1u8, 2, 3]);
308 let list = ListArray::try_new(
309 elements.into_array(),
310 buffer![0u32, 3].into_array(),
311 Validity::NonNullable,
312 )?
313 .into_array();
314 let result = list.apply(&list_sum(root()))?;
315
316 let mut ctx = array_session().create_execution_ctx();
317 let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(6)]);
318 assert_arrays_eq!(result, expected, &mut ctx);
319 Ok(())
320 }
321
322 #[test]
323 fn test_bool_elements() -> VortexResult<()> {
324 let elements = BoolArray::from_iter([true, true, false, true]);
325 let list = ListArray::try_new(
326 elements.into_array(),
327 buffer![0u32, 3, 4].into_array(),
328 Validity::NonNullable,
329 )?
330 .into_array();
331 let result = list.apply(&list_sum(root()))?;
332
333 let mut ctx = array_session().create_execution_ctx();
334 let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(2), Some(1)]);
335 assert_arrays_eq!(result, expected, &mut ctx);
336 Ok(())
337 }
338
339 #[test]
340 fn test_nan_skipped_by_default() -> VortexResult<()> {
341 let elements = PrimitiveArray::from_iter([1.0f64, f64::NAN, 2.0]);
342 let list = ListArray::try_new(
343 elements.into_array(),
344 buffer![0u32, 3].into_array(),
345 Validity::NonNullable,
346 )?
347 .into_array();
348 let result = list.apply(&list_sum(root()))?;
349
350 let mut ctx = array_session().create_execution_ctx();
351 let expected = PrimitiveArray::from_option_iter::<f64, _>([Some(3.0)]);
352 assert_arrays_eq!(result, expected, &mut ctx);
353 Ok(())
354 }
355
356 #[test]
357 fn test_all_nan_list_sums_to_zero() -> VortexResult<()> {
358 let elements = PrimitiveArray::from_iter([f64::NAN, f64::NAN]);
362 let list = ListArray::try_new(
363 elements.into_array(),
364 buffer![0u32, 2].into_array(),
365 Validity::NonNullable,
366 )?
367 .into_array();
368 let result = list.apply(&list_sum(root()))?;
369
370 let mut ctx = array_session().create_execution_ctx();
371 let expected = PrimitiveArray::from_option_iter::<f64, _>([Some(0.0)]);
372 assert_arrays_eq!(result, expected, &mut ctx);
373 Ok(())
374 }
375
376 #[test]
377 fn test_nan_poisons_with_include_nans() -> VortexResult<()> {
378 let elements = PrimitiveArray::from_iter([1.0f64, f64::NAN, 2.0]);
379 let list = ListArray::try_new(
380 elements.into_array(),
381 buffer![0u32, 3].into_array(),
382 Validity::NonNullable,
383 )?
384 .into_array();
385 let result = list.apply(&list_sum_opts(
386 root(),
387 NumericalAggregateOpts::include_nans(),
388 ))?;
389
390 let mut ctx = array_session().create_execution_ctx();
391 assert!(result.is_valid(0, &mut ctx)?);
392 let result = result.execute::<PrimitiveArray>(&mut ctx)?;
393 assert!(result.as_slice::<f64>()[0].is_nan());
394 Ok(())
395 }
396
397 #[test]
398 fn test_listview_sum() -> VortexResult<()> {
399 let elements = create_list_elements();
400 let lv = ListViewArray::new(
402 elements,
403 buffer![5u32, 0, 4, 1].into_array(),
404 buffer![2u32, 3, 0, 2].into_array(),
405 Validity::NonNullable,
406 )
407 .into_array();
408 let result = lv.apply(&list_sum(root()))?;
409
410 let mut ctx = array_session().create_execution_ctx();
411 let expected =
413 PrimitiveArray::from_option_iter::<i64, _>([Some(6), Some(6), None, Some(5)]);
414 assert_arrays_eq!(result, expected, &mut ctx);
415 Ok(())
416 }
417
418 fn create_fixed_size_list(validity: Validity) -> ArrayRef {
419 let elements = PrimitiveArray::from_iter([1i32, 2, 3, 4, 5, 6, 7, 8]).into_array();
421 FixedSizeListArray::new(elements, 2, validity, 4).into_array()
422 }
423
424 #[test]
425 fn test_fixed_size_list_sum() -> VortexResult<()> {
426 let fsl = create_fixed_size_list(Validity::NonNullable);
427 let result = fsl.apply(&list_sum(root()))?;
428
429 let mut ctx = array_session().create_execution_ctx();
430 let expected =
431 PrimitiveArray::from_option_iter::<i64, _>([Some(3), Some(7), Some(11), Some(15)]);
432 assert_arrays_eq!(result, expected, &mut ctx);
433 Ok(())
434 }
435
436 #[test]
437 fn test_fixed_size_list_sum_nullable() -> VortexResult<()> {
438 let fsl = create_fixed_size_list(Validity::Array(
439 BoolArray::from_iter([true, false, true, false]).into_array(),
440 ));
441 let result = fsl.apply(&list_sum(root()))?;
442
443 let mut ctx = array_session().create_execution_ctx();
444 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(3), None, Some(11), None]);
445 assert_arrays_eq!(result, expected, &mut ctx);
446 Ok(())
447 }
448
449 #[test]
450 fn test_list_sum_take() -> VortexResult<()> {
451 let elements = create_list_elements();
452 let list = ListArray::try_new(
453 elements,
454 buffer![0u32, 2, 5, 5, 7].into_array(),
455 Validity::NonNullable,
456 )?
457 .into_array();
458 let taken = list.take(buffer![3u64, 0, 2].into_array())?;
459
460 let result = taken.apply(&list_sum(root()))?;
461 let mut ctx = array_session().create_execution_ctx();
462 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(6), Some(3), None]);
463 assert_arrays_eq!(result, expected, &mut ctx);
464 Ok(())
465 }
466
467 #[test]
468 fn test_list_sum_slice() -> VortexResult<()> {
469 let elements = create_list_elements();
470 let list = ListArray::try_new(
471 elements,
472 buffer![0u32, 2, 5, 5, 7].into_array(),
473 Validity::NonNullable,
474 )?
475 .into_array();
476 let sliced = list.slice(1..4)?;
477
478 let result = sliced.apply(&list_sum(root()))?;
479 let mut ctx = array_session().create_execution_ctx();
480 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(12), None, Some(6)]);
481 assert_arrays_eq!(result, expected, &mut ctx);
482 Ok(())
483 }
484
485 #[test]
486 fn test_empty_array() -> VortexResult<()> {
487 let elements = PrimitiveArray::from_iter([0i32; 0]);
488 let list = ListArray::try_new(
489 elements.into_array(),
490 buffer![0u32].into_array(),
491 Validity::NonNullable,
492 )?
493 .into_array();
494 let result = list.apply(&list_sum(root()))?;
495
496 let mut ctx = array_session().create_execution_ctx();
497 let result = result.execute::<PrimitiveArray>(&mut ctx)?;
498 assert_eq!(result.len(), 0);
499 Ok(())
500 }
501
502 #[test]
503 fn test_constant_list_sum() -> VortexResult<()> {
504 let elements = create_list_elements();
505 let list = ListArray::try_new(
506 elements,
507 buffer![0u32, 2, 5, 5, 7].into_array(),
508 Validity::NonNullable,
509 )?
510 .into_array();
511 let mut ctx = array_session().create_execution_ctx();
512 let scalar = list.execute_scalar(1, &mut ctx)?;
513
514 let constant = ConstantArray::new(scalar, 3).into_array();
515 let result = constant.apply(&list_sum(root()))?;
516 let expected = PrimitiveArray::from_option_iter::<i64, _>([Some(12), Some(12), Some(12)]);
517 assert_arrays_eq!(result, expected, &mut ctx);
518 Ok(())
519 }
520
521 #[test]
522 fn test_null_scalar_list_sum() -> VortexResult<()> {
523 let null_scalar = Scalar::null(DType::List(
524 Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
525 Nullability::Nullable,
526 ));
527 let array = ConstantArray::new(null_scalar, 2).into_array();
528 let result = array.apply(&list_sum(root()))?;
529
530 let mut ctx = array_session().create_execution_ctx();
531 assert!(!result.is_valid(0, &mut ctx)?);
532 assert!(!result.is_valid(1, &mut ctx)?);
533 Ok(())
534 }
535
536 #[test]
537 fn test_unsupported_dtypes() {
538 let opts = NumericalAggregateOpts::default();
539 assert!(
541 ListSum
542 .return_dtype(
543 &opts,
544 &[DType::Primitive(PType::I32, Nullability::NonNullable)]
545 )
546 .is_err()
547 );
548 assert!(
550 ListSum
551 .return_dtype(
552 &opts,
553 &[DType::List(
554 Arc::new(DType::Utf8(Nullability::NonNullable)),
555 Nullability::NonNullable,
556 )],
557 )
558 .is_err()
559 );
560 }
561
562 #[test]
563 fn test_display() {
564 assert_eq!(list_sum(root()).to_string(), "vortex.list.sum($)");
565 assert_eq!(
566 list_sum_opts(root(), NumericalAggregateOpts::include_nans()).to_string(),
567 "vortex.list.sum($, opts=skip_nans=false)"
568 );
569 }
570
571 #[test]
572 fn test_proto_round_trip() -> VortexResult<()> {
573 for expr in [
574 list_sum(root()),
575 list_sum_opts(root(), NumericalAggregateOpts::include_nans()),
576 ] {
577 let proto = expr.serialize_proto()?;
578 let buf = proto.encode_to_vec();
579 let decoded = pb::Expr::decode(buf.as_slice())?;
580 let deser = Expression::from_proto(&decoded, &array_session())?;
581 assert_eq!(expr, deser);
582 }
583 Ok(())
584 }
585}