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::DecimalValue;
52use crate::scalar::Scalar;
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)?;
162 validate_sum_field_dtype(&sum, args.return_dtype)?;
163
164 let overflowed = checked_add_sum_state(&mut partial.sum, args.return_dtype, &sum)?;
167 partial.is_overflow = is_overflow || overflowed;
168 partial.is_empty = is_empty && !partial.is_overflow;
169 Ok(partial)
170 }
171
172 fn merge_partials(
173 &self,
174 args: AggregateArgs<'_, Self::Options>,
175 mut acc: Self::Partial,
176 partial: Self::Partial,
177 ) -> VortexResult<Self::Partial> {
178 if acc.is_overflow {
179 return Ok(acc);
180 }
181 if partial.is_overflow {
182 if acc.is_empty {
185 acc.sum = partial.sum;
186 }
187 acc.is_overflow = true;
188 acc.is_empty = false;
189 return Ok(acc);
190 }
191 if partial.is_empty {
192 return Ok(acc);
193 }
194 acc.is_overflow = checked_add_sum_states(&mut acc.sum, args.return_dtype, &partial.sum)?;
195 acc.is_empty = false;
196 Ok(acc)
197 }
198
199 fn to_scalar(
200 &self,
201 args: AggregateArgs<'_, Self::Options>,
202 partial: &Self::Partial,
203 ) -> VortexResult<Scalar> {
204 Ok(Scalar::struct_(
205 args.partial_dtype.clone(),
206 vec![
207 sum_state_scalar(partial, args.return_dtype, Nullability::NonNullable),
208 Scalar::bool(partial.is_overflow, Nullability::NonNullable),
209 Scalar::bool(partial.is_empty, Nullability::NonNullable),
210 ],
211 ))
212 }
213
214 fn is_saturated(
215 &self,
216 _args: AggregateArgs<'_, Self::Options>,
217 partial: &Self::Partial,
218 ) -> bool {
219 partial.is_overflow || matches!(&partial.sum, SumState::Float(value) if value.is_nan())
220 }
221
222 fn try_accumulate(
223 &self,
224 args: AggregateArgs<'_, Self::Options>,
225 partial: &mut Self::Partial,
226 batch: &ArrayRef,
227 _ctx: &mut ExecutionCtx,
228 ) -> VortexResult<bool> {
229 if args.options.skip_nans || !matches!(&partial.sum, SumState::Float(_)) {
230 return Ok(false);
231 }
232
233 match batch.statistics().get_as::<u64>(Stat::NaNCount) {
234 Precision::Exact(0) => Ok(false),
235 Precision::Exact(_) => {
236 let SumState::Float(sum) = &mut partial.sum else {
237 unreachable!("checked float sum state")
238 };
239 *sum = f64::NAN;
240 partial.is_empty = false;
241 Ok(true)
242 }
243 _ => Ok(false),
244 }
245 }
246
247 fn accumulate(
248 &self,
249 args: AggregateArgs<'_, Self::Options>,
250 partial: &mut Self::Partial,
251 batch: &Columnar,
252 ctx: &mut ExecutionCtx,
253 ) -> VortexResult<()> {
254 if partial.is_overflow {
255 return Ok(());
256 }
257
258 if let Columnar::Constant(constant) = batch {
259 if !constant.scalar().is_null() && !constant.is_empty() {
260 partial.is_empty = false;
261 }
262 if args.options.skip_nans
263 && constant
264 .scalar()
265 .as_primitive_opt()
266 .is_some_and(|primitive| primitive.is_nan())
267 {
268 return Ok(());
269 }
270 if let Some(product) =
271 multiply_constant(constant.scalar(), constant.len(), args.return_dtype)?
272 {
273 if product.is_null() {
274 partial.is_overflow = true;
275 partial.is_empty = false;
276 } else {
277 partial.is_overflow =
278 checked_add_sum_state(&mut partial.sum, args.return_dtype, &product)?;
279 partial.is_empty = false;
280 }
281 }
282 return Ok(());
283 }
284
285 let any_valid = partial.is_empty && has_valid_value(batch, ctx)?;
286 let result = match batch {
287 Columnar::Canonical(canonical) => match canonical {
288 Canonical::Primitive(array) => {
289 accumulate_primitive(&mut partial.sum, array, ctx, args.options.skip_nans)
290 }
291 Canonical::Bool(array) => accumulate_bool(&mut partial.sum, array, ctx),
292 Canonical::Decimal(array) => {
293 accumulate_decimal(&mut partial.sum, args.return_dtype, array, ctx)
294 }
295 _ => vortex_bail!("Unsupported canonical type for sum_v2: {}", batch.dtype()),
296 },
297 Columnar::Constant(_) => unreachable!(),
298 };
299
300 if any_valid {
301 partial.is_empty = false;
302 }
303 if result? {
304 partial.is_overflow = true;
305 partial.is_empty = false;
306 }
307 Ok(())
308 }
309
310 fn finalize(
311 &self,
312 _args: AggregateArgs<'_, Self::Options>,
313 partials: ArrayRef,
314 ) -> VortexResult<ArrayRef> {
315 if let Some(partials) = partials.as_opt::<Struct>() {
316 return finalize_struct(partials);
317 }
318
319 let sum = partials.get_item(SUM_FIELD)?;
320 let is_invalid = partials
321 .get_item(IS_OVERFLOW_FIELD)?
322 .binary(partials.get_item(IS_EMPTY_FIELD)?, Operator::Or)?
323 .fill_null(true)?;
324 sum.mask(is_invalid.not()?)
325 }
326
327 fn finalize_scalar(
328 &self,
329 args: AggregateArgs<'_, Self::Options>,
330 partial: &Self::Partial,
331 ) -> VortexResult<Scalar> {
332 if partial.is_overflow || partial.is_empty {
333 return Ok(Scalar::null(args.return_dtype.as_nullable()));
334 }
335 Ok(sum_state_scalar(
336 partial,
337 args.return_dtype,
338 Nullability::Nullable,
339 ))
340 }
341}
342
343fn finalize_struct(partials: ArrayView<'_, Struct>) -> VortexResult<ArrayRef> {
344 let sum = partials.unmasked_field_by_name(SUM_FIELD)?.clone();
345 let is_overflow = partials.unmasked_field_by_name(IS_OVERFLOW_FIELD)?.clone();
346 let is_empty = partials.unmasked_field_by_name(IS_EMPTY_FIELD)?.clone();
347
348 let is_invalid = is_overflow
349 .binary(is_empty, Operator::Or)?
350 .fill_null(true)?;
351 let mut is_valid = is_invalid.not()?;
352 match partials.struct_validity() {
353 Validity::NonNullable | Validity::AllValid => {}
354 validity => {
355 is_valid = is_valid.binary(validity.to_array(partials.len()), Operator::And)?;
356 }
357 }
358
359 sum.mask(is_valid)
360}
361
362pub struct SumV2Partial {
364 sum: SumState,
365 is_overflow: bool,
366 is_empty: bool,
367}
368
369impl SumV2Partial {
370 fn empty(return_dtype: &DType) -> Self {
372 Self {
373 sum: make_zero_state(return_dtype),
374 is_overflow: false,
375 is_empty: true,
376 }
377 }
378}
379
380fn has_valid_value(batch: &Columnar, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
381 let (validity, len) = match batch {
382 Columnar::Canonical(Canonical::Primitive(array)) => {
383 (array.as_ref().validity()?, array.as_ref().len())
384 }
385 Columnar::Canonical(Canonical::Bool(array)) => {
386 (array.as_ref().validity()?, array.as_ref().len())
387 }
388 Columnar::Canonical(Canonical::Decimal(array)) => {
389 (array.as_ref().validity()?, array.as_ref().len())
390 }
391 Columnar::Canonical(_) => return Ok(false),
392 Columnar::Constant(constant) => {
393 return Ok(!constant.is_empty() && !constant.scalar().is_null());
394 }
395 };
396 Ok(validity.execute_mask(len, ctx)?.true_count() > 0)
397}
398
399fn decode_partial_scalar(scalar: Scalar) -> VortexResult<(Scalar, bool, bool)> {
400 vortex_ensure!(!scalar.is_null(), "SumV2 partial must not be null");
401
402 let Some(fields) = scalar.as_struct_opt() else {
403 vortex_bail!("SumV2 partial must be a struct, got {}", scalar.dtype());
404 };
405 let sum = fields
406 .field(SUM_FIELD)
407 .ok_or_else(|| vortex_err!("SumV2 partial is missing the sum field"))?;
408 let is_overflow = bool::try_from(
409 &fields
410 .field(IS_OVERFLOW_FIELD)
411 .ok_or_else(|| vortex_err!("SumV2 partial is missing the is_overflow field"))?,
412 )?;
413 let is_empty = bool::try_from(
414 &fields
415 .field(IS_EMPTY_FIELD)
416 .ok_or_else(|| vortex_err!("SumV2 partial is missing the is_empty field"))?,
417 )?;
418
419 Ok((sum, is_overflow, is_empty))
420}
421
422fn validate_sum_field_dtype(sum: &Scalar, return_dtype: &DType) -> VortexResult<()> {
423 vortex_ensure!(
424 sum.dtype().nullability() == Nullability::NonNullable
425 && sum.dtype().eq_ignore_nullability(return_dtype),
426 "SumV2 partial value has dtype {}, expected {}",
427 sum.dtype(),
428 return_dtype.as_nonnullable(),
429 );
430 Ok(())
431}
432
433fn checked_add_sum_state(
434 state: &mut SumState,
435 return_dtype: &DType,
436 other: &Scalar,
437) -> VortexResult<bool> {
438 Ok(match state {
439 SumState::Unsigned(sum) => checked_add_u64(sum, u64::try_from(other)?),
440 SumState::Signed(sum) => checked_add_i64(sum, i64::try_from(other)?),
441 SumState::Float(sum) => {
442 *sum += f64::try_from(other)?;
443 false
444 }
445 SumState::Decimal(value) => {
446 let dtype = return_dtype
447 .as_decimal_opt()
448 .vortex_expect("decimal sum result dtype");
449 let other = DecimalValue::try_from(other)?;
450 match value.checked_add(&other) {
451 Some(result) if result.fits_in_precision(*dtype) => {
452 *value = result;
453 false
454 }
455 Some(_) | None => true,
456 }
457 }
458 })
459}
460
461fn sum_v2_partial_dtype(sum_dtype: DType) -> DType {
462 DType::Struct(sum_v2_partial_fields(sum_dtype), Nullability::Nullable)
463}
464
465fn sum_v2_partial_fields(sum_dtype: DType) -> StructFields {
466 StructFields::new(
467 FieldNames::from_iter([
468 FieldName::from(SUM_FIELD),
469 FieldName::from(IS_OVERFLOW_FIELD),
470 FieldName::from(IS_EMPTY_FIELD),
471 ]),
472 vec![
473 sum_dtype.as_nonnullable(),
474 DType::Bool(Nullability::NonNullable),
475 DType::Bool(Nullability::NonNullable),
476 ],
477 )
478}
479
480fn sum_state_scalar(
481 partial: &SumV2Partial,
482 return_dtype: &DType,
483 nullability: Nullability,
484) -> Scalar {
485 match &partial.sum {
486 SumState::Unsigned(value) => Scalar::primitive(*value, nullability),
487 SumState::Signed(value) => Scalar::primitive(*value, nullability),
488 SumState::Float(value) => Scalar::primitive(*value, nullability),
489 SumState::Decimal(value) => Scalar::decimal(
490 *value,
491 *return_dtype
492 .as_decimal_opt()
493 .vortex_expect("decimal sum result dtype"),
494 nullability,
495 ),
496 }
497}
498
499fn checked_add_u64(sum: &mut u64, value: u64) -> bool {
500 match sum.checked_add(value) {
501 Some(result) => {
502 *sum = result;
503 false
504 }
505 None => true,
506 }
507}
508
509fn checked_add_i64(sum: &mut i64, value: i64) -> bool {
510 match sum.checked_add(value) {
511 Some(result) => {
512 *sum = result;
513 false
514 }
515 None => true,
516 }
517}
518
519#[cfg(test)]
520mod tests;