1mod bool;
5mod decimal;
6mod extension;
7mod primitive;
8mod varbin;
9
10use std::sync::LazyLock;
11
12use vortex_error::VortexExpect;
13use vortex_error::VortexResult;
14use vortex_error::vortex_bail;
15use vortex_error::vortex_panic;
16use vortex_session::registry::CachedId;
17
18use self::bool::accumulate_bool;
19use self::decimal::accumulate_decimal;
20use self::extension::accumulate_extension;
21use self::primitive::accumulate_primitive;
22use self::varbin::accumulate_varbinview;
23use crate::ArrayRef;
24use crate::Canonical;
25use crate::Columnar;
26use crate::ExecutionCtx;
27use crate::aggregate_fn::Accumulator;
28use crate::aggregate_fn::AggregateFnId;
29use crate::aggregate_fn::AggregateFnVTable;
30use crate::aggregate_fn::DynAccumulator;
31use crate::aggregate_fn::NumericalAggregateOpts;
32use crate::dtype::DType;
33use crate::dtype::FieldNames;
34use crate::dtype::Nullability;
35use crate::dtype::PType;
36use crate::dtype::StructFields;
37use crate::dtype::half::f16;
38use crate::expr::stats::Precision;
39use crate::expr::stats::Stat;
40use crate::expr::stats::StatsProvider;
41use crate::expr::stats::StatsProviderExt;
42use crate::partial_ord::partial_max;
43use crate::partial_ord::partial_min;
44use crate::scalar::Scalar;
45
46static NAMES: LazyLock<FieldNames> = LazyLock::new(|| FieldNames::from(["min", "max"]));
47
48pub fn min_max(
58 array: &ArrayRef,
59 ctx: &mut ExecutionCtx,
60 options: NumericalAggregateOpts,
61) -> VortexResult<Option<MinMaxResult>> {
62 if !options.skip_nans && array.dtype().is_float() {
63 match array.statistics().get_as::<u64>(Stat::NaNCount) {
64 Precision::Exact(0) => {}
66 Precision::Exact(_) => return Ok(Some(nan_minmax_result(array.dtype()))),
68 _ => {
69 if array.is_empty() || array.valid_count(ctx)? == 0 {
70 return Ok(None);
71 }
72 let mut acc = Accumulator::try_new(MinMax, options, array.dtype().clone())?;
75 acc.accumulate(array, ctx)?;
76 return MinMaxResult::from_scalar(acc.finish()?);
77 }
78 }
79 }
80
81 let cached_min = array.statistics().get(Stat::Min).as_exact();
86 let cached_max = array.statistics().get(Stat::Max).as_exact();
87 if let Some((min, max)) = cached_min.zip(cached_max) {
88 let non_nullable_dtype = array.dtype().as_nonnullable();
89 return Ok(Some(MinMaxResult {
90 min: min.cast(&non_nullable_dtype)?,
91 max: max.cast(&non_nullable_dtype)?,
92 }));
93 }
94
95 if array.is_empty() || array.valid_count(ctx)? == 0 {
97 return Ok(None);
98 }
99
100 if !minmax_compute_supported_dtype(array.dtype()) {
102 return Ok(None);
103 }
104
105 let mut acc = Accumulator::try_new(
107 MinMax,
108 NumericalAggregateOpts::default(),
109 array.dtype().clone(),
110 )?;
111 acc.accumulate(array, ctx)?;
112 let result_scalar = acc.finish()?;
113 let result = MinMaxResult::from_scalar(result_scalar)?;
114
115 if let Some(r) = &result {
117 if let Some(min_value) = r.min.value() {
118 array
119 .statistics()
120 .set(Stat::Min, Precision::Exact(min_value.clone()));
121 }
122 if let Some(max_value) = r.max.value() {
123 array
124 .statistics()
125 .set(Stat::Max, Precision::Exact(max_value.clone()));
126 }
127 }
128
129 Ok(result)
130}
131
132fn nan_minmax_result(dtype: &DType) -> MinMaxResult {
134 let nan = nan_scalar(dtype);
135 MinMaxResult {
136 min: nan.clone(),
137 max: nan,
138 }
139}
140
141pub(crate) fn nan_scalar(dtype: &DType) -> Scalar {
143 match dtype.as_ptype() {
144 PType::F16 => Scalar::primitive(f16::NAN, Nullability::NonNullable),
145 PType::F32 => Scalar::primitive(f32::NAN, Nullability::NonNullable),
146 PType::F64 => Scalar::primitive(f64::NAN, Nullability::NonNullable),
147 _ => vortex_panic!("NaN scalar requested for non-float dtype {dtype}"),
148 }
149}
150
151pub(crate) fn scalar_is_nan(scalar: &Scalar) -> bool {
153 if !scalar.dtype().is_float() {
154 return false;
155 }
156
157 scalar.as_primitive_opt().is_some_and(|p| p.is_nan())
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct MinMaxResult {
163 pub min: Scalar,
164 pub max: Scalar,
165}
166
167impl MinMaxResult {
168 pub fn from_scalar(scalar: Scalar) -> VortexResult<Option<Self>> {
170 if scalar.is_null() {
171 Ok(None)
172 } else {
173 let min = scalar
174 .as_struct()
175 .field_by_idx(0)
176 .vortex_expect("missing min field");
177 let max = scalar
178 .as_struct()
179 .field_by_idx(1)
180 .vortex_expect("missing max field");
181 Ok(Some(MinMaxResult { min, max }))
182 }
183 }
184}
185
186#[derive(Clone, Debug)]
194pub struct MinMax;
195
196pub struct MinMaxPartial {
198 min: Option<Scalar>,
199 max: Option<Scalar>,
200 element_dtype: DType,
201 skip_nans: bool,
202}
203
204impl MinMaxPartial {
205 fn merge(&mut self, local: Option<MinMaxResult>) {
207 let Some(MinMaxResult { min, max }) = local else {
208 return;
209 };
210
211 if scalar_is_nan(&min) || scalar_is_nan(&max) || self.is_poisoned() {
215 if !self.skip_nans {
216 self.poison();
217 }
218 return;
219 }
220
221 self.min = Some(match self.min.take() {
222 Some(current) => partial_min(min, current).vortex_expect("incomparable min scalars"),
223 None => min,
224 });
225
226 self.max = Some(match self.max.take() {
227 Some(current) => partial_max(max, current).vortex_expect("incomparable max scalars"),
228 None => max,
229 });
230 }
231
232 fn poison(&mut self) {
234 let nan = nan_scalar(&self.element_dtype);
235 self.min = Some(nan.clone());
236 self.max = Some(nan);
237 }
238
239 fn is_poisoned(&self) -> bool {
241 self.element_dtype.is_float() && self.min.as_ref().is_some_and(scalar_is_nan)
242 }
243}
244
245pub fn make_minmax_dtype(element_dtype: &DType) -> DType {
247 DType::Struct(
248 StructFields::new(
249 NAMES.clone(),
250 vec![
251 element_dtype.as_nonnullable(),
252 element_dtype.as_nonnullable(),
253 ],
254 ),
255 Nullability::Nullable,
256 )
257}
258
259fn minmax_supported_dtype(input_dtype: &DType) -> bool {
260 match input_dtype {
261 DType::Bool(_)
262 | DType::Primitive(..)
263 | DType::Decimal(..)
264 | DType::Utf8(..)
265 | DType::Binary(..)
266 | DType::Extension(..) => true,
267 DType::List(element_dtype, _) => minmax_supported_dtype(element_dtype),
268 DType::FixedSizeList(element_dtype, ..) => minmax_supported_dtype(element_dtype),
269 _ => false,
270 }
271}
272
273fn minmax_compute_supported_dtype(input_dtype: &DType) -> bool {
279 matches!(
280 input_dtype,
281 DType::Bool(_)
282 | DType::Primitive(..)
283 | DType::Decimal(..)
284 | DType::Utf8(..)
285 | DType::Binary(..)
286 | DType::Extension(..)
287 )
288}
289
290impl AggregateFnVTable for MinMax {
291 type Options = NumericalAggregateOpts;
292 type Partial = MinMaxPartial;
293
294 fn id(&self) -> AggregateFnId {
295 static ID: CachedId = CachedId::new("vortex.min_max");
296 *ID
297 }
298
299 fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
300 Ok(None)
301 }
302
303 fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
304 minmax_supported_dtype(input_dtype).then(|| make_minmax_dtype(input_dtype))
305 }
306
307 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
308 self.return_dtype(options, input_dtype)
309 }
310
311 fn empty_partial(
312 &self,
313 options: &Self::Options,
314 input_dtype: &DType,
315 ) -> VortexResult<Self::Partial> {
316 Ok(MinMaxPartial {
317 min: None,
318 max: None,
319 element_dtype: input_dtype.clone(),
320 skip_nans: options.skip_nans,
321 })
322 }
323
324 fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
325 let local = MinMaxResult::from_scalar(other)?;
326 partial.merge(local);
327 Ok(())
328 }
329
330 fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
331 let dtype = make_minmax_dtype(&partial.element_dtype);
332 Ok(match (&partial.min, &partial.max) {
333 (Some(min), Some(max)) => Scalar::struct_(dtype, vec![min.clone(), max.clone()]),
334 _ => Scalar::null(dtype),
335 })
336 }
337
338 fn reset(&self, partial: &mut Self::Partial) {
339 partial.min = None;
340 partial.max = None;
341 }
342
343 #[inline]
344 fn is_saturated(&self, partial: &Self::Partial) -> bool {
345 partial.is_poisoned()
347 }
348
349 fn try_accumulate(
350 &self,
351 partial: &mut Self::Partial,
352 batch: &ArrayRef,
353 _ctx: &mut ExecutionCtx,
354 ) -> VortexResult<bool> {
355 if partial.skip_nans || !partial.element_dtype.is_float() {
358 return Ok(false);
359 }
360 match batch.statistics().get_as::<u64>(Stat::NaNCount) {
361 Precision::Exact(0) => {
362 let cached_min = batch.statistics().get(Stat::Min).as_exact();
364 let cached_max = batch.statistics().get(Stat::Max).as_exact();
365 if let Some((min, max)) = cached_min.zip(cached_max) {
366 let non_nullable_dtype = partial.element_dtype.as_nonnullable();
369 partial.merge(Some(MinMaxResult {
370 min: min.cast(&non_nullable_dtype)?,
371 max: max.cast(&non_nullable_dtype)?,
372 }));
373 return Ok(true);
374 }
375 Ok(false)
376 }
377 Precision::Exact(_) => {
378 partial.poison();
380 Ok(true)
381 }
382 _ => Ok(false),
383 }
384 }
385
386 fn accumulate(
387 &self,
388 partial: &mut Self::Partial,
389 batch: &Columnar,
390 ctx: &mut ExecutionCtx,
391 ) -> VortexResult<()> {
392 match batch {
393 Columnar::Constant(c) => {
394 let scalar = c.scalar();
395 if scalar.is_null() {
396 return Ok(());
397 }
398 if scalar_is_nan(scalar) {
400 if !partial.skip_nans {
401 partial.poison();
402 }
403 return Ok(());
404 }
405 let non_nullable_dtype = scalar.dtype().as_nonnullable();
406 let cast = scalar.cast(&non_nullable_dtype)?;
407 partial.merge(Some(MinMaxResult {
408 min: cast.clone(),
409 max: cast,
410 }));
411 Ok(())
412 }
413 Columnar::Canonical(c) => match c {
414 Canonical::Primitive(p) => accumulate_primitive(partial, p, ctx),
415 Canonical::Bool(b) => accumulate_bool(partial, b, ctx),
416 Canonical::VarBinView(v) => accumulate_varbinview(partial, v, ctx),
417 Canonical::Decimal(d) => accumulate_decimal(partial, d, ctx),
418 Canonical::Extension(e) => accumulate_extension(partial, e, ctx),
419 Canonical::Null(_) => Ok(()),
420 Canonical::Union(_) => {
421 todo!("TODO(connor)[Union]: implement min_max for Union arrays")
422 }
423 Canonical::Struct(_)
424 | Canonical::List(_)
425 | Canonical::Map(_)
426 | Canonical::FixedSizeList(_)
427 | Canonical::Variant(_) => {
428 vortex_bail!("Unsupported canonical type for min_max: {}", batch.dtype())
429 }
430 },
431 }
432 }
433
434 fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
435 Ok(partials)
436 }
437
438 fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
439 self.to_scalar(partial)
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use std::sync::Arc;
446 use std::sync::LazyLock;
447
448 use vortex_buffer::BitBuffer;
449 use vortex_buffer::buffer;
450 use vortex_error::VortexExpect;
451 use vortex_error::VortexResult;
452 use vortex_session::VortexSession;
453
454 use crate::IntoArray as _;
455 use crate::VortexSessionExecute;
456 use crate::aggregate_fn::Accumulator;
457 use crate::aggregate_fn::AggregateFnVTable;
458 use crate::aggregate_fn::DynAccumulator;
459 use crate::aggregate_fn::NumericalAggregateOpts;
460 use crate::aggregate_fn::fns::min_max::MinMax;
461 use crate::aggregate_fn::fns::min_max::MinMaxResult;
462 use crate::aggregate_fn::fns::min_max::make_minmax_dtype;
463 use crate::aggregate_fn::fns::min_max::min_max;
464 use crate::arrays::BoolArray;
465 use crate::arrays::ChunkedArray;
466 use crate::arrays::ConstantArray;
467 use crate::arrays::DecimalArray;
468 use crate::arrays::FixedSizeListArray;
469 use crate::arrays::ListArray;
470 use crate::arrays::NullArray;
471 use crate::arrays::PrimitiveArray;
472 use crate::arrays::VarBinArray;
473 use crate::dtype::DType;
474 use crate::dtype::DecimalDType;
475 use crate::dtype::Nullability;
476 use crate::dtype::PType;
477 use crate::expr::stats::Precision;
478 use crate::expr::stats::Stat;
479 use crate::scalar::DecimalValue;
480 use crate::scalar::Scalar;
481 use crate::scalar::ScalarValue;
482 use crate::validity::Validity;
483
484 static SESSION: LazyLock<VortexSession> = LazyLock::new(vortex_array::array_session);
485
486 #[test]
487 fn test_prim_min_max() -> VortexResult<()> {
488 let p = PrimitiveArray::new(buffer![1, 2, 3], Validity::NonNullable).into_array();
489 let mut ctx = SESSION.create_execution_ctx();
490 assert_eq!(
491 min_max(&p, &mut ctx, NumericalAggregateOpts::default())?,
492 Some(MinMaxResult {
493 min: 1.into(),
494 max: 3.into()
495 })
496 );
497 Ok(())
498 }
499
500 #[test]
501 fn test_prim_min_max_multiple_null_runs() -> VortexResult<()> {
502 let p = PrimitiveArray::from_option_iter([
505 Some(5i32),
506 Some(3),
507 None,
508 None,
509 Some(9),
510 None,
511 Some(1),
512 Some(7),
513 ])
514 .into_array();
515 let mut ctx = SESSION.create_execution_ctx();
516 assert_eq!(
517 min_max(&p, &mut ctx, NumericalAggregateOpts::default())?,
518 Some(MinMaxResult {
519 min: 1.into(),
520 max: 9.into()
521 })
522 );
523 Ok(())
524 }
525
526 #[test]
527 fn test_bool_min_max() -> VortexResult<()> {
528 let mut ctx = SESSION.create_execution_ctx();
529
530 let all_true = BoolArray::new(
531 BitBuffer::from([true, true, true].as_slice()),
532 Validity::NonNullable,
533 )
534 .into_array();
535 assert_eq!(
536 min_max(&all_true, &mut ctx, NumericalAggregateOpts::default())?,
537 Some(MinMaxResult {
538 min: true.into(),
539 max: true.into()
540 })
541 );
542
543 let all_false = BoolArray::new(
544 BitBuffer::from([false, false, false].as_slice()),
545 Validity::NonNullable,
546 )
547 .into_array();
548 assert_eq!(
549 min_max(&all_false, &mut ctx, NumericalAggregateOpts::default())?,
550 Some(MinMaxResult {
551 min: false.into(),
552 max: false.into()
553 })
554 );
555
556 let mixed = BoolArray::new(
557 BitBuffer::from([false, true, false].as_slice()),
558 Validity::NonNullable,
559 )
560 .into_array();
561 assert_eq!(
562 min_max(&mixed, &mut ctx, NumericalAggregateOpts::default())?,
563 Some(MinMaxResult {
564 min: false.into(),
565 max: true.into()
566 })
567 );
568 Ok(())
569 }
570
571 #[test]
572 fn test_null_array() -> VortexResult<()> {
573 let p = NullArray::new(1).into_array();
574 let mut ctx = SESSION.create_execution_ctx();
575 assert_eq!(
576 min_max(&p, &mut ctx, NumericalAggregateOpts::default())?,
577 None
578 );
579 Ok(())
580 }
581
582 #[test]
583 fn test_prim_nan() -> VortexResult<()> {
584 let array = PrimitiveArray::new(
585 buffer![f32::NAN, -f32::NAN, -1.0, 1.0],
586 Validity::NonNullable,
587 );
588 let mut ctx = SESSION.create_execution_ctx();
589 let result = min_max(
590 &array.into_array(),
591 &mut ctx,
592 NumericalAggregateOpts::default(),
593 )?
594 .vortex_expect("should have result");
595 assert_eq!(f32::try_from(&result.min)?, -1.0);
596 assert_eq!(f32::try_from(&result.max)?, 1.0);
597 Ok(())
598 }
599
600 #[test]
601 fn test_prim_inf() -> VortexResult<()> {
602 let array = PrimitiveArray::new(
603 buffer![f32::INFINITY, f32::NEG_INFINITY, -1.0, 1.0],
604 Validity::NonNullable,
605 );
606 let mut ctx = SESSION.create_execution_ctx();
607 let result = min_max(
608 &array.into_array(),
609 &mut ctx,
610 NumericalAggregateOpts::default(),
611 )?
612 .vortex_expect("should have result");
613 assert_eq!(f32::try_from(&result.min)?, f32::NEG_INFINITY);
614 assert_eq!(f32::try_from(&result.max)?, f32::INFINITY);
615 Ok(())
616 }
617
618 #[test]
619 fn test_multi_batch() -> VortexResult<()> {
620 let mut ctx = SESSION.create_execution_ctx();
621 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
622 let mut acc = Accumulator::try_new(MinMax, NumericalAggregateOpts::default(), dtype)?;
623
624 let batch1 = PrimitiveArray::new(buffer![10i32, 20, 5], Validity::NonNullable).into_array();
625 acc.accumulate(&batch1, &mut ctx)?;
626
627 let batch2 = PrimitiveArray::new(buffer![3i32, 25], Validity::NonNullable).into_array();
628 acc.accumulate(&batch2, &mut ctx)?;
629
630 let result = MinMaxResult::from_scalar(acc.finish()?)?.vortex_expect("should have result");
631 assert_eq!(result.min, Scalar::from(3i32));
632 assert_eq!(result.max, Scalar::from(25i32));
633 Ok(())
634 }
635
636 #[test]
637 fn test_finish_resets_state() -> VortexResult<()> {
638 let mut ctx = SESSION.create_execution_ctx();
639 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
640 let mut acc = Accumulator::try_new(MinMax, NumericalAggregateOpts::default(), dtype)?;
641
642 let batch1 = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
643 acc.accumulate(&batch1, &mut ctx)?;
644 let result1 = MinMaxResult::from_scalar(acc.finish()?)?.vortex_expect("should have result");
645 assert_eq!(result1.min, Scalar::from(10i32));
646 assert_eq!(result1.max, Scalar::from(20i32));
647
648 let batch2 = PrimitiveArray::new(buffer![3i32, 6, 9], Validity::NonNullable).into_array();
649 acc.accumulate(&batch2, &mut ctx)?;
650 let result2 = MinMaxResult::from_scalar(acc.finish()?)?.vortex_expect("should have result");
651 assert_eq!(result2.min, Scalar::from(3i32));
652 assert_eq!(result2.max, Scalar::from(9i32));
653 Ok(())
654 }
655
656 #[test]
657 fn test_state_merge() -> VortexResult<()> {
658 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
659 let mut state = MinMax.empty_partial(&NumericalAggregateOpts::default(), &dtype)?;
660
661 let struct_dtype = make_minmax_dtype(&dtype);
662 let scalar1 = Scalar::struct_(
663 struct_dtype.clone(),
664 vec![Scalar::from(5i32), Scalar::from(15i32)],
665 );
666 MinMax.combine_partials(&mut state, scalar1)?;
667
668 let scalar2 = Scalar::struct_(struct_dtype, vec![Scalar::from(2i32), Scalar::from(10i32)]);
669 MinMax.combine_partials(&mut state, scalar2)?;
670
671 let result = MinMaxResult::from_scalar(MinMax.to_scalar(&state)?)?
672 .vortex_expect("should have result");
673 assert_eq!(result.min, Scalar::from(2i32));
674 assert_eq!(result.max, Scalar::from(15i32));
675 Ok(())
676 }
677
678 #[test]
679 fn test_constant_nan() -> VortexResult<()> {
680 let scalar = Scalar::primitive(f16::NAN, Nullability::NonNullable);
681 let array = ConstantArray::new(scalar, 2).into_array();
682 let mut ctx = SESSION.create_execution_ctx();
683 assert_eq!(
684 min_max(&array, &mut ctx, NumericalAggregateOpts::default())?,
685 None
686 );
687 Ok(())
688 }
689
690 const KEEP_NANS: NumericalAggregateOpts = NumericalAggregateOpts::include_nans();
691
692 fn assert_poisoned(result: Option<MinMaxResult>) -> VortexResult<()> {
693 let result = result.vortex_expect("should have result");
694 assert!(f64::try_from(&result.min.cast(&result.min.dtype().as_nullable())?)?.is_nan());
695 assert!(f64::try_from(&result.max.cast(&result.max.dtype().as_nullable())?)?.is_nan());
696 Ok(())
697 }
698
699 #[test]
700 fn test_prim_nan_not_skipping() -> VortexResult<()> {
701 let array = PrimitiveArray::new(
702 buffer![f32::NAN, -f32::NAN, -1.0, 1.0],
703 Validity::NonNullable,
704 )
705 .into_array();
706 let mut ctx = SESSION.create_execution_ctx();
707 assert_poisoned(min_max(&array, &mut ctx, KEEP_NANS)?)
708 }
709
710 #[test]
711 fn test_prim_no_nan_not_skipping() -> VortexResult<()> {
712 let array =
713 PrimitiveArray::new(buffer![3.0f32, -1.0, 1.0], Validity::NonNullable).into_array();
714 let mut ctx = SESSION.create_execution_ctx();
715 let result = min_max(&array, &mut ctx, KEEP_NANS)?.vortex_expect("should have result");
716 assert_eq!(f32::try_from(&result.min)?, -1.0);
717 assert_eq!(f32::try_from(&result.max)?, 3.0);
718 Ok(())
719 }
720
721 #[test]
722 fn test_constant_nan_not_skipping() -> VortexResult<()> {
723 let scalar = Scalar::primitive(f64::NAN, Nullability::NonNullable);
724 let array = ConstantArray::new(scalar, 2).into_array();
725 let mut ctx = SESSION.create_execution_ctx();
726 assert_poisoned(min_max(&array, &mut ctx, KEEP_NANS)?)
727 }
728
729 #[test]
730 fn test_not_skipping_shortcircuits_on_exact_nan_count_stat() -> VortexResult<()> {
731 let array =
734 PrimitiveArray::new(buffer![1.0f64, 2.0, 3.0], Validity::NonNullable).into_array();
735 array
736 .statistics()
737 .set(Stat::NaNCount, Precision::Exact(ScalarValue::from(2u64)));
738 let mut ctx = SESSION.create_execution_ctx();
739 assert_poisoned(min_max(&array, &mut ctx, KEEP_NANS)?)
740 }
741
742 #[test]
743 fn test_not_skipping_uses_cached_stats_when_nan_free() -> VortexResult<()> {
744 let array =
746 PrimitiveArray::new(buffer![1.0f64, 2.0, 3.0], Validity::NonNullable).into_array();
747 array
748 .statistics()
749 .set(Stat::NaNCount, Precision::Exact(ScalarValue::from(0u64)));
750 array
751 .statistics()
752 .set(Stat::Min, Precision::Exact(ScalarValue::from(-10.0f64)));
753 array
754 .statistics()
755 .set(Stat::Max, Precision::Exact(ScalarValue::from(10.0f64)));
756 let mut ctx = SESSION.create_execution_ctx();
757 let result = min_max(&array, &mut ctx, KEEP_NANS)?.vortex_expect("should have result");
758 assert_eq!(f64::try_from(&result.min)?, -10.0);
759 assert_eq!(f64::try_from(&result.max)?, 10.0);
760 Ok(())
761 }
762
763 #[test]
764 fn test_accumulator_nan_including_nullable_cached_stats() -> VortexResult<()> {
765 let mut ctx = SESSION.create_execution_ctx();
769 let array =
770 PrimitiveArray::from_option_iter([Some(1.0f64), Some(2.0), Some(3.0)]).into_array();
771 array
772 .statistics()
773 .set(Stat::NaNCount, Precision::Exact(ScalarValue::from(0u64)));
774 array
775 .statistics()
776 .set(Stat::Min, Precision::Exact(ScalarValue::from(1.0f64)));
777 array
778 .statistics()
779 .set(Stat::Max, Precision::Exact(ScalarValue::from(3.0f64)));
780
781 let mut acc = Accumulator::try_new(MinMax, KEEP_NANS, array.dtype().clone())?;
782 acc.accumulate(&array, &mut ctx)?;
783 let result = MinMaxResult::from_scalar(acc.finish()?)?.vortex_expect("should have result");
784 assert_eq!(f64::try_from(&result.min)?, 1.0);
785 assert_eq!(f64::try_from(&result.max)?, 3.0);
786 Ok(())
787 }
788
789 #[test]
790 fn test_multi_batch_nan_poisoning() -> VortexResult<()> {
791 let mut ctx = SESSION.create_execution_ctx();
792 let dtype = DType::Primitive(PType::F64, Nullability::NonNullable);
793 let mut acc = Accumulator::try_new(MinMax, KEEP_NANS, dtype)?;
794
795 let batch1 = PrimitiveArray::new(buffer![1.0f64, 2.0], Validity::NonNullable).into_array();
796 acc.accumulate(&batch1, &mut ctx)?;
797 assert!(!acc.is_saturated());
798
799 let batch2 = PrimitiveArray::new(buffer![f64::NAN], Validity::NonNullable).into_array();
800 acc.accumulate(&batch2, &mut ctx)?;
801 assert!(acc.is_saturated());
802
803 assert_poisoned(MinMaxResult::from_scalar(acc.finish()?)?)
804 }
805
806 #[test]
807 fn test_chunked() -> VortexResult<()> {
808 let chunk1 = PrimitiveArray::from_option_iter([Some(5i32), None, Some(1)]);
809 let chunk2 = PrimitiveArray::from_option_iter([Some(10i32), Some(3), None]);
810 let dtype = chunk1.dtype().clone();
811 let chunked = ChunkedArray::try_new(vec![chunk1.into_array(), chunk2.into_array()], dtype)?;
812 let mut ctx = SESSION.create_execution_ctx();
813 let result = min_max(
814 &chunked.into_array(),
815 &mut ctx,
816 NumericalAggregateOpts::default(),
817 )?
818 .vortex_expect("should have result");
819 assert_eq!(result.min, Scalar::from(1i32));
820 assert_eq!(result.max, Scalar::from(10i32));
821 Ok(())
822 }
823
824 #[test]
825 fn test_all_null() -> VortexResult<()> {
826 let p = PrimitiveArray::from_option_iter::<i32, _>([None, None, None]);
827 let mut ctx = SESSION.create_execution_ctx();
828 assert_eq!(
829 min_max(&p.into_array(), &mut ctx, NumericalAggregateOpts::default())?,
830 None
831 );
832 Ok(())
833 }
834
835 #[test]
836 fn test_varbin() -> VortexResult<()> {
837 let array = VarBinArray::from_iter(
838 vec![
839 Some("hello world"),
840 None,
841 Some("hello world this is a long string"),
842 None,
843 ],
844 DType::Utf8(Nullability::Nullable),
845 );
846 let mut ctx = SESSION.create_execution_ctx();
847 let result = min_max(
848 &array.into_array(),
849 &mut ctx,
850 NumericalAggregateOpts::default(),
851 )?
852 .vortex_expect("should have result");
853 assert_eq!(
854 result.min,
855 Scalar::utf8("hello world", Nullability::NonNullable)
856 );
857 assert_eq!(
858 result.max,
859 Scalar::utf8(
860 "hello world this is a long string",
861 Nullability::NonNullable
862 )
863 );
864 Ok(())
865 }
866
867 #[test]
868 fn test_decimal() -> VortexResult<()> {
869 let decimal = DecimalArray::new(
870 buffer![100i32, 2000i32, 200i32],
871 DecimalDType::new(4, 2),
872 Validity::from_iter([true, false, true]),
873 );
874 let mut ctx = SESSION.create_execution_ctx();
875 let result = min_max(
876 &decimal.into_array(),
877 &mut ctx,
878 NumericalAggregateOpts::default(),
879 )?
880 .vortex_expect("should have result");
881
882 let non_nullable_dtype = DType::Decimal(DecimalDType::new(4, 2), Nullability::NonNullable);
883 let expected_min = Scalar::try_new(
884 non_nullable_dtype.clone(),
885 Some(ScalarValue::from(DecimalValue::from(100i32))),
886 )?;
887 let expected_max = Scalar::try_new(
888 non_nullable_dtype,
889 Some(ScalarValue::from(DecimalValue::from(200i32))),
890 )?;
891 assert_eq!(result.min, expected_min);
892 assert_eq!(result.max, expected_max);
893 Ok(())
894 }
895
896 #[test]
897 fn list_and_fixed_size_list_return_dtype() {
898 let element_dtype = DType::Primitive(PType::I32, Nullability::Nullable);
899 let list_dtype = DType::List(Arc::new(element_dtype.clone()), Nullability::Nullable);
900 let fixed_size_list_dtype =
901 DType::FixedSizeList(Arc::new(element_dtype), 1, Nullability::Nullable);
902
903 assert_eq!(
904 MinMax.return_dtype(&NumericalAggregateOpts::default(), &list_dtype),
905 Some(make_minmax_dtype(&list_dtype))
906 );
907 assert_eq!(
908 MinMax.return_dtype(&NumericalAggregateOpts::default(), &fixed_size_list_dtype),
909 Some(make_minmax_dtype(&fixed_size_list_dtype))
910 );
911 }
912
913 #[test]
914 fn list_and_fixed_size_list_min_max_returns_none() -> VortexResult<()> {
915 let mut ctx = SESSION.create_execution_ctx();
916
917 let list_array = ListArray::try_new(
918 buffer![1i32, 2, 3].into_array(),
919 buffer![0u32, 2, 3].into_array(),
920 Validity::NonNullable,
921 )?
922 .into_array();
923 assert_eq!(
924 min_max(&list_array, &mut ctx, NumericalAggregateOpts::default())?,
925 None
926 );
927
928 let fixed_size_list_array = FixedSizeListArray::try_new(
929 buffer![1i32, 2, 3, 4].into_array(),
930 2,
931 Validity::NonNullable,
932 2,
933 )?
934 .into_array();
935 assert_eq!(
936 min_max(
937 &fixed_size_list_array,
938 &mut ctx,
939 NumericalAggregateOpts::default()
940 )?,
941 None
942 );
943
944 Ok(())
945 }
946
947 use crate::dtype::half::f16;
948
949 #[test]
950 fn test_bool_with_nulls() -> VortexResult<()> {
951 let mut ctx = SESSION.create_execution_ctx();
952
953 let result = min_max(
954 &BoolArray::from_iter(vec![Some(true), Some(true), None, None]).into_array(),
955 &mut ctx,
956 NumericalAggregateOpts::default(),
957 )?;
958 assert_eq!(
959 result,
960 Some(MinMaxResult {
961 min: Scalar::bool(true, Nullability::NonNullable),
962 max: Scalar::bool(true, Nullability::NonNullable),
963 })
964 );
965
966 let result = min_max(
967 &BoolArray::from_iter(vec![None, Some(true), Some(true)]).into_array(),
968 &mut ctx,
969 NumericalAggregateOpts::default(),
970 )?;
971 assert_eq!(
972 result,
973 Some(MinMaxResult {
974 min: Scalar::bool(true, Nullability::NonNullable),
975 max: Scalar::bool(true, Nullability::NonNullable),
976 })
977 );
978
979 let result = min_max(
980 &BoolArray::from_iter(vec![None, Some(true), Some(true), None]).into_array(),
981 &mut ctx,
982 NumericalAggregateOpts::default(),
983 )?;
984 assert_eq!(
985 result,
986 Some(MinMaxResult {
987 min: Scalar::bool(true, Nullability::NonNullable),
988 max: Scalar::bool(true, Nullability::NonNullable),
989 })
990 );
991
992 let result = min_max(
993 &BoolArray::from_iter(vec![Some(false), Some(false), None, None]).into_array(),
994 &mut ctx,
995 NumericalAggregateOpts::default(),
996 )?;
997 assert_eq!(
998 result,
999 Some(MinMaxResult {
1000 min: Scalar::bool(false, Nullability::NonNullable),
1001 max: Scalar::bool(false, Nullability::NonNullable),
1002 })
1003 );
1004 Ok(())
1005 }
1006
1007 #[test]
1013 fn test_bool_chunked_with_empty_chunk() -> VortexResult<()> {
1014 let mut ctx = SESSION.create_execution_ctx();
1015
1016 let empty = BoolArray::new(BitBuffer::from([].as_slice()), Validity::NonNullable);
1017 let chunk1 = BoolArray::new(
1018 BitBuffer::from([true, true].as_slice()),
1019 Validity::NonNullable,
1020 );
1021 let chunk2 = BoolArray::new(
1022 BitBuffer::from([true, true, true].as_slice()),
1023 Validity::NonNullable,
1024 );
1025 let chunked = ChunkedArray::try_new(
1026 vec![empty.into_array(), chunk1.into_array(), chunk2.into_array()],
1027 DType::Bool(Nullability::NonNullable),
1028 )?;
1029
1030 let result = min_max(
1031 &chunked.into_array(),
1032 &mut ctx,
1033 NumericalAggregateOpts::default(),
1034 )?;
1035 assert_eq!(
1036 result,
1037 Some(MinMaxResult {
1038 min: Scalar::bool(true, Nullability::NonNullable),
1039 max: Scalar::bool(true, Nullability::NonNullable),
1040 })
1041 );
1042 Ok(())
1043 }
1044
1045 #[test]
1052 fn test_chunked_with_empty_constant_chunk() -> VortexResult<()> {
1053 let mut ctx = SESSION.create_execution_ctx();
1054
1055 let empty = ConstantArray::new(Scalar::primitive(u32::MAX, Nullability::NonNullable), 0)
1056 .into_array();
1057 let chunk1 = PrimitiveArray::new(buffer![7631471u32], Validity::NonNullable).into_array();
1058 let chunk2 = PrimitiveArray::new(buffer![0u32], Validity::NonNullable).into_array();
1059 let chunked = ChunkedArray::try_new(
1060 vec![empty, chunk1, chunk2],
1061 DType::Primitive(PType::U32, Nullability::NonNullable),
1062 )?;
1063
1064 assert_eq!(
1065 min_max(
1066 &chunked.into_array(),
1067 &mut ctx,
1068 NumericalAggregateOpts::default()
1069 )?,
1070 Some(MinMaxResult {
1071 min: Scalar::primitive(0u32, Nullability::NonNullable),
1072 max: Scalar::primitive(7631471u32, Nullability::NonNullable),
1073 })
1074 );
1075 Ok(())
1076 }
1077
1078 #[test]
1079 fn test_varbin_all_nulls() -> VortexResult<()> {
1080 let array = VarBinArray::from_iter(
1081 vec![Option::<&str>::None, None, None],
1082 DType::Utf8(Nullability::Nullable),
1083 );
1084 let mut ctx = SESSION.create_execution_ctx();
1085 assert_eq!(
1086 min_max(
1087 &array.into_array(),
1088 &mut ctx,
1089 NumericalAggregateOpts::default()
1090 )?,
1091 None
1092 );
1093 Ok(())
1094 }
1095}