Skip to content

Commit e2b15cb

Browse files
committed
perf: streamline sum partial handling
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent bf3c8f5 commit e2b15cb

3 files changed

Lines changed: 125 additions & 124 deletions

File tree

vortex-array/src/aggregate_fn/fns/sum/grouped.rs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ use vortex_buffer::BitBufferMut;
66
use vortex_error::VortexResult;
77
use vortex_mask::Mask;
88

9-
use super::IS_EMPTY_FIELD;
10-
use super::IS_OVERFLOW_FIELD;
11-
use super::SUM_FIELD;
129
use super::Sum;
1310
use super::primitive::sum_float_all;
1411
use super::primitive::sum_signed_all;
1512
use super::primitive::sum_unsigned_all;
13+
use super::sum_partial_fields;
1614
use crate::ArrayRef;
1715
use crate::ExecutionCtx;
1816
use crate::IntoArray;
@@ -24,8 +22,6 @@ use crate::arrays::BoolArray;
2422
use crate::arrays::Primitive;
2523
use crate::arrays::PrimitiveArray;
2624
use crate::arrays::StructArray;
27-
use crate::dtype::FieldName;
28-
use crate::dtype::FieldNames;
2925
use crate::dtype::NativePType;
3026
use crate::dtype::Nullability;
3127
use crate::match_each_native_ptype;
@@ -109,20 +105,22 @@ fn grouped_sum(
109105
}
110106
);
111107

112-
Ok(StructArray::try_new(
113-
FieldNames::from_iter([
114-
FieldName::from(SUM_FIELD),
115-
FieldName::from(IS_OVERFLOW_FIELD),
116-
FieldName::from(IS_EMPTY_FIELD),
117-
]),
118-
vec![
119-
sums.into_array(),
120-
BoolArray::new(is_overflow, Validity::NonNullable).into_array(),
121-
BoolArray::new(is_empty, Validity::NonNullable).into_array(),
122-
],
123-
group_validity.len(),
124-
Validity::from_mask(group_validity.clone(), Nullability::Nullable),
125-
)?
108+
let partial_fields = sum_partial_fields(sums.dtype().clone());
109+
110+
// SAFETY: all three children have one value per group and match `partial_fields`; the struct
111+
// validity is derived from the same group count.
112+
Ok(unsafe {
113+
StructArray::new_unchecked(
114+
vec![
115+
sums.into_array(),
116+
BoolArray::new(is_overflow, Validity::NonNullable).into_array(),
117+
BoolArray::new(is_empty, Validity::NonNullable).into_array(),
118+
],
119+
partial_fields,
120+
group_validity.len(),
121+
Validity::from_mask(group_validity.clone(), Nullability::Nullable),
122+
)
123+
}
126124
.into_array())
127125
}
128126

vortex-array/src/aggregate_fn/fns/sum/mod.rs

Lines changed: 85 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use std::fmt::Formatter;
1212

1313
pub(crate) use grouped::PrimitiveGroupedSumEncodingKernel;
1414
use prost::Message;
15-
use vortex_error::VortexExpect;
1615
use vortex_error::VortexResult;
1716
use vortex_error::vortex_bail;
17+
use vortex_error::vortex_ensure;
1818
use vortex_error::vortex_err;
1919
use vortex_error::vortex_panic;
2020
use vortex_proto::expr as pb;
@@ -250,85 +250,31 @@ impl AggregateFnVTable for Sum {
250250
}
251251

252252
fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
253-
let other = other.cast(&sum_partial_dtype(partial.return_dtype.clone()))?;
254-
if other.is_null() {
255-
partial.is_empty = false;
256-
partial.is_overflow = true;
253+
let (other_sum, other_is_overflow, other_is_empty) = decode_sum_partial_scalar(other)?;
254+
validate_sum_field_dtype(&other_sum, &partial.return_dtype)?;
255+
256+
if partial.is_overflow {
257257
return Ok(());
258258
}
259-
260-
let fields = other.as_struct();
261-
let other = fields
262-
.field(SUM_FIELD)
263-
.ok_or_else(|| vortex_err!("Sum partial is missing the `{SUM_FIELD}` field"))?;
264-
let other_is_overflow = fields
265-
.field(IS_OVERFLOW_FIELD)
266-
.and_then(|is_overflow| is_overflow.as_bool().value())
267-
.ok_or_else(|| vortex_err!("Sum partial has an invalid `{IS_OVERFLOW_FIELD}` field"))?;
268-
let other_is_empty = fields
269-
.field(IS_EMPTY_FIELD)
270-
.and_then(|is_empty| is_empty.as_bool().value())
271-
.ok_or_else(|| vortex_err!("Sum partial has an invalid `{IS_EMPTY_FIELD}` field"))?;
272-
273-
partial.is_empty &= other_is_empty;
274-
if partial.is_overflow || other_is_overflow {
259+
if other_is_overflow {
275260
partial.is_overflow = true;
261+
partial.is_empty = false;
276262
return Ok(());
277263
}
278264
if other_is_empty {
279265
return Ok(());
280266
}
281267

282-
let saturated = match &mut partial.sum {
283-
SumState::Unsigned(acc) => {
284-
let val = other
285-
.as_primitive()
286-
.typed_value::<u64>()
287-
.vortex_expect("checked non-null");
288-
checked_add_u64(acc, val)
289-
}
290-
SumState::Signed(acc) => {
291-
let val = other
292-
.as_primitive()
293-
.typed_value::<i64>()
294-
.vortex_expect("checked non-null");
295-
checked_add_i64(acc, val)
296-
}
297-
SumState::Float(acc) => {
298-
let val = other
299-
.as_primitive()
300-
.typed_value::<f64>()
301-
.vortex_expect("checked non-null");
302-
*acc += val;
303-
false
304-
}
305-
SumState::Decimal { value, dtype } => {
306-
let val = other
307-
.as_decimal()
308-
.decimal_value()
309-
.vortex_expect("checked non-null");
310-
match value.checked_add(&val) {
311-
Some(r) if r.fits_in_precision(*dtype) => {
312-
*value = r;
313-
false
314-
}
315-
Some(_) | None => true,
316-
}
317-
}
318-
};
319-
if saturated {
320-
partial.is_overflow = true;
321-
} else {
322-
partial.is_empty = false;
323-
}
268+
partial.is_overflow = checked_add_sum_state(&mut partial.sum, &other_sum)?;
269+
partial.is_empty = false;
324270
Ok(())
325271
}
326272

327273
fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
328274
Ok(Scalar::struct_(
329275
sum_partial_dtype(partial.return_dtype.clone()),
330276
vec![
331-
sum_state_scalar(partial),
277+
sum_state_scalar(partial, Nullability::NonNullable),
332278
Scalar::bool(partial.is_overflow, Nullability::NonNullable),
333279
Scalar::bool(partial.is_empty, Nullability::NonNullable),
334280
],
@@ -481,6 +427,60 @@ pub enum SumState {
481427
},
482428
}
483429

430+
fn decode_sum_partial_scalar(scalar: Scalar) -> VortexResult<(Scalar, bool, bool)> {
431+
vortex_ensure!(!scalar.is_null(), "Sum partial must not be null");
432+
433+
let Some(fields) = scalar.as_struct_opt() else {
434+
vortex_bail!("Sum partial must be a struct, got {}", scalar.dtype());
435+
};
436+
let sum = fields
437+
.field(SUM_FIELD)
438+
.ok_or_else(|| vortex_err!("Sum partial is missing the `{SUM_FIELD}` field"))?;
439+
let is_overflow =
440+
bool::try_from(&fields.field(IS_OVERFLOW_FIELD).ok_or_else(|| {
441+
vortex_err!("Sum partial is missing the `{IS_OVERFLOW_FIELD}` field")
442+
})?)?;
443+
let is_empty = bool::try_from(
444+
&fields
445+
.field(IS_EMPTY_FIELD)
446+
.ok_or_else(|| vortex_err!("Sum partial is missing the `{IS_EMPTY_FIELD}` field"))?,
447+
)?;
448+
449+
Ok((sum, is_overflow, is_empty))
450+
}
451+
452+
fn validate_sum_field_dtype(sum: &Scalar, return_dtype: &DType) -> VortexResult<()> {
453+
vortex_ensure!(
454+
sum.dtype().nullability() == Nullability::NonNullable
455+
&& sum.dtype().eq_ignore_nullability(return_dtype),
456+
"Sum partial value has dtype {}, expected {}",
457+
sum.dtype(),
458+
return_dtype.as_nonnullable(),
459+
);
460+
Ok(())
461+
}
462+
463+
fn checked_add_sum_state(state: &mut SumState, other: &Scalar) -> VortexResult<bool> {
464+
Ok(match state {
465+
SumState::Unsigned(acc) => checked_add_u64(acc, u64::try_from(other)?),
466+
SumState::Signed(acc) => checked_add_i64(acc, i64::try_from(other)?),
467+
SumState::Float(acc) => {
468+
*acc += f64::try_from(other)?;
469+
false
470+
}
471+
SumState::Decimal { value, dtype } => {
472+
let other = DecimalValue::try_from(other)?;
473+
match value.checked_add(&other) {
474+
Some(result) if result.fits_in_precision(*dtype) => {
475+
*value = result;
476+
false
477+
}
478+
Some(_) | None => true,
479+
}
480+
}
481+
})
482+
}
483+
484484
fn make_zero_state(return_dtype: &DType) -> SumState {
485485
match return_dtype {
486486
DType::Primitive(ptype, _) => match ptype {
@@ -497,35 +497,30 @@ fn make_zero_state(return_dtype: &DType) -> SumState {
497497
}
498498

499499
fn sum_partial_dtype(sum_dtype: DType) -> DType {
500-
DType::Struct(
501-
StructFields::new(
502-
FieldNames::from_iter([
503-
FieldName::from(SUM_FIELD),
504-
FieldName::from(IS_OVERFLOW_FIELD),
505-
FieldName::from(IS_EMPTY_FIELD),
506-
]),
507-
vec![
508-
sum_dtype.as_nonnullable(),
509-
DType::Bool(Nullability::NonNullable),
510-
DType::Bool(Nullability::NonNullable),
511-
],
512-
),
513-
Nullability::Nullable,
500+
DType::Struct(sum_partial_fields(sum_dtype), Nullability::Nullable)
501+
}
502+
503+
fn sum_partial_fields(sum_dtype: DType) -> StructFields {
504+
StructFields::new(
505+
FieldNames::from_iter([
506+
FieldName::from(SUM_FIELD),
507+
FieldName::from(IS_OVERFLOW_FIELD),
508+
FieldName::from(IS_EMPTY_FIELD),
509+
]),
510+
vec![
511+
sum_dtype.as_nonnullable(),
512+
DType::Bool(Nullability::NonNullable),
513+
DType::Bool(Nullability::NonNullable),
514+
],
514515
)
515516
}
516517

517-
fn sum_state_scalar(partial: &SumPartial) -> Scalar {
518+
fn sum_state_scalar(partial: &SumPartial, nullability: Nullability) -> Scalar {
518519
match &partial.sum {
519-
SumState::Unsigned(v) => Scalar::primitive(*v, Nullability::NonNullable),
520-
SumState::Signed(v) => Scalar::primitive(*v, Nullability::NonNullable),
521-
SumState::Float(v) => Scalar::primitive(*v, Nullability::NonNullable),
522-
SumState::Decimal { value, .. } => {
523-
let decimal_dtype = *partial
524-
.return_dtype
525-
.as_decimal_opt()
526-
.vortex_expect("return dtype must be decimal");
527-
Scalar::decimal(*value, decimal_dtype, Nullability::NonNullable)
528-
}
520+
SumState::Unsigned(v) => Scalar::primitive(*v, nullability),
521+
SumState::Signed(v) => Scalar::primitive(*v, nullability),
522+
SumState::Float(v) => Scalar::primitive(*v, nullability),
523+
SumState::Decimal { value, dtype } => Scalar::decimal(*value, *dtype, nullability),
529524
}
530525
}
531526

@@ -534,22 +529,7 @@ fn sum_value_scalar(partial: &SumPartial) -> Scalar {
534529
return Scalar::null(partial.return_dtype.as_nullable());
535530
}
536531

537-
nullable_sum_state_scalar(partial)
538-
}
539-
540-
fn nullable_sum_state_scalar(partial: &SumPartial) -> Scalar {
541-
match &partial.sum {
542-
SumState::Unsigned(v) => Scalar::primitive(*v, Nullability::Nullable),
543-
SumState::Signed(v) => Scalar::primitive(*v, Nullability::Nullable),
544-
SumState::Float(v) => Scalar::primitive(*v, Nullability::Nullable),
545-
SumState::Decimal { value, .. } => {
546-
let decimal_dtype = *partial
547-
.return_dtype
548-
.as_decimal_opt()
549-
.vortex_expect("return dtype must be decimal");
550-
Scalar::decimal(*value, decimal_dtype, Nullability::Nullable)
551-
}
552-
}
532+
sum_state_scalar(partial, Nullability::Nullable)
553533
}
554534

555535
/// Convert scalar legacy Sum partials read from storage (a single nullable primitive) to the struct partial shape.

vortex-array/src/aggregate_fn/fns/sum/tests.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@ fn legacy_options_only_describe_the_stored_partial() -> VortexResult<()> {
113113

114114
// State algebra: the `{sum, is_overflow, is_empty}` monoid.
115115

116+
#[test]
117+
fn sum_rejects_null_partial() -> VortexResult<()> {
118+
let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
119+
let mut state = Sum.empty_partial(&SumAggregateOpts::default(), &dtype)?;
120+
let partial_dtype = Sum.to_scalar(&state)?.dtype().clone();
121+
122+
assert!(
123+
Sum.combine_partials(&mut state, Scalar::null(partial_dtype))
124+
.is_err()
125+
);
126+
Ok(())
127+
}
128+
129+
#[test]
130+
fn sum_rejects_partial_with_wrong_sum_dtype() -> VortexResult<()> {
131+
let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
132+
let mut state = Sum.empty_partial(&SumAggregateOpts::default(), &dtype)?;
133+
let wrong_partial = partial_with_value(Scalar::primitive(1i32, Nullable))?;
134+
135+
assert!(Sum.combine_partials(&mut state, wrong_partial).is_err());
136+
Ok(())
137+
}
138+
116139
#[test]
117140
fn sum_state_empty_is_null() -> VortexResult<()> {
118141
// A state that never saw a valid value finalizes to null, and combining empty states

0 commit comments

Comments
 (0)