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