Skip to content

Commit 4dec695

Browse files
committed
Tighten pruning aggregate semantics
Signed-off-by: Nicholas Gates <nick@nickgates.com>
1 parent d4bdc41 commit 4dec695

12 files changed

Lines changed: 271 additions & 50 deletions

File tree

vortex-array/public-api.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ pub fn vortex_array::aggregate_fn::fns::bounded_max::BoundedMax::try_accumulate(
468468

469469
pub struct vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions
470470

471-
pub vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions::max_bytes: usize
471+
pub vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions::max_bytes: core::num::nonzero::NonZeroUsize
472472

473473
impl core::clone::Clone for vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions
474474

@@ -546,7 +546,7 @@ pub fn vortex_array::aggregate_fn::fns::bounded_min::BoundedMin::try_accumulate(
546546

547547
pub struct vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions
548548

549-
pub vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions::max_bytes: usize
549+
pub vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions::max_bytes: core::num::nonzero::NonZeroUsize
550550

551551
impl core::clone::Clone for vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions
552552

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::scalar::Scalar;
1717

1818
/// Compute whether every value in an array is NaN.
1919
///
20+
/// Like other `all` aggregates, this is vacuously true for empty input.
21+
///
2022
/// This is a pruning aggregate, not just a convenience wrapper around
2123
/// [`NanCount`][crate::aggregate_fn::fns::nan_count::NanCount]. Pruning aggregates must prove a
2224
/// row-wise fact for every value in the scope, so their partials remain valid when a stats column is
@@ -39,7 +41,8 @@ impl AggregateFnVTable for AllNan {
3941
}
4042

4143
fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
42-
has_nans(input_dtype).then_some(DType::Bool(Nullability::Nullable))
44+
matches!(input_dtype, DType::Primitive(ptype, _) if ptype.is_float())
45+
.then_some(DType::Bool(Nullability::Nullable))
4346
}
4447

4548
fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
@@ -77,7 +80,7 @@ impl AggregateFnVTable for AllNan {
7780
batch: &ArrayRef,
7881
ctx: &mut ExecutionCtx,
7982
) -> VortexResult<bool> {
80-
if !has_nans(batch.dtype()) {
83+
if !matches!(batch.dtype(), DType::Primitive(ptype, _) if ptype.is_float()) {
8184
*state = false;
8285
return Ok(true);
8386
}
@@ -96,7 +99,7 @@ impl AggregateFnVTable for AllNan {
9699
Columnar::Constant(c) => c.clone().into_array(),
97100
Columnar::Canonical(c) => c.clone().into_array(),
98101
};
99-
if !has_nans(array.dtype()) {
102+
if !matches!(array.dtype(), DType::Primitive(ptype, _) if ptype.is_float()) {
100103
*partial = false;
101104
return Ok(());
102105
}
@@ -114,10 +117,6 @@ impl AggregateFnVTable for AllNan {
114117
}
115118
}
116119

117-
fn has_nans(dtype: &DType) -> bool {
118-
matches!(dtype, DType::Primitive(ptype, _) if ptype.is_float())
119-
}
120-
121120
#[cfg(test)]
122121
mod tests {
123122
use vortex_error::VortexResult;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::validity::Validity;
5151
/// Returns `true` if and only if:
5252
/// - Both arrays have the same dtype and length
5353
/// - At every position, both are null or both are non-null with the same value
54+
/// - The arrays are empty, vacuously
5455
///
5556
/// This is a fused `bool_all(non_distinct(lhs, rhs))` aggregate that allows early
5657
/// termination via accumulator saturation as soon as a mismatch is found.
@@ -102,6 +103,8 @@ static NAMES: LazyLock<FieldNames> = LazyLock::new(|| FieldNames::from(["lhs", "
102103
/// as the first distinct pair is found, the accumulator is saturated and remaining batches
103104
/// are skipped.
104105
///
106+
/// Like other `all` aggregates, this is vacuously true for empty input.
107+
///
105108
/// The input is a `Struct{lhs: T, rhs: T}` and the result is `Bool(NonNullable)`.
106109
#[derive(Clone, Debug)]
107110
pub struct AllNonDistinct;

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::scalar::Scalar;
1717

1818
/// Compute whether every value in an array is not NaN.
1919
///
20+
/// Like other `all` aggregates, this is vacuously true for empty input.
21+
///
2022
/// This is a pruning aggregate, not just a convenience wrapper around
2123
/// [`NanCount`][crate::aggregate_fn::fns::nan_count::NanCount]. Pruning aggregates must prove a
2224
/// row-wise fact for every value in the scope, so their partials remain valid when a stats column is
@@ -39,7 +41,8 @@ impl AggregateFnVTable for AllNonNan {
3941
}
4042

4143
fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
42-
has_nans(input_dtype).then_some(DType::Bool(Nullability::Nullable))
44+
matches!(input_dtype, DType::Primitive(ptype, _) if ptype.is_float())
45+
.then_some(DType::Bool(Nullability::Nullable))
4346
}
4447

4548
fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
@@ -104,10 +107,6 @@ impl AggregateFnVTable for AllNonNan {
104107
}
105108
}
106109

107-
fn has_nans(dtype: &DType) -> bool {
108-
matches!(dtype, DType::Primitive(ptype, _) if ptype.is_float())
109-
}
110-
111110
#[cfg(test)]
112111
mod tests {
113112
use vortex_error::VortexResult;

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use crate::dtype::Nullability;
1515
use crate::scalar::Scalar;
1616

1717
/// Compute whether every value in an array is non-null.
18+
///
19+
/// Like other `all` aggregates, this is vacuously true for empty input.
1820
#[derive(Clone, Debug)]
1921
pub struct AllNonNull;
2022

@@ -136,4 +138,17 @@ mod tests {
136138
assert!(!bool::try_from(&acc.finish()?)?);
137139
Ok(())
138140
}
141+
142+
#[test]
143+
fn all_non_null_true_for_empty_input() -> VortexResult<()> {
144+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
145+
let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
146+
let mut acc = Accumulator::try_new(AllNonNull, EmptyOptions, dtype)?;
147+
148+
let batch = PrimitiveArray::empty::<i32>(Nullability::Nullable).into_array();
149+
acc.accumulate(&batch, &mut ctx)?;
150+
151+
assert!(bool::try_from(&acc.finish()?)?);
152+
Ok(())
153+
}
139154
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use crate::dtype::Nullability;
1515
use crate::scalar::Scalar;
1616

1717
/// Compute whether every value in an array is null.
18+
///
19+
/// Like other `all` aggregates, this is vacuously true for empty input.
1820
#[derive(Clone, Debug)]
1921
pub struct AllNull;
2022

@@ -139,4 +141,17 @@ mod tests {
139141
assert!(!bool::try_from(&acc.finish()?)?);
140142
Ok(())
141143
}
144+
145+
#[test]
146+
fn all_null_true_for_empty_input() -> VortexResult<()> {
147+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
148+
let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
149+
let mut acc = Accumulator::try_new(AllNull, EmptyOptions, dtype)?;
150+
151+
let batch = PrimitiveArray::empty::<i32>(Nullability::Nullable).into_array();
152+
acc.accumulate(&batch, &mut ctx)?;
153+
154+
assert!(bool::try_from(&acc.finish()?)?);
155+
Ok(())
156+
}
142157
}

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

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use std::fmt::Display;
55
use std::fmt::Formatter;
6+
use std::num::NonZeroUsize;
67

78
use vortex_buffer::BufferString;
89
use vortex_buffer::ByteBuffer;
@@ -30,12 +31,12 @@ use crate::scalar::upper_bound;
3031
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3132
pub struct BoundedMaxOptions {
3233
/// Maximum byte length for UTF8/Binary bounds.
33-
pub max_bytes: usize,
34+
pub max_bytes: NonZeroUsize,
3435
}
3536

3637
impl Display for BoundedMaxOptions {
3738
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38-
write!(f, "{}", self.max_bytes)
39+
write!(f, "{}", self.max_bytes.get())
3940
}
4041
}
4142

@@ -53,7 +54,7 @@ enum BoundedMaxState {
5354
pub struct BoundedMaxPartial {
5455
state: BoundedMaxState,
5556
element_dtype: DType,
56-
max_bytes: usize,
57+
max_bytes: NonZeroUsize,
5758
}
5859

5960
impl BoundedMaxPartial {
@@ -86,7 +87,7 @@ impl AggregateFnVTable for BoundedMax {
8687
}
8788

8889
fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
89-
let max_bytes = u64::try_from(options.max_bytes)?;
90+
let max_bytes = u64::try_from(options.max_bytes.get())?;
9091
Ok(Some(max_bytes.to_le_bytes().to_vec()))
9192
}
9293

@@ -105,7 +106,9 @@ impl AggregateFnVTable for BoundedMax {
105106
bytes.copy_from_slice(metadata);
106107
let max_bytes = usize::try_from(u64::from_le_bytes(bytes))?;
107108
vortex_ensure!(max_bytes > 0, "BoundedMax requires max_bytes > 0");
108-
Ok(BoundedMaxOptions { max_bytes })
109+
Ok(BoundedMaxOptions {
110+
max_bytes: NonZeroUsize::new(max_bytes).vortex_expect("checked non-zero max_bytes"),
111+
})
109112
}
110113

111114
fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
@@ -162,7 +165,7 @@ impl AggregateFnVTable for BoundedMax {
162165
let Some(result) = min_max(&array, ctx)? else {
163166
return Ok(());
164167
};
165-
match truncate_max(result.max, partial.max_bytes)? {
168+
match truncate_max(result.max, partial.max_bytes.get())? {
166169
Some(bound) => partial.merge(bound),
167170
None => partial.unknown(),
168171
}
@@ -178,11 +181,7 @@ impl AggregateFnVTable for BoundedMax {
178181
}
179182
}
180183

181-
fn supported_dtype<'a>(options: &BoundedMaxOptions, input_dtype: &'a DType) -> Option<&'a DType> {
182-
if options.max_bytes == 0 {
183-
return None;
184-
}
185-
184+
fn supported_dtype<'a>(_options: &BoundedMaxOptions, input_dtype: &'a DType) -> Option<&'a DType> {
186185
MinMax
187186
.return_dtype(&EmptyOptions, input_dtype)
188187
.map(|_| input_dtype)
@@ -209,7 +208,10 @@ fn truncate_max(value: Scalar, max_bytes: usize) -> VortexResult<Option<Scalar>>
209208

210209
#[cfg(test)]
211210
mod tests {
211+
use std::num::NonZeroUsize;
212+
212213
use vortex_buffer::buffer;
214+
use vortex_error::VortexExpect;
213215
use vortex_error::VortexResult;
214216
use vortex_session::VortexSession;
215217

@@ -227,13 +229,19 @@ mod tests {
227229
use crate::scalar::Scalar;
228230
use crate::validity::Validity;
229231

232+
fn max_bytes(value: usize) -> NonZeroUsize {
233+
NonZeroUsize::new(value).vortex_expect("non-zero max_bytes")
234+
}
235+
230236
#[test]
231237
fn bounded_max_truncates_utf8_to_upper_bound() -> VortexResult<()> {
232238
let mut ctx = LEGACY_SESSION.create_execution_ctx();
233239
let array = VarBinViewArray::from_iter_str(["aardvark", "char🪩"]).into_array();
234240
let mut acc = Accumulator::try_new(
235241
BoundedMax,
236-
BoundedMaxOptions { max_bytes: 5 },
242+
BoundedMaxOptions {
243+
max_bytes: max_bytes(5),
244+
},
237245
array.dtype().clone(),
238246
)?;
239247

@@ -249,7 +257,9 @@ mod tests {
249257
let array = VarBinViewArray::from_iter_bin([&[255u8, 255, 255][..]]).into_array();
250258
let mut acc = Accumulator::try_new(
251259
BoundedMax,
252-
BoundedMaxOptions { max_bytes: 2 },
260+
BoundedMaxOptions {
261+
max_bytes: max_bytes(2),
262+
},
253263
array.dtype().clone(),
254264
)?;
255265

@@ -259,13 +269,58 @@ mod tests {
259269
Ok(())
260270
}
261271

272+
#[test]
273+
fn bounded_max_empty_does_not_poison_later_values() -> VortexResult<()> {
274+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
275+
let empty = VarBinViewArray::from_iter_bin(Vec::<&[u8]>::new()).into_array();
276+
let values = VarBinViewArray::from_iter_bin([&[1u8][..]]).into_array();
277+
let mut acc = Accumulator::try_new(
278+
BoundedMax,
279+
BoundedMaxOptions {
280+
max_bytes: max_bytes(2),
281+
},
282+
empty.dtype().clone(),
283+
)?;
284+
285+
acc.accumulate(&empty, &mut ctx)?;
286+
acc.accumulate(&values, &mut ctx)?;
287+
288+
assert_eq!(
289+
acc.finish()?,
290+
Scalar::binary(buffer![1u8], Nullability::Nullable)
291+
);
292+
Ok(())
293+
}
294+
295+
#[test]
296+
fn bounded_max_unknown_poisons_later_values() -> VortexResult<()> {
297+
let mut ctx = LEGACY_SESSION.create_execution_ctx();
298+
let unknown = VarBinViewArray::from_iter_bin([&[255u8, 255, 255][..]]).into_array();
299+
let values = VarBinViewArray::from_iter_bin([&[1u8][..]]).into_array();
300+
let mut acc = Accumulator::try_new(
301+
BoundedMax,
302+
BoundedMaxOptions {
303+
max_bytes: max_bytes(2),
304+
},
305+
unknown.dtype().clone(),
306+
)?;
307+
308+
acc.accumulate(&unknown, &mut ctx)?;
309+
acc.accumulate(&values, &mut ctx)?;
310+
311+
assert_eq!(acc.finish()?, Scalar::null(unknown.dtype().as_nullable()));
312+
Ok(())
313+
}
314+
262315
#[test]
263316
fn bounded_max_keeps_fixed_width_values_exact() -> VortexResult<()> {
264317
let mut ctx = LEGACY_SESSION.create_execution_ctx();
265318
let array = PrimitiveArray::new(buffer![10i32, 20, 5], Validity::NonNullable).into_array();
266319
let mut acc = Accumulator::try_new(
267320
BoundedMax,
268-
BoundedMaxOptions { max_bytes: 9 },
321+
BoundedMaxOptions {
322+
max_bytes: max_bytes(9),
323+
},
269324
array.dtype().clone(),
270325
)?;
271326

@@ -280,7 +335,9 @@ mod tests {
280335

281336
#[test]
282337
fn bounded_max_options_round_trip() -> VortexResult<()> {
283-
let options = BoundedMaxOptions { max_bytes: 64 };
338+
let options = BoundedMaxOptions {
339+
max_bytes: max_bytes(64),
340+
};
284341
let metadata = BoundedMax
285342
.serialize(&options)?
286343
.expect("serializable options");

0 commit comments

Comments
 (0)