1mod grouped;
11pub(crate) use grouped::PrimitiveGroupedSumV2EncodingKernel;
12use vortex_error::VortexExpect;
13use vortex_error::VortexResult;
14use vortex_error::vortex_bail;
15use vortex_error::vortex_ensure;
16use vortex_error::vortex_err;
17use vortex_session::VortexSession;
18use vortex_session::registry::CachedId;
19
20use crate::ArrayRef;
21use crate::ArrayView;
22use crate::Canonical;
23use crate::Columnar;
24use crate::ExecutionCtx;
25use crate::aggregate_fn::Accumulator;
26use crate::aggregate_fn::AggregateArgs;
27use crate::aggregate_fn::AggregateFnId;
28use crate::aggregate_fn::AggregateFnVTable;
29use crate::aggregate_fn::DynAccumulator;
30use crate::aggregate_fn::NumericalAggregateOpts;
31use crate::aggregate_fn::fns::sum::Sum;
32use crate::aggregate_fn::fns::sum::SumState;
33use crate::aggregate_fn::fns::sum::accumulate_bool;
34use crate::aggregate_fn::fns::sum::accumulate_decimal;
35use crate::aggregate_fn::fns::sum::accumulate_primitive;
36use crate::aggregate_fn::fns::sum::checked_add_sum_states;
37use crate::aggregate_fn::fns::sum::make_zero_state;
38use crate::aggregate_fn::fns::sum::multiply_constant;
39use crate::arrays::Struct;
40use crate::arrays::struct_::StructArrayExt;
41use crate::builtins::ArrayBuiltins;
42use crate::dtype::DType;
43use crate::dtype::FieldName;
44use crate::dtype::FieldNames;
45use crate::dtype::Nullability;
46use crate::dtype::PType;
47use crate::dtype::StructFields;
48use crate::expr::stats::Precision;
49use crate::expr::stats::Stat;
50use crate::expr::stats::StatsProviderExt;
51use crate::scalar::Scalar;
52use crate::scalar::ScalarValue;
53use crate::scalar_fn::fns::operators::Operator;
54use crate::validity::Validity;
55
56const SUM_FIELD: &str = "sum";
57const IS_OVERFLOW_FIELD: &str = "is_overflow";
58const IS_EMPTY_FIELD: &str = "is_empty";
59
60pub fn sum_v2(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Scalar> {
65 let mut acc = Accumulator::try_new(
66 SumV2,
67 NumericalAggregateOpts::default(),
68 array.dtype().clone(),
69 )?;
70 acc.accumulate(array, ctx)?;
71 acc.finish()
72}
73
74#[derive(Clone, Copy, Debug)]
84pub struct SumV2;
85
86impl SumV2 {
87 pub fn partial_from_sum(sum: Scalar, is_empty: bool) -> VortexResult<Scalar> {
93 vortex_ensure!(
94 matches!(
95 sum.dtype(),
96 DType::Primitive(PType::U64 | PType::I64 | PType::F64, _)
97 ),
98 "Expected a widened primitive sum, got {}",
99 sum.dtype(),
100 );
101
102 let sum_dtype = sum.dtype().as_nonnullable();
103 let is_overflow = sum.is_null();
104 let sum = if is_overflow {
105 Scalar::zero_value(&sum_dtype)
106 } else {
107 sum.cast(&sum_dtype)?
108 };
109
110 Ok(Scalar::struct_(
111 sum_v2_partial_dtype(sum_dtype),
112 vec![
113 sum,
114 Scalar::bool(is_overflow, Nullability::NonNullable),
115 Scalar::bool(is_empty && !is_overflow, Nullability::NonNullable),
116 ],
117 ))
118 }
119}
120
121impl AggregateFnVTable for SumV2 {
122 type Options = NumericalAggregateOpts;
123 type Partial = SumV2Partial;
124
125 fn id(&self) -> AggregateFnId {
126 static ID: CachedId = CachedId::new("vortex.sum_v2");
127 *ID
128 }
129
130 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
131 Ok(Some(options.serialize()))
132 }
133
134 fn deserialize(
135 &self,
136 metadata: &[u8],
137 _session: &VortexSession,
138 ) -> VortexResult<Self::Options> {
139 NumericalAggregateOpts::deserialize(metadata)
140 }
141
142 fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
143 Sum.return_dtype(options, input_dtype)
144 }
145
146 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
147 self.return_dtype(options, input_dtype)
148 .map(sum_v2_partial_dtype)
149 }
150
151 fn empty_partial(&self, args: AggregateArgs<'_, Self::Options>) -> VortexResult<Self::Partial> {
152 Ok(SumV2Partial::empty(args.return_dtype))
153 }
154
155 fn partial_from_scalar(
156 &self,
157 args: AggregateArgs<'_, Self::Options>,
158 scalar: &Scalar,
159 ) -> VortexResult<Self::Partial> {
160 let mut partial = SumV2Partial::empty(args.return_dtype);
161 let (sum, is_overflow, is_empty) = decode_partial_scalar(scalar, args.return_dtype)?;
162
163 let overflowed = checked_add_sum_value(&mut partial.sum, args.return_dtype, sum)?;
166 partial.is_overflow = is_overflow || overflowed;
167 partial.is_empty = is_empty && !partial.is_overflow;
168 Ok(partial)
169 }
170
171 fn merge_partials(
172 &self,
173 args: AggregateArgs<'_, Self::Options>,
174 mut acc: Self::Partial,
175 partial: Self::Partial,
176 ) -> VortexResult<Self::Partial> {
177 if acc.is_overflow {
178 return Ok(acc);
179 }
180 if partial.is_overflow {
181 if acc.is_empty {
184 acc.sum = partial.sum;
185 }
186 acc.is_overflow = true;
187 acc.is_empty = false;
188 return Ok(acc);
189 }
190 if partial.is_empty {
191 return Ok(acc);
192 }
193 acc.is_overflow = checked_add_sum_states(&mut acc.sum, args.return_dtype, &partial.sum)?;
194 acc.is_empty = false;
195 Ok(acc)
196 }
197
198 fn to_scalar(
199 &self,
200 args: AggregateArgs<'_, Self::Options>,
201 partial: &Self::Partial,
202 ) -> VortexResult<Scalar> {
203 Ok(Scalar::struct_(
204 args.partial_dtype.clone(),
205 vec![
206 sum_state_scalar(partial, args.return_dtype, Nullability::NonNullable),
207 Scalar::bool(partial.is_overflow, Nullability::NonNullable),
208 Scalar::bool(partial.is_empty, Nullability::NonNullable),
209 ],
210 ))
211 }
212
213 fn is_saturated(
214 &self,
215 _args: AggregateArgs<'_, Self::Options>,
216 partial: &Self::Partial,
217 ) -> bool {
218 partial.is_overflow || matches!(&partial.sum, SumState::Float(value) if value.is_nan())
219 }
220
221 fn try_accumulate(
222 &self,
223 args: AggregateArgs<'_, Self::Options>,
224 partial: &mut Self::Partial,
225 batch: &ArrayRef,
226 _ctx: &mut ExecutionCtx,
227 ) -> VortexResult<bool> {
228 if args.options.skip_nans || !matches!(&partial.sum, SumState::Float(_)) {
229 return Ok(false);
230 }
231
232 match batch.statistics().get_as::<u64>(Stat::NaNCount) {
233 Precision::Exact(0) => Ok(false),
234 Precision::Exact(_) => {
235 let SumState::Float(sum) = &mut partial.sum else {
236 unreachable!("checked float sum state")
237 };
238 *sum = f64::NAN;
239 partial.is_empty = false;
240 Ok(true)
241 }
242 _ => Ok(false),
243 }
244 }
245
246 fn accumulate(
247 &self,
248 args: AggregateArgs<'_, Self::Options>,
249 partial: &mut Self::Partial,
250 batch: &Columnar,
251 ctx: &mut ExecutionCtx,
252 ) -> VortexResult<()> {
253 if partial.is_overflow {
254 return Ok(());
255 }
256
257 if let Columnar::Constant(constant) = batch {
258 if !constant.scalar().is_null() && !constant.is_empty() {
259 partial.is_empty = false;
260 }
261 if args.options.skip_nans
262 && constant
263 .scalar()
264 .as_primitive_opt()
265 .is_some_and(|primitive| primitive.is_nan())
266 {
267 return Ok(());
268 }
269 if let Some(product) =
270 multiply_constant(constant.scalar(), constant.len(), args.return_dtype)?
271 {
272 if product.is_null() {
273 partial.is_overflow = true;
274 partial.is_empty = false;
275 } else {
276 partial.is_overflow =
277 checked_add_sum_state(&mut partial.sum, args.return_dtype, &product)?;
278 partial.is_empty = false;
279 }
280 }
281 return Ok(());
282 }
283
284 let any_valid = partial.is_empty && has_valid_value(batch, ctx)?;
285 let result = match batch {
286 Columnar::Canonical(canonical) => match canonical {
287 Canonical::Primitive(array) => {
288 accumulate_primitive(&mut partial.sum, array, ctx, args.options.skip_nans)
289 }
290 Canonical::Bool(array) => accumulate_bool(&mut partial.sum, array, ctx),
291 Canonical::Decimal(array) => {
292 accumulate_decimal(&mut partial.sum, args.return_dtype, array, ctx)
293 }
294 _ => vortex_bail!("Unsupported canonical type for sum_v2: {}", batch.dtype()),
295 },
296 Columnar::Constant(_) => unreachable!(),
297 };
298
299 if any_valid {
300 partial.is_empty = false;
301 }
302 if result? {
303 partial.is_overflow = true;
304 partial.is_empty = false;
305 }
306 Ok(())
307 }
308
309 fn finalize(
310 &self,
311 _args: AggregateArgs<'_, Self::Options>,
312 partials: ArrayRef,
313 ) -> VortexResult<ArrayRef> {
314 if let Some(partials) = partials.as_opt::<Struct>() {
315 return finalize_struct(partials);
316 }
317
318 let sum = partials.get_item(SUM_FIELD)?;
319 let is_invalid = partials
320 .get_item(IS_OVERFLOW_FIELD)?
321 .binary(partials.get_item(IS_EMPTY_FIELD)?, Operator::Or)?
322 .fill_null(true)?;
323 sum.mask(is_invalid.not()?)
324 }
325
326 fn finalize_scalar(
327 &self,
328 args: AggregateArgs<'_, Self::Options>,
329 partial: &Self::Partial,
330 ) -> VortexResult<Scalar> {
331 if partial.is_overflow || partial.is_empty {
332 return Ok(Scalar::null(args.return_dtype.as_nullable()));
333 }
334 Ok(sum_state_scalar(
335 partial,
336 args.return_dtype,
337 Nullability::Nullable,
338 ))
339 }
340}
341
342fn finalize_struct(partials: ArrayView<'_, Struct>) -> VortexResult<ArrayRef> {
343 let sum = partials.unmasked_field_by_name(SUM_FIELD)?.clone();
344 let is_overflow = partials.unmasked_field_by_name(IS_OVERFLOW_FIELD)?.clone();
345 let is_empty = partials.unmasked_field_by_name(IS_EMPTY_FIELD)?.clone();
346
347 let is_invalid = is_overflow
348 .binary(is_empty, Operator::Or)?
349 .fill_null(true)?;
350 let mut is_valid = is_invalid.not()?;
351 match partials.struct_validity() {
352 Validity::NonNullable | Validity::AllValid => {}
353 validity => {
354 is_valid = is_valid.binary(validity.to_array(partials.len()), Operator::And)?;
355 }
356 }
357
358 sum.mask(is_valid)
359}
360
361pub struct SumV2Partial {
363 sum: SumState,
364 is_overflow: bool,
365 is_empty: bool,
366}
367
368impl SumV2Partial {
369 fn empty(return_dtype: &DType) -> Self {
371 Self {
372 sum: make_zero_state(return_dtype),
373 is_overflow: false,
374 is_empty: true,
375 }
376 }
377}
378
379fn has_valid_value(batch: &Columnar, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
380 let (validity, len) = match batch {
381 Columnar::Canonical(Canonical::Primitive(array)) => {
382 (array.as_ref().validity()?, array.as_ref().len())
383 }
384 Columnar::Canonical(Canonical::Bool(array)) => {
385 (array.as_ref().validity()?, array.as_ref().len())
386 }
387 Columnar::Canonical(Canonical::Decimal(array)) => {
388 (array.as_ref().validity()?, array.as_ref().len())
389 }
390 Columnar::Canonical(_) => return Ok(false),
391 Columnar::Constant(constant) => {
392 return Ok(!constant.is_empty() && !constant.scalar().is_null());
393 }
394 };
395 Ok(validity.execute_mask(len, ctx)?.true_count() > 0)
396}
397
398fn decode_partial_scalar<'a>(
403 scalar: &'a Scalar,
404 return_dtype: &DType,
405) -> VortexResult<(&'a ScalarValue, bool, bool)> {
406 vortex_ensure!(!scalar.is_null(), "SumV2 partial must not be null");
407
408 let (DType::Struct(fields, _), Some(ScalarValue::Tuple(values))) =
409 (scalar.dtype(), scalar.value())
410 else {
411 vortex_bail!("SumV2 partial must be a struct, got {}", scalar.dtype());
412 };
413
414 let field_index = |name: &str| {
415 fields
416 .find(name)
417 .ok_or_else(|| vortex_err!("SumV2 partial is missing the {name} field"))
418 };
419 let bool_field = |name: &str| -> VortexResult<bool> {
420 match &values[field_index(name)?] {
421 Some(ScalarValue::Bool(value)) => Ok(*value),
422 _ => vortex_bail!("SumV2 partial field {name} must be a non-null bool"),
423 }
424 };
425
426 let sum_index = field_index(SUM_FIELD)?;
427 let sum_dtype = fields
428 .field_dtypes()
429 .nth(sum_index)
430 .vortex_expect("field index from find is in bounds");
431 validate_sum_field_dtype(sum_dtype, return_dtype)?;
432
433 let sum = values[sum_index]
434 .as_ref()
435 .ok_or_else(|| vortex_err!("SumV2 partial sum must not be null"))?;
436
437 Ok((
438 sum,
439 bool_field(IS_OVERFLOW_FIELD)?,
440 bool_field(IS_EMPTY_FIELD)?,
441 ))
442}
443
444fn validate_sum_field_dtype(sum_dtype: &DType, return_dtype: &DType) -> VortexResult<()> {
445 vortex_ensure!(
446 sum_dtype.nullability() == Nullability::NonNullable
447 && sum_dtype.eq_ignore_nullability(return_dtype),
448 "SumV2 partial value has dtype {}, expected {}",
449 sum_dtype,
450 return_dtype.as_nonnullable(),
451 );
452 Ok(())
453}
454
455fn checked_add_sum_state(
456 state: &mut SumState,
457 return_dtype: &DType,
458 other: &Scalar,
459) -> VortexResult<bool> {
460 let other = other
461 .value()
462 .ok_or_else(|| vortex_err!("Can't extract present value from null scalar"))?;
463 checked_add_sum_value(state, return_dtype, other)
464}
465
466fn checked_add_sum_value(
467 state: &mut SumState,
468 return_dtype: &DType,
469 other: &ScalarValue,
470) -> VortexResult<bool> {
471 Ok(match (state, other) {
472 (SumState::Unsigned(sum), ScalarValue::Primitive(other)) => {
473 checked_add_u64(sum, other.cast::<u64>()?)
474 }
475 (SumState::Signed(sum), ScalarValue::Primitive(other)) => {
476 checked_add_i64(sum, other.cast::<i64>()?)
477 }
478 (SumState::Float(sum), ScalarValue::Primitive(other)) => {
479 *sum += other.cast::<f64>()?;
480 false
481 }
482 (SumState::Decimal(value), ScalarValue::Decimal(other)) => {
483 let dtype = return_dtype
484 .as_decimal_opt()
485 .vortex_expect("decimal sum result dtype");
486 match value.checked_add(other) {
487 Some(result) if result.fits_in_precision(*dtype) => {
488 *value = result;
489 false
490 }
491 Some(_) | None => true,
492 }
493 }
494 (_, other) => vortex_bail!("SumV2 cannot add {other} to a {return_dtype} sum"),
495 })
496}
497
498fn sum_v2_partial_dtype(sum_dtype: DType) -> DType {
499 DType::Struct(sum_v2_partial_fields(sum_dtype), Nullability::Nullable)
500}
501
502fn sum_v2_partial_fields(sum_dtype: DType) -> StructFields {
503 StructFields::new(
504 FieldNames::from_iter([
505 FieldName::from(SUM_FIELD),
506 FieldName::from(IS_OVERFLOW_FIELD),
507 FieldName::from(IS_EMPTY_FIELD),
508 ]),
509 vec![
510 sum_dtype.as_nonnullable(),
511 DType::Bool(Nullability::NonNullable),
512 DType::Bool(Nullability::NonNullable),
513 ],
514 )
515}
516
517fn sum_state_scalar(
518 partial: &SumV2Partial,
519 return_dtype: &DType,
520 nullability: Nullability,
521) -> Scalar {
522 match &partial.sum {
523 SumState::Unsigned(value) => Scalar::primitive(*value, nullability),
524 SumState::Signed(value) => Scalar::primitive(*value, nullability),
525 SumState::Float(value) => Scalar::primitive(*value, nullability),
526 SumState::Decimal(value) => Scalar::decimal(
527 *value,
528 *return_dtype
529 .as_decimal_opt()
530 .vortex_expect("decimal sum result dtype"),
531 nullability,
532 ),
533 }
534}
535
536fn checked_add_u64(sum: &mut u64, value: u64) -> bool {
537 match sum.checked_add(value) {
538 Some(result) => {
539 *sum = result;
540 false
541 }
542 None => true,
543 }
544}
545
546fn checked_add_i64(sum: &mut i64, value: i64) -> bool {
547 match sum.checked_add(value) {
548 Some(result) => {
549 *sum = result;
550 false
551 }
552 None => true,
553 }
554}
555
556#[cfg(test)]
557mod tests;