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