Skip to main content

vortex_array/stats/
stats_set.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::any::type_name;
5use std::fmt::Debug;
6
7use enum_iterator::all;
8use num_traits::CheckedAdd;
9use smallvec::SmallVec;
10use smallvec::smallvec;
11use vortex_error::VortexError;
12use vortex_error::VortexExpect;
13use vortex_error::VortexResult;
14use vortex_error::vortex_err;
15use vortex_error::vortex_panic;
16
17use crate::dtype::DType;
18use crate::expr::stats::IsConstant;
19use crate::expr::stats::IsSorted;
20use crate::expr::stats::IsStrictSorted;
21use crate::expr::stats::Max;
22use crate::expr::stats::Min;
23use crate::expr::stats::NaNCount;
24use crate::expr::stats::NullCount;
25use crate::expr::stats::Precision;
26use crate::expr::stats::Stat;
27use crate::expr::stats::StatBound;
28use crate::expr::stats::StatType;
29use crate::expr::stats::StatsProvider;
30use crate::expr::stats::StatsProviderExt;
31use crate::expr::stats::Sum;
32use crate::expr::stats::UncompressedSizeInBytes;
33use crate::scalar::Scalar;
34use crate::scalar::ScalarValue;
35
36/// Type of the SmallVec stored inside StatsSet
37pub type StatsArray = [(Stat, Precision<ScalarValue>); 4];
38
39#[derive(Default, Debug, Clone)]
40pub struct StatsSet {
41    values: SmallVec<StatsArray>,
42}
43
44impl StatsSet {
45    /// Create new StatSet without validating uniqueness of all the entries
46    ///
47    /// # Safety
48    ///
49    /// This method will not panic or trigger UB, but may lead to duplicate stats being stored.
50    pub unsafe fn new_unchecked(values: SmallVec<StatsArray>) -> Self {
51        Self { values }
52    }
53
54    /// Create StatsSet from single stat and value
55    pub fn of(stat: Stat, value: Precision<ScalarValue>) -> Self {
56        Self {
57            values: smallvec![(stat, value)],
58        }
59    }
60
61    /// Wrap stats set with a dtype for mutable typed scalar access
62    pub fn as_mut_typed_ref<'a, 'b>(&'a mut self, dtype: &'b DType) -> MutTypedStatsSetRef<'a, 'b> {
63        MutTypedStatsSetRef {
64            values: self,
65            dtype,
66        }
67    }
68
69    /// Wrap stats set with a dtype for typed scalar access
70    pub fn as_typed_ref<'a, 'b>(&'a self, dtype: &'b DType) -> TypedStatsSetRef<'a, 'b> {
71        TypedStatsSetRef {
72            values: self,
73            dtype,
74        }
75    }
76}
77
78// Getters and setters for individual stats.
79impl StatsSet {
80    /// Set the stat `stat` to `value`.
81    pub fn set(&mut self, stat: Stat, value: Precision<ScalarValue>) {
82        if let Some(existing) = self.values.iter_mut().find(|(s, _)| *s == stat) {
83            *existing = (stat, value);
84        } else {
85            self.values.push((stat, value));
86        }
87    }
88
89    /// Clear the stat `stat` from the set.
90    pub fn clear(&mut self, stat: Stat) {
91        self.values.retain(|(s, _)| *s != stat);
92    }
93
94    /// Only keep given stats
95    pub fn retain_only(&mut self, stats: &[Stat]) {
96        self.values.retain(|(s, _)| stats.contains(s));
97    }
98
99    /// Iterate over the statistic names and values in-place.
100    ///
101    /// See [Iterator].
102    pub fn iter(&self) -> impl Iterator<Item = &(Stat, Precision<ScalarValue>)> {
103        self.values.iter()
104    }
105
106    /// Get value for a given stat
107    pub fn get(&self, stat: Stat) -> Precision<ScalarValue> {
108        self.get_ref(stat).map(ScalarValue::clone)
109    }
110
111    /// Get a reference to the value for a given stat.
112    fn get_ref(&self, stat: Stat) -> Precision<&ScalarValue> {
113        self.values
114            .iter()
115            .find(|(s, _)| *s == stat)
116            .map(|(_, v)| v.as_ref())
117            .unwrap_or(Precision::Absent)
118    }
119
120    /// Get the value for a count stat, such as [`Stat::NullCount`], without building a [`Scalar`].
121    pub(super) fn get_usize(&self, stat: Stat) -> Precision<usize> {
122        self.get_ref(stat).map(|v| {
123            usize::try_from(v).unwrap_or_else(|err| {
124                vortex_panic!(
125                    err,
126                    "Failed to get stat {} as {}",
127                    stat,
128                    type_name::<usize>()
129                )
130            })
131        })
132    }
133
134    /// Get the value for a boolean stat, such as [`Stat::IsSorted`], without building a
135    /// [`Scalar`].
136    pub(super) fn get_bool(&self, stat: Stat) -> Precision<bool> {
137        self.get_ref(stat).map(|v| match v {
138            ScalarValue::Bool(b) => *b,
139            _ => vortex_panic!("Failed to get stat {} as bool, found {}", stat, v),
140        })
141    }
142
143    /// Length of the stats set
144    pub fn len(&self) -> usize {
145        self.values.len()
146    }
147
148    /// Check whether the statset is empty
149    pub fn is_empty(&self) -> bool {
150        self.values.is_empty()
151    }
152
153    /// Get scalar value of a given dtype
154    pub fn get_as<T: for<'a> TryFrom<&'a Scalar, Error = VortexError>>(
155        &self,
156        stat: Stat,
157        dtype: &DType,
158    ) -> Precision<T> {
159        self.get(stat).map(|v| {
160            T::try_from(
161                &Scalar::try_new(dtype.clone(), Some(v))
162                    .vortex_expect("failed to construct a scalar statistic"),
163            )
164            .unwrap_or_else(|err| {
165                vortex_panic!(err, "Failed to get stat {} as {}", stat, type_name::<T>())
166            })
167        })
168    }
169}
170
171// StatSetIntoIter just exists to protect current implementation from exposure on the public API.
172
173/// Owned iterator over the stats.
174///
175/// See [IntoIterator].
176pub struct StatsSetIntoIter(smallvec::IntoIter<StatsArray>);
177
178impl Iterator for StatsSetIntoIter {
179    type Item = (Stat, Precision<ScalarValue>);
180
181    fn next(&mut self) -> Option<Self::Item> {
182        self.0.next()
183    }
184}
185
186impl IntoIterator for StatsSet {
187    type Item = (Stat, Precision<ScalarValue>);
188    type IntoIter = StatsSetIntoIter;
189
190    fn into_iter(self) -> Self::IntoIter {
191        StatsSetIntoIter(self.values.into_iter())
192    }
193}
194
195impl FromIterator<(Stat, Precision<ScalarValue>)> for StatsSet {
196    fn from_iter<T: IntoIterator<Item = (Stat, Precision<ScalarValue>)>>(iter: T) -> Self {
197        let iter = iter.into_iter();
198
199        let mut this = Self {
200            values: SmallVec::new(),
201        };
202        this.extend(iter);
203        this
204    }
205}
206
207impl Extend<(Stat, Precision<ScalarValue>)> for StatsSet {
208    #[inline]
209    fn extend<T: IntoIterator<Item = (Stat, Precision<ScalarValue>)>>(&mut self, iter: T) {
210        iter.into_iter()
211            .for_each(|(stat, value)| self.set(stat, value));
212    }
213}
214
215/// Merge helpers
216impl StatsSet {
217    /// Merge stats set `other` into `self`, with the semantic assumption that `other`
218    /// contains stats from a disjoint array that is *appended* to the array represented by `self`.
219    pub fn merge_ordered(mut self, other: &Self, dtype: &DType) -> Self {
220        self.as_mut_typed_ref(dtype)
221            .merge_ordered(&other.as_typed_ref(dtype));
222        self
223    }
224
225    /// Merge stats set `other` into `self`, from a disjoint array, with no ordering assumptions.
226    /// Stats that are not commutative (e.g., is_sorted) are dropped from the result.
227    pub fn merge_unordered(mut self, other: &Self, dtype: &DType) -> Self {
228        self.as_mut_typed_ref(dtype)
229            .merge_unordered(&other.as_typed_ref(dtype));
230        self
231    }
232
233    /// Given two sets of stats (of differing precision) for the same array, combine them
234    pub fn combine_sets(&mut self, other: &Self, dtype: &DType) -> VortexResult<()> {
235        self.as_mut_typed_ref(dtype)
236            .combine_sets(&other.as_typed_ref(dtype))
237    }
238}
239
240/// Builds the typed [`Scalar`] for `stat` over an array of `dtype` from its stored value.
241fn stat_scalar(values: &StatsSet, stat: Stat, dtype: &DType) -> Precision<Scalar> {
242    values.get_ref(stat).map(|value| {
243        let stat_dtype = stat
244            .dtype(dtype)
245            .vortex_expect("Must have valid dtype if value is present");
246        Scalar::validate(&stat_dtype, Some(value))
247            .vortex_expect("failed to construct a scalar statistic");
248
249        // SAFETY: The value was validated against the dtype above.
250        unsafe { Scalar::new_unchecked(stat_dtype, Some(value.clone())) }
251    })
252}
253
254pub struct TypedStatsSetRef<'a, 'b> {
255    pub values: &'a StatsSet,
256    pub dtype: &'b DType,
257}
258
259impl StatsProvider for TypedStatsSetRef<'_, '_> {
260    fn get(&self, stat: Stat) -> Precision<Scalar> {
261        stat_scalar(self.values, stat, self.dtype)
262    }
263
264    fn len(&self) -> usize {
265        self.values.len()
266    }
267}
268
269pub struct MutTypedStatsSetRef<'a, 'b> {
270    pub values: &'a mut StatsSet,
271    pub dtype: &'b DType,
272}
273
274impl MutTypedStatsSetRef<'_, '_> {
275    /// Set the stat `stat` to `value`.
276    pub fn set(&mut self, stat: Stat, value: Precision<ScalarValue>) {
277        self.values.set(stat, value);
278    }
279
280    /// Clear the stat `stat` from the set.
281    pub fn clear(&mut self, stat: Stat) {
282        self.values.clear(stat);
283    }
284}
285
286impl StatsProvider for MutTypedStatsSetRef<'_, '_> {
287    fn get(&self, stat: Stat) -> Precision<Scalar> {
288        stat_scalar(self.values, stat, self.dtype)
289    }
290
291    fn len(&self) -> usize {
292        self.values.len()
293    }
294}
295
296// Merge helpers
297impl MutTypedStatsSetRef<'_, '_> {
298    /// Merge stats set `other` into `self`, with the semantic assumption that `other`
299    /// contains stats from a disjoint array that is *appended* to the array represented by `self`.
300    pub fn merge_ordered(mut self, other: &TypedStatsSetRef) -> Self {
301        for s in all::<Stat>() {
302            match s {
303                Stat::IsConstant => self.merge_is_constant(other),
304                Stat::IsSorted => self.merge_is_sorted(other),
305                Stat::IsStrictSorted => self.merge_is_strict_sorted(other),
306                Stat::Max => self.merge_max(other),
307                Stat::Min => self.merge_min(other),
308                Stat::Sum => self.merge_sum(other),
309                Stat::NullCount => self.merge_null_count(other),
310                Stat::UncompressedSizeInBytes => self.merge_uncompressed_size_in_bytes(other),
311                Stat::NaNCount => self.merge_nan_count(other),
312            }
313        }
314
315        self
316    }
317
318    /// Merge stats set `other` into `self`, from a disjoint array, with no ordering assumptions.
319    /// Stats that are not commutative (e.g., is_sorted) are dropped from the result.
320    pub fn merge_unordered(mut self, other: &TypedStatsSetRef) -> Self {
321        for s in all::<Stat>() {
322            if !s.is_commutative() {
323                self.clear(s);
324                continue;
325            }
326
327            match s {
328                Stat::IsConstant => self.merge_is_constant(other),
329                Stat::Max => self.merge_max(other),
330                Stat::Min => self.merge_min(other),
331                Stat::Sum => self.merge_sum(other),
332                Stat::NullCount => self.merge_null_count(other),
333                Stat::UncompressedSizeInBytes => self.merge_uncompressed_size_in_bytes(other),
334                Stat::IsSorted | Stat::IsStrictSorted => {
335                    unreachable!("not commutative")
336                }
337                Stat::NaNCount => self.merge_nan_count(other),
338            }
339        }
340
341        self
342    }
343
344    /// Given two sets of stats (of differing precision) for the same array, combine them
345    pub fn combine_sets(&mut self, other: &TypedStatsSetRef) -> VortexResult<()> {
346        let other_stats: Vec<_> = other.values.iter().map(|(stat, _)| *stat).collect();
347        for s in other_stats {
348            match s {
349                Stat::Max => self.combine_bound::<Max>(other)?,
350                Stat::Min => self.combine_bound::<Min>(other)?,
351                Stat::UncompressedSizeInBytes => {
352                    self.combine_bound::<UncompressedSizeInBytes>(other)?
353                }
354                Stat::IsConstant => self.combine_bool_stat::<IsConstant>(other)?,
355                Stat::IsSorted => self.combine_bool_stat::<IsSorted>(other)?,
356                Stat::IsStrictSorted => self.combine_bool_stat::<IsStrictSorted>(other)?,
357                Stat::NullCount => self.combine_bound::<NullCount>(other)?,
358                Stat::Sum => self.combine_bound::<Sum>(other)?,
359                Stat::NaNCount => self.combine_bound::<NaNCount>(other)?,
360            }
361        }
362        Ok(())
363    }
364
365    fn combine_bound<S: StatType<Scalar>>(&mut self, other: &TypedStatsSetRef) -> VortexResult<()>
366    where
367        S::Bound: StatBound<Scalar> + Debug + Eq + PartialEq,
368    {
369        match (self.get_scalar_bound::<S>(), other.get_scalar_bound::<S>()) {
370            (Some(m1), Some(m2)) => {
371                let meet = m1
372                    .intersection(&m2)
373                    .vortex_expect("can always compare scalar")
374                    .ok_or_else(|| {
375                        vortex_err!("{:?} bounds ({m1:?}, {m2:?}) do not overlap", S::STAT)
376                    })?;
377                if meet != m1 {
378                    self.set(
379                        S::STAT,
380                        meet.into_value().map(|s| {
381                            s.into_value()
382                                .vortex_expect("stat scalar value cannot be null")
383                        }),
384                    );
385                }
386            }
387            (None, Some(m)) => self.set(
388                S::STAT,
389                m.into_value().map(|s| {
390                    s.into_value()
391                        .vortex_expect("stat scalar value cannot be null")
392                }),
393            ),
394            (Some(_), _) => (),
395            (None, None) => self.clear(S::STAT),
396        }
397        Ok(())
398    }
399
400    fn combine_bool_stat<S: StatType<bool>>(&mut self, other: &TypedStatsSetRef) -> VortexResult<()>
401    where
402        S::Bound: StatBound<bool> + Debug + Eq + PartialEq,
403    {
404        match (
405            self.get_as_bound::<S, bool>(),
406            other.get_as_bound::<S, bool>(),
407        ) {
408            (Some(m1), Some(m2)) => {
409                let intersection = m1
410                    .intersection(&m2)
411                    .vortex_expect("can always compare boolean")
412                    .ok_or_else(|| {
413                        vortex_err!("{:?} bounds ({m1:?}, {m2:?}) do not overlap", S::STAT)
414                    })?;
415                if intersection != m1 {
416                    self.set(S::STAT, intersection.into_value().map(ScalarValue::from));
417                }
418            }
419            (None, Some(m)) => self.set(S::STAT, m.into_value().map(ScalarValue::from)),
420            (Some(_), None) => (),
421            (None, None) => self.clear(S::STAT),
422        }
423        Ok(())
424    }
425
426    fn merge_min(&mut self, other: &TypedStatsSetRef) {
427        match (
428            self.get_scalar_bound::<Min>(),
429            other.get_scalar_bound::<Min>(),
430        ) {
431            (Some(m1), Some(m2)) => {
432                let meet = m1.union(&m2).vortex_expect("can compare scalar");
433                if meet != m1 {
434                    self.set(
435                        Stat::Min,
436                        meet.into_value().map(|s| {
437                            s.into_value()
438                                .vortex_expect("stat scalar value cannot be null")
439                        }),
440                    );
441                }
442            }
443            _ => self.clear(Stat::Min),
444        }
445    }
446
447    fn merge_max(&mut self, other: &TypedStatsSetRef) {
448        match (
449            self.get_scalar_bound::<Max>(),
450            other.get_scalar_bound::<Max>(),
451        ) {
452            (Some(m1), Some(m2)) => {
453                let meet = m1.union(&m2).vortex_expect("can compare scalar");
454                if meet != m1 {
455                    self.set(
456                        Stat::Max,
457                        meet.into_value().map(|s| {
458                            s.into_value()
459                                .vortex_expect("stat scalar value cannot be null")
460                        }),
461                    );
462                }
463            }
464            _ => self.clear(Stat::Max),
465        }
466    }
467
468    fn merge_sum(&mut self, other: &TypedStatsSetRef) {
469        match (
470            self.get_scalar_bound::<Sum>(),
471            other.get_scalar_bound::<Sum>(),
472        ) {
473            (Some(m1), Some(m2)) => {
474                // If the combined sum is exact, then we can sum them.
475                let merged = m1.zip(m2).as_exact().and_then(|(s1, s2)| match s1.dtype() {
476                    DType::Primitive(..) => s1
477                        .as_primitive()
478                        .checked_add(&s2.as_primitive())
479                        .and_then(|pscalar| pscalar.pvalue().map(ScalarValue::Primitive)),
480                    // Add widens the result precision, so the merged sum is only exact as a
481                    // stat if it still fits the summands' own decimal type.
482                    DType::Decimal(decimal_dtype, _) => s1
483                        .as_decimal()
484                        .checked_binary_numeric(
485                            &s2.as_decimal(),
486                            crate::scalar::NumericOperator::Add,
487                        )
488                        .ok()
489                        .flatten()
490                        .and_then(|scalar| scalar.as_decimal().decimal_value())
491                        .filter(|value| value.fits_in_precision(*decimal_dtype))
492                        .map(ScalarValue::Decimal),
493                    _ => None,
494                });
495
496                match merged {
497                    Some(scalar_value) => self.set(Stat::Sum, Precision::Exact(scalar_value)),
498                    // An overflow, an inexact bound or a non-numeric dtype leaves the combined
499                    // sum unknown. Keeping this set's own sum would report a partial total.
500                    None => self.clear(Stat::Sum),
501                }
502            }
503            _ => self.clear(Stat::Sum),
504        }
505    }
506
507    fn merge_is_constant(&mut self, other: &TypedStatsSetRef) {
508        let self_const = self.values.get_bool(Stat::IsConstant);
509        let other_const = other.values.get_bool(Stat::IsConstant);
510
511        match (self_const, other_const) {
512            // A non-constant part makes the whole array non-constant.
513            (Precision::Exact(false), _) | (_, Precision::Exact(false)) => {
514                self.set(Stat::IsConstant, Precision::exact(false));
515            }
516            (Precision::Exact(true), Precision::Exact(true)) => {
517                // Both mins have the same dtype, so comparing the values is the same as comparing
518                // scalars.
519                let self_min = self.values.get_ref(Stat::Min).as_exact();
520                let other_min = other.values.get_ref(Stat::Min).as_exact();
521
522                match (self_min, other_min) {
523                    (Some(self_min), Some(other_min)) => {
524                        let is_constant = self_min == other_min;
525                        self.set(Stat::IsConstant, Precision::exact(is_constant));
526                    }
527                    // Without both exact mins, e.g. for an all-null part, the parts cannot be
528                    // compared.
529                    _ => self.clear(Stat::IsConstant),
530                }
531            }
532            _ => self.clear(Stat::IsConstant),
533        }
534    }
535
536    fn merge_is_sorted(&mut self, other: &TypedStatsSetRef) {
537        self.merge_sortedness_stat(other, Stat::IsSorted, PartialOrd::le)
538    }
539
540    fn merge_is_strict_sorted(&mut self, other: &TypedStatsSetRef) {
541        self.merge_sortedness_stat(other, Stat::IsStrictSorted, PartialOrd::lt)
542    }
543
544    fn merge_sortedness_stat<F: Fn(&Scalar, &Scalar) -> bool>(
545        &mut self,
546        other: &TypedStatsSetRef,
547        stat: Stat,
548        cmp: F,
549    ) {
550        if (Precision::Exact(true), Precision::Exact(true))
551            == (self.values.get_bool(stat), other.values.get_bool(stat))
552        {
553            // There might be no stat because it was dropped, or it doesn't exist
554            // (e.g. an all null array).
555            // We assume that it was the dropped case since the doesn't exist might imply sorted,
556            // but this in-precision is correct.
557            if let (Some(self_max), Some(other_min)) = (
558                self.get_scalar_bound::<Max>().and_then(|v| v.max_value()),
559                other.get_scalar_bound::<Min>().and_then(|v| v.min_value()),
560            ) {
561                return if cmp(&self_max, &other_min) {
562                    // keep value
563                } else {
564                    self.set(stat, Precision::inexact(false));
565                };
566            }
567        }
568        self.clear(stat);
569    }
570
571    fn merge_null_count(&mut self, other: &TypedStatsSetRef) {
572        self.merge_sum_stat(Stat::NullCount, other)
573    }
574
575    fn merge_nan_count(&mut self, other: &TypedStatsSetRef) {
576        self.merge_sum_stat(Stat::NaNCount, other)
577    }
578
579    fn merge_uncompressed_size_in_bytes(&mut self, other: &TypedStatsSetRef) {
580        self.merge_sum_stat(Stat::UncompressedSizeInBytes, other)
581    }
582
583    fn merge_sum_stat(&mut self, stat: Stat, other: &TypedStatsSetRef) {
584        let merged = self
585            .values
586            .get_usize(stat)
587            .zip(other.values.get_usize(stat))
588            .map(|(l, r)| ScalarValue::from(l + r));
589
590        if merged.is_absent() {
591            self.clear(stat);
592        } else {
593            self.set(stat, merged);
594        }
595    }
596}
597
598#[cfg(test)]
599mod test {
600    use enum_iterator::all;
601    use itertools::Itertools;
602    use rstest::rstest;
603    use smallvec::smallvec;
604
605    use crate::VortexSessionExecute;
606    use crate::array_session;
607    use crate::arrays::PrimitiveArray;
608    use crate::dtype::DType;
609    use crate::dtype::DecimalDType;
610    use crate::dtype::MAX_PRECISION;
611    use crate::dtype::NativeDecimalType;
612    use crate::dtype::Nullability;
613    use crate::dtype::PType;
614    use crate::dtype::i256;
615    use crate::expr::stats::IsConstant;
616    use crate::expr::stats::Precision;
617    use crate::expr::stats::Stat;
618    use crate::expr::stats::StatsProvider;
619    use crate::expr::stats::StatsProviderExt;
620    use crate::scalar::DecimalValue;
621    use crate::scalar::ScalarValue;
622    use crate::stats::StatsSet;
623    use crate::stats::stats_set::Scalar;
624
625    #[test]
626    fn test_iter() {
627        // SAFETY: No duplicate stats.
628        let set = unsafe {
629            StatsSet::new_unchecked(smallvec![
630                (Stat::Max, Precision::exact(100)),
631                (Stat::Min, Precision::exact(42)),
632            ])
633        };
634        let mut iter = set.iter();
635        let first = iter.next().unwrap().clone();
636        assert_eq!(first.0, Stat::Max);
637        assert_eq!(
638            first.1.map(
639                |f| i32::try_from(&Scalar::try_new(PType::I32.into(), Some(f)).unwrap()).unwrap()
640            ),
641            Precision::exact(100)
642        );
643        let snd = iter.next().unwrap().clone();
644        assert_eq!(snd.0, Stat::Min);
645        assert_eq!(
646            snd.1.map(
647                |s| i32::try_from(&Scalar::try_new(PType::I32.into(), Some(s)).unwrap()).unwrap()
648            ),
649            Precision::exact(42)
650        );
651        assert!(iter.next().is_none());
652    }
653
654    #[test]
655    fn into_iter() {
656        // SAFETY: No duplicate stats.
657        let mut set = unsafe {
658            StatsSet::new_unchecked(smallvec![
659                (Stat::Max, Precision::exact(100)),
660                (Stat::Min, Precision::exact(42)),
661            ])
662        }
663        .into_iter();
664        let (stat, first) = set.next().unwrap();
665        assert_eq!(stat, Stat::Max);
666        assert_eq!(
667            first.map(
668                |f| i32::try_from(&Scalar::try_new(PType::I32.into(), Some(f)).unwrap()).unwrap()
669            ),
670            Precision::exact(100)
671        );
672        let snd = set.next().unwrap();
673        assert_eq!(snd.0, Stat::Min);
674        assert_eq!(
675            snd.1.map(
676                |s| i32::try_from(&Scalar::try_new(PType::I32.into(), Some(s)).unwrap()).unwrap()
677            ),
678            Precision::exact(42)
679        );
680        assert!(set.next().is_none());
681    }
682
683    #[test]
684    fn merge_sums_overflow_clears() {
685        // A sum that cannot be combined exactly leaves the merged set with no Sum at all;
686        // retaining this set's own sum would report a partial total as the combined one.
687        let dtype = DType::Primitive(PType::I64, Nullability::NonNullable);
688        let merged = StatsSet::of(Stat::Sum, Precision::exact(i64::MAX))
689            .merge_ordered(&StatsSet::of(Stat::Sum, Precision::exact(i64::MAX)), &dtype);
690
691        assert!(merged.get(Stat::Sum).is_absent());
692    }
693
694    #[test]
695    fn merge_decimal_sums_out_of_precision_clears() {
696        // A decimal(70, 0) array's Sum stat is itself decimal(76, 0): `sum_decimal_dtype` adds
697        // ten digits of headroom but saturates at MAX_PRECISION, so at this input precision there
698        // is none left and two maximal sums cannot be combined exactly.
699        let dtype = DType::Decimal(DecimalDType::new(70, 0), Nullability::NonNullable);
700        let max = ScalarValue::Decimal(DecimalValue::I256(
701            <i256 as NativeDecimalType>::MAX_BY_PRECISION[usize::from(MAX_PRECISION)],
702        ));
703        let merged = StatsSet::of(Stat::Sum, Precision::exact(max.clone()))
704            .merge_ordered(&StatsSet::of(Stat::Sum, Precision::exact(max)), &dtype);
705
706        assert!(merged.get(Stat::Sum).is_absent());
707    }
708
709    #[test]
710    fn merge_constant() {
711        let first = StatsSet::from_iter([
712            (Stat::Min, Precision::exact(42)),
713            (Stat::IsConstant, Precision::exact(true)),
714        ])
715        .merge_ordered(
716            &StatsSet::from_iter([
717                (Stat::Min, Precision::inexact(42)),
718                (Stat::IsConstant, Precision::exact(true)),
719            ]),
720            &DType::Primitive(PType::I32, Nullability::NonNullable),
721        );
722
723        // With an inexact min, the two constant parts cannot be compared.
724        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
725        assert_eq!(
726            first_ref.get_as::<bool>(Stat::IsConstant),
727            Precision::Absent
728        );
729        assert_eq!(first_ref.get_as::<i32>(Stat::Min), Precision::exact(42));
730    }
731
732    #[rstest]
733    #[case::equal_mins(
734        Precision::exact(true),
735        42,
736        Precision::exact(true),
737        42,
738        Precision::exact(true)
739    )]
740    #[case::different_mins(
741        Precision::exact(true),
742        42,
743        Precision::exact(true),
744        7,
745        Precision::exact(false)
746    )]
747    #[case::first_not_constant(
748        Precision::exact(false),
749        42,
750        Precision::exact(true),
751        42,
752        Precision::exact(false)
753    )]
754    #[case::second_not_constant(
755        Precision::exact(true),
756        42,
757        Precision::exact(false),
758        42,
759        Precision::exact(false)
760    )]
761    #[case::inexact_constant(
762        Precision::inexact(true),
763        42,
764        Precision::exact(true),
765        42,
766        Precision::Absent
767    )]
768    fn merge_constant_parts(
769        #[case] first_const: Precision<bool>,
770        #[case] first_min: i32,
771        #[case] second_const: Precision<bool>,
772        #[case] second_min: i32,
773        #[case] expected: Precision<bool>,
774    ) {
775        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
776        let stats = |is_constant: Precision<bool>, min: i32| {
777            StatsSet::from_iter([
778                (Stat::Min, Precision::exact(min)),
779                (Stat::IsConstant, is_constant.map(ScalarValue::from)),
780            ])
781        };
782
783        let merged =
784            stats(first_const, first_min).merge_ordered(&stats(second_const, second_min), &dtype);
785
786        assert_eq!(
787            merged.as_typed_ref(&dtype).get_as::<bool>(Stat::IsConstant),
788            expected
789        );
790    }
791
792    #[test]
793    fn merge_into_min() {
794        let first = StatsSet::of(Stat::Min, Precision::exact(42)).merge_ordered(
795            &StatsSet::default(),
796            &DType::Primitive(PType::I32, Nullability::NonNullable),
797        );
798
799        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
800        assert!(first_ref.get(Stat::Min).is_absent());
801    }
802
803    #[test]
804    fn merge_from_min() {
805        let first = StatsSet::default().merge_ordered(
806            &StatsSet::of(Stat::Min, Precision::exact(42)),
807            &DType::Primitive(PType::I32, Nullability::NonNullable),
808        );
809
810        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
811        assert!(first_ref.get(Stat::Min).is_absent());
812    }
813
814    #[test]
815    fn merge_mins() {
816        let first = StatsSet::of(Stat::Min, Precision::exact(37)).merge_ordered(
817            &StatsSet::of(Stat::Min, Precision::exact(42)),
818            &DType::Primitive(PType::I32, Nullability::NonNullable),
819        );
820
821        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
822        assert_eq!(first_ref.get_as::<i32>(Stat::Min), Precision::exact(37));
823    }
824
825    #[test]
826    fn merge_into_bound_max() {
827        let first = StatsSet::of(Stat::Max, Precision::exact(42)).merge_ordered(
828            &StatsSet::default(),
829            &DType::Primitive(PType::I32, Nullability::NonNullable),
830        );
831        assert!(first.get(Stat::Max).is_absent());
832    }
833
834    #[test]
835    fn merge_from_max() {
836        let first = StatsSet::default().merge_ordered(
837            &StatsSet::of(Stat::Max, Precision::exact(42)),
838            &DType::Primitive(PType::I32, Nullability::NonNullable),
839        );
840        assert!(first.get(Stat::Max).is_absent());
841    }
842
843    #[test]
844    fn merge_maxes() {
845        let first = StatsSet::of(Stat::Max, Precision::exact(37)).merge_ordered(
846            &StatsSet::of(Stat::Max, Precision::exact(42)),
847            &DType::Primitive(PType::I32, Nullability::NonNullable),
848        );
849        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
850        assert_eq!(first_ref.get_as::<i32>(Stat::Max), Precision::exact(42));
851    }
852
853    #[test]
854    fn merge_maxes_bound() {
855        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
856        let first = StatsSet::of(Stat::Max, Precision::exact(42i32))
857            .merge_ordered(&StatsSet::of(Stat::Max, Precision::inexact(43i32)), &dtype);
858        let first_ref = first.as_typed_ref(&dtype);
859        assert_eq!(first_ref.get_as::<i32>(Stat::Max), Precision::inexact(43));
860    }
861
862    #[test]
863    fn merge_into_scalar() {
864        // Sum stats for primitive types are always the 64-bit version (i64 for signed, u64
865        // for unsigned, f64 for floats).
866        let first = StatsSet::of(Stat::Sum, Precision::exact(42i64)).merge_ordered(
867            &StatsSet::default(),
868            &DType::Primitive(PType::I32, Nullability::NonNullable),
869        );
870        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
871        assert!(first_ref.get(Stat::Sum).is_absent());
872    }
873
874    #[test]
875    fn merge_from_scalar() {
876        // Sum stats for primitive types are always the 64-bit version (i64 for signed, u64
877        // for unsigned, f64 for floats).
878        let first = StatsSet::default().merge_ordered(
879            &StatsSet::of(Stat::Sum, Precision::exact(42i64)),
880            &DType::Primitive(PType::I32, Nullability::NonNullable),
881        );
882        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
883        assert!(first_ref.get(Stat::Sum).is_absent());
884    }
885
886    #[test]
887    fn merge_scalars() {
888        // Sum stats for primitive types are always the 64-bit version (i64 for signed, u64
889        // for unsigned, f64 for floats).
890        let first = StatsSet::of(Stat::Sum, Precision::exact(37i64)).merge_ordered(
891            &StatsSet::of(Stat::Sum, Precision::exact(42i64)),
892            &DType::Primitive(PType::I32, Nullability::NonNullable),
893        );
894        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
895        assert_eq!(first_ref.get_as::<i64>(Stat::Sum), Precision::exact(79i64));
896    }
897
898    #[test]
899    fn merge_into_sortedness() {
900        let first = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true)).merge_ordered(
901            &StatsSet::default(),
902            &DType::Primitive(PType::I32, Nullability::NonNullable),
903        );
904        assert!(first.get(Stat::IsStrictSorted).is_absent());
905    }
906
907    #[test]
908    fn merge_from_sortedness() {
909        let first = StatsSet::default().merge_ordered(
910            &StatsSet::of(Stat::IsStrictSorted, Precision::exact(true)),
911            &DType::Primitive(PType::I32, Nullability::NonNullable),
912        );
913        assert!(first.get(Stat::IsStrictSorted).is_absent());
914    }
915
916    #[test]
917    fn merge_sortedness() {
918        let mut first = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
919        first.set(Stat::Max, Precision::exact(1));
920        let mut second = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
921        second.set(Stat::Min, Precision::exact(2));
922        first = first.merge_ordered(
923            &second,
924            &DType::Primitive(PType::I32, Nullability::NonNullable),
925        );
926
927        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
928        assert_eq!(
929            first_ref.get_as::<bool>(Stat::IsStrictSorted),
930            Precision::exact(true)
931        );
932    }
933
934    #[test]
935    fn merge_sortedness_out_of_order() {
936        let mut first = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
937        first.set(Stat::Min, Precision::exact(1));
938        let mut second = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
939        second.set(Stat::Max, Precision::exact(2));
940        second = second.merge_ordered(
941            &first,
942            &DType::Primitive(PType::I32, Nullability::NonNullable),
943        );
944
945        let second_ref =
946            second.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
947        assert_eq!(
948            second_ref.get_as::<bool>(Stat::IsStrictSorted),
949            Precision::inexact(false)
950        );
951    }
952
953    #[test]
954    fn merge_sortedness_only_one_sorted() {
955        let mut first = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
956        first.set(Stat::Max, Precision::exact(1));
957        let mut second = StatsSet::of(Stat::IsStrictSorted, Precision::exact(false));
958        second.set(Stat::Min, Precision::exact(2));
959        let merged = first.merge_ordered(
960            &second,
961            &DType::Primitive(PType::I32, Nullability::NonNullable),
962        );
963
964        assert!(merged.get(Stat::IsStrictSorted).is_absent());
965    }
966
967    #[test]
968    fn merge_sortedness_missing_min() {
969        let mut first = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
970        first.set(Stat::Max, Precision::exact(1));
971        let second = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
972        first = first.merge_ordered(
973            &second,
974            &DType::Primitive(PType::I32, Nullability::NonNullable),
975        );
976        assert!(first.get(Stat::IsStrictSorted).is_absent());
977    }
978
979    #[test]
980    fn merge_sortedness_bound_min() {
981        let mut first = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
982        first.set(Stat::Max, Precision::exact(1));
983        let mut second = StatsSet::of(Stat::IsStrictSorted, Precision::exact(true));
984        second.set(Stat::Min, Precision::inexact(2));
985        first = first.merge_ordered(
986            &second,
987            &DType::Primitive(PType::I32, Nullability::NonNullable),
988        );
989
990        let first_ref = first.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
991        assert_eq!(
992            first_ref.get_as::<bool>(Stat::IsStrictSorted),
993            Precision::exact(true)
994        );
995    }
996
997    #[test]
998    fn merge_unordered() {
999        let array =
1000            PrimitiveArray::from_option_iter([Some(1), None, Some(2), Some(42), Some(10000), None]);
1001        let all_stats = all::<Stat>()
1002            .filter(|s| !matches!(s, Stat::Sum))
1003            .filter(|s| !matches!(s, Stat::NaNCount))
1004            .collect_vec();
1005        array
1006            .statistics()
1007            .compute_all(&all_stats, &mut array_session().create_execution_ctx())
1008            .unwrap();
1009
1010        let stats = array.statistics().to_owned();
1011        for stat in &all_stats {
1012            assert!(!stats.get(*stat).is_absent(), "Stat {stat} is missing");
1013        }
1014
1015        let merged = stats.clone().merge_unordered(
1016            &stats,
1017            &DType::Primitive(PType::I32, Nullability::NonNullable),
1018        );
1019        for (stat, expected) in [
1020            (Stat::Min, Precision::exact(ScalarValue::from(1i32))),
1021            (Stat::Max, Precision::exact(ScalarValue::from(10000i32))),
1022            (Stat::NullCount, Precision::exact(ScalarValue::from(4u64))),
1023            (
1024                Stat::UncompressedSizeInBytes,
1025                Precision::exact(ScalarValue::from(50u64)),
1026            ),
1027            (Stat::IsConstant, Precision::exact(ScalarValue::from(false))),
1028            (Stat::IsSorted, Precision::Absent),
1029            (Stat::IsStrictSorted, Precision::Absent),
1030        ] {
1031            assert_eq!(merged.get(stat), expected, "{stat}");
1032        }
1033    }
1034
1035    #[test]
1036    fn merge_min_bound_same() {
1037        // Merging a stat with a bound and another with an exact results in exact stat.
1038        // since bound for min is a lower bound, it can in fact contain any value >= bound.
1039        let merged = StatsSet::of(Stat::Min, Precision::inexact(5)).merge_ordered(
1040            &StatsSet::of(Stat::Min, Precision::exact(5)),
1041            &DType::Primitive(PType::I32, Nullability::NonNullable),
1042        );
1043        let merged_ref =
1044            merged.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1045        assert_eq!(merged_ref.get_as::<i32>(Stat::Min), Precision::exact(5));
1046    }
1047
1048    #[test]
1049    fn merge_min_bound_bound_lower() {
1050        let merged = StatsSet::of(Stat::Min, Precision::inexact(4)).merge_ordered(
1051            &StatsSet::of(Stat::Min, Precision::exact(5)),
1052            &DType::Primitive(PType::I32, Nullability::NonNullable),
1053        );
1054        let merged_ref =
1055            merged.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1056        assert_eq!(merged_ref.get_as::<i32>(Stat::Min), Precision::inexact(4));
1057    }
1058
1059    #[test]
1060    fn test_combine_is_constant() {
1061        {
1062            let mut stats = StatsSet::of(Stat::IsConstant, Precision::exact(true));
1063            let stats2 = StatsSet::of(Stat::IsConstant, Precision::exact(true));
1064            let mut stats_ref =
1065                stats.as_mut_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1066            stats_ref
1067                .combine_bool_stat::<IsConstant>(
1068                    &stats2.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable)),
1069                )
1070                .unwrap();
1071            assert_eq!(
1072                stats_ref.get_as::<bool>(Stat::IsConstant),
1073                Precision::exact(true)
1074            );
1075        }
1076
1077        {
1078            let mut stats = StatsSet::of(Stat::IsConstant, Precision::exact(true));
1079            let stats2 = StatsSet::of(Stat::IsConstant, Precision::inexact(false));
1080            let mut stats_ref =
1081                stats.as_mut_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1082            stats_ref
1083                .combine_bool_stat::<IsConstant>(
1084                    &stats2.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable)),
1085                )
1086                .unwrap();
1087            assert_eq!(
1088                stats_ref.get_as::<bool>(Stat::IsConstant),
1089                Precision::exact(true)
1090            );
1091        }
1092
1093        {
1094            let mut stats = StatsSet::of(Stat::IsConstant, Precision::exact(false));
1095            let stats2 = StatsSet::of(Stat::IsConstant, Precision::inexact(false));
1096            let mut stats_ref =
1097                stats.as_mut_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1098            stats_ref
1099                .combine_bool_stat::<IsConstant>(
1100                    &stats2.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable)),
1101                )
1102                .unwrap();
1103            assert_eq!(
1104                stats_ref.get_as::<bool>(Stat::IsConstant),
1105                Precision::exact(false)
1106            );
1107        }
1108    }
1109
1110    #[test]
1111    fn test_combine_sets_boolean_conflict() {
1112        let mut stats1 = StatsSet::from_iter([
1113            (Stat::IsConstant, Precision::exact(true)),
1114            (Stat::IsSorted, Precision::exact(true)),
1115        ]);
1116
1117        let stats2 = StatsSet::from_iter([
1118            (Stat::IsConstant, Precision::exact(false)),
1119            (Stat::IsSorted, Precision::exact(true)),
1120        ]);
1121
1122        let result = stats1.combine_sets(
1123            &stats2,
1124            &DType::Primitive(PType::I32, Nullability::NonNullable),
1125        );
1126        assert!(result.is_err());
1127    }
1128
1129    #[test]
1130    fn test_combine_sets_with_missing_stats() {
1131        let mut stats1 = StatsSet::from_iter([
1132            (Stat::Min, Precision::exact(42)),
1133            (Stat::UncompressedSizeInBytes, Precision::exact(1000u64)),
1134        ]);
1135
1136        let stats2 = StatsSet::from_iter([
1137            (Stat::Max, Precision::exact(100)),
1138            (Stat::IsStrictSorted, Precision::exact(true)),
1139        ]);
1140
1141        stats1
1142            .combine_sets(
1143                &stats2,
1144                &DType::Primitive(PType::I32, Nullability::NonNullable),
1145            )
1146            .unwrap();
1147
1148        let stats_ref =
1149            stats1.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1150
1151        assert_eq!(
1152            stats_ref.get_as::<u64>(Stat::UncompressedSizeInBytes),
1153            Precision::exact(1000u64)
1154        );
1155        // Min should remain unchanged
1156        assert_eq!(stats_ref.get_as::<i32>(Stat::Min), Precision::exact(42));
1157        // Max should be added
1158        assert_eq!(stats_ref.get_as::<i32>(Stat::Max), Precision::exact(100));
1159        // IsStrictSorted should be added
1160        assert_eq!(
1161            stats_ref.get_as::<bool>(Stat::IsStrictSorted),
1162            Precision::exact(true)
1163        );
1164    }
1165
1166    #[test]
1167    fn test_combine_sets_with_inexact() {
1168        let mut stats1 = StatsSet::from_iter([
1169            (Stat::Min, Precision::exact(42)),
1170            (Stat::Max, Precision::inexact(100)),
1171            (Stat::IsConstant, Precision::exact(false)),
1172        ]);
1173
1174        let stats2 = StatsSet::from_iter([
1175            // Must ensure Min from stats2 is <= Min from stats1
1176            (Stat::Min, Precision::inexact(40)),
1177            (Stat::Max, Precision::exact(90)),
1178            (Stat::IsSorted, Precision::exact(true)),
1179        ]);
1180
1181        stats1
1182            .combine_sets(
1183                &stats2,
1184                &DType::Primitive(PType::I32, Nullability::NonNullable),
1185            )
1186            .unwrap();
1187
1188        let stats_ref =
1189            stats1.as_typed_ref(&DType::Primitive(PType::I32, Nullability::NonNullable));
1190
1191        assert_eq!(
1192            stats_ref.get_as::<bool>(Stat::IsConstant),
1193            Precision::exact(false)
1194        );
1195        // Min should remain unchanged since it's more restrictive than the inexact value
1196        assert_eq!(stats_ref.get_as::<i32>(Stat::Min), Precision::exact(42));
1197        // Check that max was updated with the exact value
1198        assert_eq!(stats_ref.get_as::<i32>(Stat::Max), Precision::exact(90));
1199        // Check that IsSorted was added
1200        assert_eq!(
1201            stats_ref.get_as::<bool>(Stat::IsSorted),
1202            Precision::exact(true)
1203        );
1204    }
1205}