Skip to content

Commit aa967fc

Browse files
committed
Add pruning aggregate functions
Signed-off-by: Nicholas Gates <nick@nickgates.com>
1 parent 2138a72 commit aa967fc

16 files changed

Lines changed: 1838 additions & 19 deletions

File tree

vortex-array/public-api.lock

Lines changed: 524 additions & 0 deletions
Large diffs are not rendered by default.

vortex-array/src/aggregate_fn/accumulator.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,19 @@ impl<V: AggregateFnVTable> DynAccumulator for Accumulator<V> {
124124
if let Some(stat) = Stat::from_aggregate_fn(&self.aggregate_fn)
125125
&& let Some(Precision::Exact(partial)) = batch.statistics().get(stat)
126126
{
127-
vortex_ensure!(
128-
partial.dtype() == &self.partial_dtype,
129-
"Aggregate {} read legacy stat {} with dtype {}, expected {}",
130-
self.aggregate_fn,
131-
stat,
132-
partial.dtype(),
133-
self.partial_dtype,
134-
);
127+
let partial = if partial.dtype() == &self.partial_dtype {
128+
partial
129+
} else {
130+
vortex_ensure!(
131+
partial.dtype().eq_ignore_nullability(&self.partial_dtype),
132+
"Aggregate {} read legacy stat {} with dtype {}, expected {}",
133+
self.aggregate_fn,
134+
stat,
135+
partial.dtype(),
136+
self.partial_dtype,
137+
);
138+
partial.cast(&self.partial_dtype)?
139+
};
135140
self.vtable.combine_partials(&mut self.partial, partial)?;
136141
return Ok(());
137142
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexResult;
5+
6+
use crate::ArrayRef;
7+
use crate::Columnar;
8+
use crate::ExecutionCtx;
9+
use crate::IntoArray;
10+
use crate::aggregate_fn::AggregateFnId;
11+
use crate::aggregate_fn::AggregateFnVTable;
12+
use crate::aggregate_fn::EmptyOptions;
13+
use crate::aggregate_fn::fns::nan_count::nan_count;
14+
use crate::dtype::DType;
15+
use crate::dtype::Nullability;
16+
use crate::scalar::Scalar;
17+
18+
/// Compute whether every value in an array is NaN.
19+
///
20+
/// This is a pruning aggregate, not just a convenience wrapper around
21+
/// [`NanCount`][crate::aggregate_fn::fns::nan_count::NanCount]. Pruning aggregates must prove a
22+
/// row-wise fact for every value in the scope, so their partials remain valid when a stats column is
23+
/// sliced or concatenated alongside the data. [`NanCount`][crate::aggregate_fn::fns::nan_count::NanCount]
24+
/// carries cross-row count information instead, so it is useful as a legacy storage format but not
25+
/// as the pruning expression itself.
26+
#[derive(Clone, Debug)]
27+
pub struct AllNan;
28+
29+
impl AggregateFnVTable for AllNan {
30+
type Options = EmptyOptions;
31+
type Partial = bool;
32+
33+
fn id(&self) -> AggregateFnId {
34+
AggregateFnId::new("vortex.all_nan")
35+
}
36+
37+
fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
38+
Ok(None)
39+
}
40+
41+
fn return_dtype(&self, _options: &Self::Options, _input_dtype: &DType) -> Option<DType> {
42+
Some(DType::Bool(Nullability::NonNullable))
43+
}
44+
45+
fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
46+
self.return_dtype(options, input_dtype)
47+
}
48+
49+
fn empty_partial(
50+
&self,
51+
_options: &Self::Options,
52+
input_dtype: &DType,
53+
) -> VortexResult<Self::Partial> {
54+
Ok(has_nans(input_dtype))
55+
}
56+
57+
fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
58+
*partial &= bool::try_from(&other)?;
59+
Ok(())
60+
}
61+
62+
fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
63+
Ok(Scalar::bool(*partial, Nullability::NonNullable))
64+
}
65+
66+
fn reset(&self, partial: &mut Self::Partial) {
67+
*partial = true;
68+
}
69+
70+
fn is_saturated(&self, partial: &Self::Partial) -> bool {
71+
!*partial
72+
}
73+
74+
fn try_accumulate(
75+
&self,
76+
state: &mut Self::Partial,
77+
batch: &ArrayRef,
78+
ctx: &mut ExecutionCtx,
79+
) -> VortexResult<bool> {
80+
if !has_nans(batch.dtype()) {
81+
*state = false;
82+
return Ok(true);
83+
}
84+
85+
*state &= nan_count(batch, ctx)? == batch.len();
86+
Ok(true)
87+
}
88+
89+
fn accumulate(
90+
&self,
91+
partial: &mut Self::Partial,
92+
batch: &Columnar,
93+
ctx: &mut ExecutionCtx,
94+
) -> VortexResult<()> {
95+
let array = match batch {
96+
Columnar::Constant(c) => c.clone().into_array(),
97+
Columnar::Canonical(c) => c.clone().into_array(),
98+
};
99+
if !has_nans(array.dtype()) {
100+
*partial = false;
101+
return Ok(());
102+
}
103+
104+
*partial &= nan_count(&array, ctx)? == array.len();
105+
Ok(())
106+
}
107+
108+
fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
109+
Ok(partials)
110+
}
111+
112+
fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
113+
self.to_scalar(partial)
114+
}
115+
}
116+
117+
fn has_nans(dtype: &DType) -> bool {
118+
matches!(dtype, DType::Primitive(ptype, _) if ptype.is_float())
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use vortex_error::VortexResult;
124+
125+
use crate::IntoArray;
126+
use crate::LEGACY_SESSION;
127+
use crate::VortexSessionExecute;
128+
use crate::aggregate_fn::Accumulator;
129+
use crate::aggregate_fn::DynAccumulator;
130+
use crate::aggregate_fn::EmptyOptions;
131+
use crate::aggregate_fn::fns::all_nan::AllNan;
132+
use crate::arrays::PrimitiveArray;
133+
use crate::dtype::DType;
134+
use crate::dtype::Nullability;
135+
use crate::dtype::PType;
136+
137+
#[test]
138+
fn all_nan_aggregate_fn() -> VortexResult<()> {
139+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
140+
let dtype = DType::Primitive(PType::F32, Nullability::Nullable);
141+
let mut acc = Accumulator::try_new(AllNan, EmptyOptions, dtype)?;
142+
143+
let batch = PrimitiveArray::from_option_iter([Some(f32::NAN), Some(f32::NAN)]).into_array();
144+
acc.accumulate(&batch, &mut ctx)?;
145+
146+
assert!(bool::try_from(&acc.finish()?)?);
147+
Ok(())
148+
}
149+
150+
#[test]
151+
fn all_nan_false_with_non_nan() -> VortexResult<()> {
152+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
153+
let dtype = DType::Primitive(PType::F32, Nullability::Nullable);
154+
let mut acc = Accumulator::try_new(AllNan, EmptyOptions, dtype)?;
155+
156+
let batch = PrimitiveArray::from_option_iter([Some(f32::NAN), Some(1.0f32)]).into_array();
157+
acc.accumulate(&batch, &mut ctx)?;
158+
159+
assert!(!bool::try_from(&acc.finish()?)?);
160+
Ok(())
161+
}
162+
163+
#[test]
164+
fn all_nan_false_for_non_float_values() -> VortexResult<()> {
165+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
166+
let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
167+
let mut acc = Accumulator::try_new(AllNan, EmptyOptions, dtype)?;
168+
169+
let batch = PrimitiveArray::from_option_iter([Some(1i32), None]).into_array();
170+
acc.accumulate(&batch, &mut ctx)?;
171+
172+
assert!(!bool::try_from(&acc.finish()?)?);
173+
Ok(())
174+
}
175+
176+
#[test]
177+
fn all_nan_false_for_empty_non_float_values() -> VortexResult<()> {
178+
let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
179+
let mut acc = Accumulator::try_new(AllNan, EmptyOptions, dtype)?;
180+
181+
assert!(!bool::try_from(&acc.finish()?)?);
182+
Ok(())
183+
}
184+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexResult;
5+
6+
use crate::ArrayRef;
7+
use crate::Columnar;
8+
use crate::ExecutionCtx;
9+
use crate::IntoArray;
10+
use crate::aggregate_fn::AggregateFnId;
11+
use crate::aggregate_fn::AggregateFnVTable;
12+
use crate::aggregate_fn::EmptyOptions;
13+
use crate::aggregate_fn::fns::nan_count::nan_count;
14+
use crate::dtype::DType;
15+
use crate::dtype::Nullability;
16+
use crate::scalar::Scalar;
17+
18+
/// Compute whether every value in an array is not NaN.
19+
///
20+
/// This is a pruning aggregate, not just a convenience wrapper around
21+
/// [`NanCount`][crate::aggregate_fn::fns::nan_count::NanCount]. Pruning aggregates must prove a
22+
/// row-wise fact for every value in the scope, so their partials remain valid when a stats column is
23+
/// sliced or concatenated alongside the data. [`NanCount`][crate::aggregate_fn::fns::nan_count::NanCount]
24+
/// carries cross-row count information instead, so it is useful as a legacy storage format but not
25+
/// as the pruning expression itself.
26+
#[derive(Clone, Debug)]
27+
pub struct AllNonNan;
28+
29+
impl AggregateFnVTable for AllNonNan {
30+
type Options = EmptyOptions;
31+
type Partial = bool;
32+
33+
fn id(&self) -> AggregateFnId {
34+
AggregateFnId::new("vortex.all_non_nan")
35+
}
36+
37+
fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
38+
Ok(None)
39+
}
40+
41+
fn return_dtype(&self, _options: &Self::Options, _input_dtype: &DType) -> Option<DType> {
42+
Some(DType::Bool(Nullability::NonNullable))
43+
}
44+
45+
fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
46+
self.return_dtype(options, input_dtype)
47+
}
48+
49+
fn empty_partial(
50+
&self,
51+
_options: &Self::Options,
52+
_input_dtype: &DType,
53+
) -> VortexResult<Self::Partial> {
54+
Ok(true)
55+
}
56+
57+
fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
58+
*partial &= bool::try_from(&other)?;
59+
Ok(())
60+
}
61+
62+
fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
63+
Ok(Scalar::bool(*partial, Nullability::NonNullable))
64+
}
65+
66+
fn reset(&self, partial: &mut Self::Partial) {
67+
*partial = true;
68+
}
69+
70+
fn is_saturated(&self, partial: &Self::Partial) -> bool {
71+
!*partial
72+
}
73+
74+
fn try_accumulate(
75+
&self,
76+
state: &mut Self::Partial,
77+
batch: &ArrayRef,
78+
ctx: &mut ExecutionCtx,
79+
) -> VortexResult<bool> {
80+
*state &= nan_count(batch, ctx)? == 0;
81+
Ok(true)
82+
}
83+
84+
fn accumulate(
85+
&self,
86+
partial: &mut Self::Partial,
87+
batch: &Columnar,
88+
ctx: &mut ExecutionCtx,
89+
) -> VortexResult<()> {
90+
let array = match batch {
91+
Columnar::Constant(c) => c.clone().into_array(),
92+
Columnar::Canonical(c) => c.clone().into_array(),
93+
};
94+
*partial &= nan_count(&array, ctx)? == 0;
95+
Ok(())
96+
}
97+
98+
fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
99+
Ok(partials)
100+
}
101+
102+
fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
103+
self.to_scalar(partial)
104+
}
105+
}
106+
107+
#[cfg(test)]
108+
mod tests {
109+
use vortex_error::VortexResult;
110+
111+
use crate::IntoArray;
112+
use crate::LEGACY_SESSION;
113+
use crate::VortexSessionExecute;
114+
use crate::aggregate_fn::Accumulator;
115+
use crate::aggregate_fn::DynAccumulator;
116+
use crate::aggregate_fn::EmptyOptions;
117+
use crate::aggregate_fn::fns::all_non_nan::AllNonNan;
118+
use crate::arrays::PrimitiveArray;
119+
use crate::dtype::DType;
120+
use crate::dtype::Nullability;
121+
use crate::dtype::PType;
122+
123+
#[test]
124+
fn all_non_nan_aggregate_fn() -> VortexResult<()> {
125+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
126+
let dtype = DType::Primitive(PType::F32, Nullability::Nullable);
127+
let mut acc = Accumulator::try_new(AllNonNan, EmptyOptions, dtype)?;
128+
129+
let batch = PrimitiveArray::from_option_iter([Some(1.0f32), None, Some(3.0)]).into_array();
130+
acc.accumulate(&batch, &mut ctx)?;
131+
132+
assert!(bool::try_from(&acc.finish()?)?);
133+
Ok(())
134+
}
135+
136+
#[test]
137+
fn all_non_nan_false_with_nan() -> VortexResult<()> {
138+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
139+
let dtype = DType::Primitive(PType::F32, Nullability::Nullable);
140+
let mut acc = Accumulator::try_new(AllNonNan, EmptyOptions, dtype)?;
141+
142+
let batch = PrimitiveArray::from_option_iter([Some(1.0f32), Some(f32::NAN)]).into_array();
143+
acc.accumulate(&batch, &mut ctx)?;
144+
145+
assert!(!bool::try_from(&acc.finish()?)?);
146+
Ok(())
147+
}
148+
149+
#[test]
150+
fn all_non_nan_true_for_non_float() -> VortexResult<()> {
151+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
152+
let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
153+
let mut acc = Accumulator::try_new(AllNonNan, EmptyOptions, dtype)?;
154+
155+
let batch = PrimitiveArray::from_option_iter([Some(1i32), None]).into_array();
156+
acc.accumulate(&batch, &mut ctx)?;
157+
158+
assert!(bool::try_from(&acc.finish()?)?);
159+
Ok(())
160+
}
161+
}

0 commit comments

Comments
 (0)