vortex_array/aggregate_fn/fns/bounded_min/
mod.rs1use std::fmt::Display;
5use std::fmt::Formatter;
6use std::num::NonZeroUsize;
7
8use vortex_buffer::BufferString;
9use vortex_buffer::ByteBuffer;
10use vortex_error::VortexExpect;
11use vortex_error::VortexResult;
12use vortex_error::vortex_ensure;
13use vortex_error::vortex_ensure_eq;
14use vortex_session::VortexSession;
15use vortex_session::registry::CachedId;
16
17use crate::ArrayRef;
18use crate::Columnar;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::aggregate_fn::AggregateArgs;
22use crate::aggregate_fn::AggregateFnId;
23use crate::aggregate_fn::AggregateFnRef;
24use crate::aggregate_fn::AggregateFnSatisfaction;
25use crate::aggregate_fn::AggregateFnVTable;
26use crate::aggregate_fn::NumericalAggregateOpts;
27use crate::aggregate_fn::fns::min::Min;
28use crate::aggregate_fn::fns::min_max::MinMax;
29use crate::aggregate_fn::fns::min_max::min_max;
30use crate::dtype::DType;
31use crate::partial_ord::partial_min;
32use crate::scalar::Scalar;
33use crate::scalar::ScalarTruncation;
34use crate::scalar::lower_bound;
35
36#[derive(Clone, Debug, PartialEq, Eq, Hash)]
38pub struct BoundedMinOptions {
39 pub max_bytes: NonZeroUsize,
41}
42
43impl Display for BoundedMinOptions {
44 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}", self.max_bytes.get())
46 }
47}
48
49#[derive(Clone, Debug)]
51pub struct BoundedMin;
52
53enum BoundedMinState {
54 Empty,
55 Value(Scalar),
56}
57
58pub struct BoundedMinPartial {
60 state: BoundedMinState,
61}
62
63impl BoundedMinPartial {
64 fn merge(&mut self, min: Scalar) {
65 if min.is_null() {
66 return;
67 }
68
69 self.state = match std::mem::replace(&mut self.state, BoundedMinState::Empty) {
70 BoundedMinState::Empty => BoundedMinState::Value(min),
71 BoundedMinState::Value(current) => BoundedMinState::Value(
72 partial_min(min, current).vortex_expect("incomparable bounded min scalars"),
73 ),
74 };
75 }
76}
77
78impl AggregateFnVTable for BoundedMin {
79 type Options = BoundedMinOptions;
80 type Partial = BoundedMinPartial;
81
82 fn id(&self) -> AggregateFnId {
83 static ID: CachedId = CachedId::new("vortex.bounded_min");
84 *ID
85 }
86
87 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
88 let max_bytes = u64::try_from(options.max_bytes.get())?;
89 Ok(Some(max_bytes.to_le_bytes().to_vec()))
90 }
91
92 fn deserialize(
93 &self,
94 metadata: &[u8],
95 _session: &VortexSession,
96 ) -> VortexResult<Self::Options> {
97 vortex_ensure_eq!(
98 metadata.len(),
99 size_of::<u64>(),
100 "BoundedMin options have the wrong byte length"
101 );
102 let mut bytes = [0u8; size_of::<u64>()];
103 bytes.copy_from_slice(metadata);
104 let max_bytes = usize::try_from(u64::from_le_bytes(bytes))?;
105 vortex_ensure!(max_bytes > 0, "BoundedMin requires max_bytes > 0");
106 Ok(BoundedMinOptions {
107 max_bytes: NonZeroUsize::new(max_bytes).vortex_expect("checked non-zero max_bytes"),
108 })
109 }
110
111 fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
112 supported_dtype(options, input_dtype).map(DType::as_nullable)
113 }
114
115 fn can_satisfy(
116 &self,
117 options: &Self::Options,
118 requested: &AggregateFnRef,
119 ) -> AggregateFnSatisfaction {
120 if let Some(other) = requested.as_opt::<Self>() {
121 return if other == options {
122 AggregateFnSatisfaction::Exact
123 } else if options.max_bytes >= other.max_bytes {
124 AggregateFnSatisfaction::Approximate
125 } else {
126 AggregateFnSatisfaction::No
127 };
128 }
129
130 if requested
132 .as_opt::<Min>()
133 .is_some_and(|options| options.skip_nans)
134 {
135 AggregateFnSatisfaction::Approximate
136 } else {
137 AggregateFnSatisfaction::No
138 }
139 }
140
141 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
142 self.return_dtype(options, input_dtype)
143 }
144
145 fn empty_partial(
146 &self,
147 _args: AggregateArgs<'_, Self::Options>,
148 ) -> VortexResult<Self::Partial> {
149 Ok(BoundedMinPartial {
150 state: BoundedMinState::Empty,
151 })
152 }
153
154 fn partial_from_scalar(
155 &self,
156 _args: AggregateArgs<'_, Self::Options>,
157 scalar: &Scalar,
158 ) -> VortexResult<Self::Partial> {
159 let state = if scalar.is_null() {
161 BoundedMinState::Empty
162 } else {
163 BoundedMinState::Value(scalar.clone())
164 };
165 Ok(BoundedMinPartial { state })
166 }
167
168 fn merge_partials(
169 &self,
170 _args: AggregateArgs<'_, Self::Options>,
171 mut first: Self::Partial,
172 second: Self::Partial,
173 ) -> VortexResult<Self::Partial> {
174 if let BoundedMinState::Value(min) = second.state {
175 first.merge(min);
176 }
177 Ok(first)
178 }
179
180 fn to_scalar(
181 &self,
182 args: AggregateArgs<'_, Self::Options>,
183 partial: &Self::Partial,
184 ) -> VortexResult<Scalar> {
185 let dtype = args.dtype.as_nullable();
186 match &partial.state {
187 BoundedMinState::Empty => Ok(Scalar::null(dtype)),
188 BoundedMinState::Value(min) => min.cast(&dtype),
189 }
190 }
191
192 fn is_saturated(
193 &self,
194 _args: AggregateArgs<'_, Self::Options>,
195 _partial: &Self::Partial,
196 ) -> bool {
197 false
198 }
199
200 fn accumulate(
201 &self,
202 args: AggregateArgs<'_, Self::Options>,
203 partial: &mut Self::Partial,
204 batch: &Columnar,
205 ctx: &mut ExecutionCtx,
206 ) -> VortexResult<()> {
207 let array = match batch {
210 Columnar::Canonical(canonical) => canonical.clone().into_array(),
211 Columnar::Constant(constant) => constant.clone().into_array(),
212 };
213 let Some(result) = min_max(&array, ctx, NumericalAggregateOpts::default())? else {
214 return Ok(());
215 };
216 if let Some(bound) = truncate_min(result.min, args.options.max_bytes.get())? {
217 partial.merge(bound);
218 }
219 Ok(())
220 }
221
222 fn finalize(
223 &self,
224 _args: AggregateArgs<'_, Self::Options>,
225 partials: ArrayRef,
226 ) -> VortexResult<ArrayRef> {
227 Ok(partials)
228 }
229
230 fn finalize_scalar(
231 &self,
232 args: AggregateArgs<'_, Self::Options>,
233 partial: &Self::Partial,
234 ) -> VortexResult<Scalar> {
235 self.to_scalar(args, partial)
236 }
237}
238
239fn supported_dtype<'a>(_options: &BoundedMinOptions, input_dtype: &'a DType) -> Option<&'a DType> {
240 MinMax
241 .return_dtype(&NumericalAggregateOpts::default(), input_dtype)
242 .map(|_| input_dtype)
243}
244
245fn truncate_min(value: Scalar, max_bytes: usize) -> VortexResult<Option<Scalar>> {
246 let nullability = value.dtype().nullability();
247 match value.dtype() {
248 DType::Utf8(_) => {
249 Ok(
250 lower_bound(BufferString::from_scalar(value)?, max_bytes, nullability)
251 .map(|(bound, _)| bound),
252 )
253 }
254 DType::Binary(_) => {
255 Ok(
256 lower_bound(ByteBuffer::from_scalar(value)?, max_bytes, nullability)
257 .map(|(bound, _)| bound),
258 )
259 }
260 _ => Ok(Some(value)),
261 }
262}
263#[cfg(test)]
264mod tests {
265 use std::num::NonZeroUsize;
266
267 use vortex_buffer::buffer;
268 use vortex_error::VortexExpect;
269 use vortex_error::VortexResult;
270 use vortex_session::VortexSession;
271
272 use crate::IntoArray;
273 use crate::VortexSessionExecute;
274 use crate::aggregate_fn::Accumulator;
275 use crate::aggregate_fn::AggregateFnSatisfaction;
276 use crate::aggregate_fn::AggregateFnVTable;
277 use crate::aggregate_fn::AggregateFnVTableExt;
278 use crate::aggregate_fn::DynAccumulator;
279 use crate::aggregate_fn::NumericalAggregateOpts;
280 use crate::aggregate_fn::fns::bounded_min::BoundedMin;
281 use crate::aggregate_fn::fns::bounded_min::BoundedMinOptions;
282 use crate::aggregate_fn::fns::max::Max;
283 use crate::aggregate_fn::fns::min::Min;
284 use crate::array_session;
285 use crate::arrays::PrimitiveArray;
286 use crate::arrays::VarBinViewArray;
287 use crate::dtype::Nullability;
288 use crate::scalar::Scalar;
289 use crate::validity::Validity;
290
291 fn max_bytes(value: usize) -> NonZeroUsize {
292 NonZeroUsize::new(value).vortex_expect("non-zero max_bytes")
293 }
294
295 fn fresh_session() -> VortexSession {
296 array_session()
297 }
298
299 #[test]
300 fn bounded_min_truncates_utf8_to_lower_bound() -> VortexResult<()> {
301 let mut ctx = array_session().create_execution_ctx();
302 let array =
303 VarBinViewArray::from_iter_str(["snowman⛄️snowman", "untruncated"]).into_array();
304 let mut acc = Accumulator::try_new(
305 BoundedMin,
306 BoundedMinOptions {
307 max_bytes: max_bytes(9),
308 },
309 array.dtype().clone(),
310 )?;
311
312 acc.accumulate(&array, &mut ctx)?;
313
314 assert_eq!(
315 acc.finish()?,
316 Scalar::utf8("snowman", Nullability::Nullable)
317 );
318 Ok(())
319 }
320
321 #[test]
322 fn bounded_min_keeps_fixed_width_values_exact() -> VortexResult<()> {
323 let mut ctx = array_session().create_execution_ctx();
324 let array = PrimitiveArray::new(buffer![10i32, 20, 5], Validity::NonNullable).into_array();
325 let mut acc = Accumulator::try_new(
326 BoundedMin,
327 BoundedMinOptions {
328 max_bytes: max_bytes(9),
329 },
330 array.dtype().clone(),
331 )?;
332
333 acc.accumulate(&array, &mut ctx)?;
334
335 assert_eq!(
336 acc.finish()?,
337 Scalar::primitive(5i32, Nullability::Nullable)
338 );
339 Ok(())
340 }
341
342 #[test]
343 fn bounded_min_null_partial_does_not_poison_existing_bound() -> VortexResult<()> {
344 let mut ctx = fresh_session().create_execution_ctx();
345 let values = VarBinViewArray::from_iter_bin([&[1u8][..]]).into_array();
346 let mut acc = Accumulator::try_new(
347 BoundedMin,
348 BoundedMinOptions {
349 max_bytes: max_bytes(2),
350 },
351 values.dtype().clone(),
352 )?;
353
354 acc.accumulate(&values, &mut ctx)?;
355 let empty = acc.empty_partial()?;
356 acc.fold_partial(empty)?;
357
358 assert_eq!(
359 acc.finish()?,
360 Scalar::binary(buffer![1u8], Nullability::Nullable)
361 );
362 Ok(())
363 }
364
365 #[test]
366 fn bounded_min_satisfies_min_bounds() {
367 let stored = BoundedMin.bind(BoundedMinOptions {
368 max_bytes: max_bytes(5),
369 });
370 let same = BoundedMin.bind(BoundedMinOptions {
371 max_bytes: max_bytes(5),
372 });
373 let looser_bounded = BoundedMin.bind(BoundedMinOptions {
374 max_bytes: max_bytes(4),
375 });
376 let tighter_bounded = BoundedMin.bind(BoundedMinOptions {
377 max_bytes: max_bytes(6),
378 });
379
380 assert_eq!(stored.can_satisfy(&same), AggregateFnSatisfaction::Exact);
381 assert_eq!(
382 stored.can_satisfy(&looser_bounded),
383 AggregateFnSatisfaction::Approximate
384 );
385 assert_eq!(
386 stored.can_satisfy(&tighter_bounded),
387 AggregateFnSatisfaction::No
388 );
389 assert_eq!(
390 stored.can_satisfy(&Min.bind(NumericalAggregateOpts::default())),
391 AggregateFnSatisfaction::Approximate
392 );
393 assert_eq!(
394 stored.can_satisfy(&Min.bind(NumericalAggregateOpts::include_nans())),
395 AggregateFnSatisfaction::No
396 );
397 assert_eq!(
398 Min.bind(NumericalAggregateOpts::include_nans())
399 .can_satisfy(&stored),
400 AggregateFnSatisfaction::No
401 );
402 assert_eq!(
403 Min.bind(NumericalAggregateOpts::default())
404 .can_satisfy(&stored),
405 AggregateFnSatisfaction::Approximate
406 );
407 assert_eq!(
408 stored.can_satisfy(&Max.bind(NumericalAggregateOpts::default())),
409 AggregateFnSatisfaction::No
410 );
411 }
412
413 #[test]
414 fn bounded_min_options_round_trip() -> VortexResult<()> {
415 let options = BoundedMinOptions {
416 max_bytes: max_bytes(64),
417 };
418 let metadata = BoundedMin
419 .serialize(&options)?
420 .expect("serializable options");
421 let roundtrip = BoundedMin.deserialize(&metadata, &VortexSession::empty())?;
422
423 assert_eq!(roundtrip, options);
424 Ok(())
425 }
426}