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