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