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