Skip to main content

vortex_compressor/stats/
integer.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Integer compression statistics.
5
6use std::hash::Hash;
7
8use num_traits::PrimInt;
9use rustc_hash::FxBuildHasher;
10use vortex_array::ExecutionCtx;
11use vortex_array::arrays::PrimitiveArray;
12use vortex_array::arrays::primitive::NativeValue;
13use vortex_array::dtype::IntegerPType;
14use vortex_array::expr::stats::Stat;
15use vortex_array::match_each_integer_ptype;
16use vortex_array::scalar::PValue;
17use vortex_array::scalar::Scalar;
18use vortex_error::VortexError;
19use vortex_error::VortexExpect;
20use vortex_error::VortexResult;
21use vortex_mask::AllOr;
22use vortex_utils::aliases::hash_map::HashMap;
23
24use super::GenerateStatsOptions;
25
26/// Information about the distinct values in an integer array.
27#[derive(Debug, Clone)]
28pub struct DistinctInfo<T> {
29    /// The unique values and their occurrences.
30    distinct_values: HashMap<NativeValue<T>, u32, FxBuildHasher>,
31    /// The count of unique values. This _must_ be non-zero.
32    distinct_count: u32,
33    /// The most frequent value.
34    most_frequent_value: T,
35    /// The number of times the most frequent value occurs.
36    top_frequency: u32,
37}
38
39impl<T> DistinctInfo<T> {
40    /// Returns a reference to the distinct values map.
41    pub fn distinct_values(&self) -> &HashMap<NativeValue<T>, u32, FxBuildHasher> {
42        &self.distinct_values
43    }
44}
45
46/// Typed statistics for a specific integer type.
47#[derive(Debug, Clone)]
48pub struct TypedStats<T> {
49    /// The minimum value.
50    min: T,
51    /// The maximum value.
52    max: T,
53    /// Distinct value information, or `None` if not computed.
54    distinct: Option<DistinctInfo<T>>,
55}
56
57impl<T> TypedStats<T> {
58    /// Returns the distinct value information, if computed.
59    pub fn distinct(&self) -> Option<&DistinctInfo<T>> {
60        self.distinct.as_ref()
61    }
62}
63
64impl<T> TypedStats<T> {
65    /// Get the count of distinct values, if we have computed it already.
66    fn distinct_count(&self) -> Option<u32> {
67        Some(self.distinct.as_ref()?.distinct_count)
68    }
69
70    /// Get the most commonly occurring value and its count, if we have computed it already.
71    fn most_frequent_value_and_count(&self) -> Option<(&T, u32)> {
72        let distinct = self.distinct.as_ref()?;
73        Some((&distinct.most_frequent_value, distinct.top_frequency))
74    }
75}
76
77/// Type-erased container for one of the [`TypedStats`] variants.
78///
79/// Building the `TypedStats` is considerably faster and cheaper than building a type-erased
80/// set of stats. We then perform a variety of access methods on them.
81#[derive(Clone, Debug)]
82pub enum ErasedStats {
83    /// Stats for `u8` arrays.
84    U8(TypedStats<u8>),
85    /// Stats for `u16` arrays.
86    U16(TypedStats<u16>),
87    /// Stats for `u32` arrays.
88    U32(TypedStats<u32>),
89    /// Stats for `u64` arrays.
90    U64(TypedStats<u64>),
91    /// Stats for `i8` arrays.
92    I8(TypedStats<i8>),
93    /// Stats for `i16` arrays.
94    I16(TypedStats<i16>),
95    /// Stats for `i32` arrays.
96    I32(TypedStats<i32>),
97    /// Stats for `i64` arrays.
98    I64(TypedStats<i64>),
99}
100
101impl ErasedStats {
102    /// Returns `true` if the minimum value is zero.
103    pub fn min_is_zero(&self) -> bool {
104        match &self {
105            ErasedStats::U8(x) => x.min == 0,
106            ErasedStats::U16(x) => x.min == 0,
107            ErasedStats::U32(x) => x.min == 0,
108            ErasedStats::U64(x) => x.min == 0,
109            ErasedStats::I8(x) => x.min == 0,
110            ErasedStats::I16(x) => x.min == 0,
111            ErasedStats::I32(x) => x.min == 0,
112            ErasedStats::I64(x) => x.min == 0,
113        }
114    }
115
116    /// Returns `true` if the minimum value is negative.
117    pub fn min_is_negative(&self) -> bool {
118        match &self {
119            ErasedStats::U8(_)
120            | ErasedStats::U16(_)
121            | ErasedStats::U32(_)
122            | ErasedStats::U64(_) => false,
123            ErasedStats::I8(x) => x.min < 0,
124            ErasedStats::I16(x) => x.min < 0,
125            ErasedStats::I32(x) => x.min < 0,
126            ErasedStats::I64(x) => x.min < 0,
127        }
128    }
129
130    /// Difference between max and min.
131    pub fn max_minus_min(&self) -> u64 {
132        match &self {
133            ErasedStats::U8(x) => (x.max - x.min) as u64,
134            ErasedStats::U16(x) => (x.max - x.min) as u64,
135            ErasedStats::U32(x) => (x.max - x.min) as u64,
136            ErasedStats::U64(x) => x.max - x.min,
137            ErasedStats::I8(x) => (x.max as i16 - x.min as i16) as u64,
138            ErasedStats::I16(x) => (x.max as i32 - x.min as i32) as u64,
139            ErasedStats::I32(x) => (x.max as i64 - x.min as i64) as u64,
140            ErasedStats::I64(x) => u64::try_from(x.max as i128 - x.min as i128)
141                .vortex_expect("max minus min result bigger than u64"),
142        }
143    }
144
145    /// Returns the ilog2 of the max value when transmuted to unsigned, or `None` if zero.
146    ///
147    /// This matches how BitPacking computes bit width: it reinterprets signed values as
148    /// unsigned (preserving bit pattern) and uses `leading_zeros`. For non-negative signed
149    /// values, the transmuted value equals the original value.
150    ///
151    /// This is used to determine if FOR encoding would reduce bit width compared to
152    /// direct BitPacking. If `max_ilog2() == max_minus_min_ilog2()`, FOR doesn't help.
153    pub fn max_ilog2(&self) -> Option<u32> {
154        match &self {
155            ErasedStats::U8(x) => x.max.checked_ilog2(),
156            ErasedStats::U16(x) => x.max.checked_ilog2(),
157            ErasedStats::U32(x) => x.max.checked_ilog2(),
158            ErasedStats::U64(x) => x.max.checked_ilog2(),
159            // Transmute signed to unsigned (bit pattern preserved) to match BitPacking behavior.
160            ErasedStats::I8(x) => (x.max as u8).checked_ilog2(),
161            ErasedStats::I16(x) => (x.max as u16).checked_ilog2(),
162            ErasedStats::I32(x) => (x.max as u32).checked_ilog2(),
163            ErasedStats::I64(x) => (x.max as u64).checked_ilog2(),
164        }
165    }
166
167    /// Get the count of distinct values, if we have computed it already.
168    pub fn distinct_count(&self) -> Option<u32> {
169        match &self {
170            ErasedStats::U8(x) => x.distinct_count(),
171            ErasedStats::U16(x) => x.distinct_count(),
172            ErasedStats::U32(x) => x.distinct_count(),
173            ErasedStats::U64(x) => x.distinct_count(),
174            ErasedStats::I8(x) => x.distinct_count(),
175            ErasedStats::I16(x) => x.distinct_count(),
176            ErasedStats::I32(x) => x.distinct_count(),
177            ErasedStats::I64(x) => x.distinct_count(),
178        }
179    }
180
181    /// Get the most commonly occurring value and its count.
182    pub fn most_frequent_value_and_count(&self) -> Option<(PValue, u32)> {
183        match &self {
184            ErasedStats::U8(x) => {
185                let (top_value, count) = x.most_frequent_value_and_count()?;
186                Some(((*top_value).into(), count))
187            }
188            ErasedStats::U16(x) => {
189                let (top_value, count) = x.most_frequent_value_and_count()?;
190                Some(((*top_value).into(), count))
191            }
192            ErasedStats::U32(x) => {
193                let (top_value, count) = x.most_frequent_value_and_count()?;
194                Some(((*top_value).into(), count))
195            }
196            ErasedStats::U64(x) => {
197                let (top_value, count) = x.most_frequent_value_and_count()?;
198                Some(((*top_value).into(), count))
199            }
200            ErasedStats::I8(x) => {
201                let (top_value, count) = x.most_frequent_value_and_count()?;
202                Some(((*top_value).into(), count))
203            }
204            ErasedStats::I16(x) => {
205                let (top_value, count) = x.most_frequent_value_and_count()?;
206                Some(((*top_value).into(), count))
207            }
208            ErasedStats::I32(x) => {
209                let (top_value, count) = x.most_frequent_value_and_count()?;
210                Some(((*top_value).into(), count))
211            }
212            ErasedStats::I64(x) => {
213                let (top_value, count) = x.most_frequent_value_and_count()?;
214                Some(((*top_value).into(), count))
215            }
216        }
217    }
218}
219
220/// Implements `From<TypedStats<$T>>` for [`ErasedStats`].
221macro_rules! impl_from_typed {
222    ($T:ty, $variant:path) => {
223        impl From<TypedStats<$T>> for ErasedStats {
224            fn from(typed: TypedStats<$T>) -> Self {
225                $variant(typed)
226            }
227        }
228    };
229}
230
231impl_from_typed!(u8, ErasedStats::U8);
232impl_from_typed!(u16, ErasedStats::U16);
233impl_from_typed!(u32, ErasedStats::U32);
234impl_from_typed!(u64, ErasedStats::U64);
235impl_from_typed!(i8, ErasedStats::I8);
236impl_from_typed!(i16, ErasedStats::I16);
237impl_from_typed!(i32, ErasedStats::I32);
238impl_from_typed!(i64, ErasedStats::I64);
239
240/// Array of integers and relevant stats for compression.
241#[derive(Clone, Debug)]
242pub struct IntegerStats {
243    /// Cache for `validity.false_count()`.
244    null_count: u32,
245    /// Cache for `validity.true_count()`.
246    value_count: u32,
247    /// The average run length.
248    average_run_length: u32,
249    /// Type-erased typed statistics.
250    erased: ErasedStats,
251}
252
253impl IntegerStats {
254    /// Generates stats, returning an error on failure.
255    fn generate_opts_fallible(
256        input: &PrimitiveArray,
257        opts: GenerateStatsOptions,
258        ctx: &mut ExecutionCtx,
259    ) -> VortexResult<Self> {
260        match_each_integer_ptype!(input.ptype(), |T| {
261            typed_int_stats::<T>(input, opts.count_distinct_values, ctx)
262        })
263    }
264
265    /// Get the count of distinct values, if we have computed it already.
266    pub fn distinct_count(&self) -> Option<u32> {
267        self.erased.distinct_count()
268    }
269
270    /// Get the most commonly occurring value and its count, if we have computed it already.
271    pub fn most_frequent_value_and_count(&self) -> Option<(PValue, u32)> {
272        self.erased.most_frequent_value_and_count()
273    }
274}
275
276impl IntegerStats {
277    /// Generates stats with default options.
278    pub fn generate(input: &PrimitiveArray, ctx: &mut ExecutionCtx) -> Self {
279        Self::generate_opts(input, GenerateStatsOptions::default(), ctx)
280    }
281
282    /// Generates stats with provided options.
283    pub fn generate_opts(
284        input: &PrimitiveArray,
285        opts: GenerateStatsOptions,
286        ctx: &mut ExecutionCtx,
287    ) -> Self {
288        Self::generate_opts_fallible(input, opts, ctx)
289            .vortex_expect("IntegerStats::generate_opts should not fail")
290    }
291
292    /// Returns the number of null values.
293    pub fn null_count(&self) -> u32 {
294        self.null_count
295    }
296
297    /// Returns the number of non-null values.
298    pub fn value_count(&self) -> u32 {
299        self.value_count
300    }
301
302    /// Returns the average run length.
303    pub fn average_run_length(&self) -> u32 {
304        self.average_run_length
305    }
306
307    /// Returns the type-erased typed statistics.
308    pub fn erased(&self) -> &ErasedStats {
309        &self.erased
310    }
311}
312
313/// Computes typed integer statistics for a specific integer type.
314fn typed_int_stats<T>(
315    array: &PrimitiveArray,
316    count_distinct_values: bool,
317    ctx: &mut ExecutionCtx,
318) -> VortexResult<IntegerStats>
319where
320    T: IntegerPType + PrimInt + for<'a> TryFrom<&'a Scalar, Error = VortexError>,
321    TypedStats<T>: Into<ErasedStats>,
322    NativeValue<T>: Eq + Hash,
323{
324    // Special case: empty array.
325    if array.is_empty() {
326        return Ok(IntegerStats {
327            null_count: 0,
328            value_count: 0,
329            average_run_length: 0,
330            erased: TypedStats {
331                min: T::max_value(),
332                max: T::min_value(),
333                distinct: None,
334            }
335            .into(),
336        });
337    }
338
339    if array.all_invalid(ctx)? {
340        return Ok(IntegerStats {
341            null_count: u32::try_from(array.len())?,
342            value_count: 0,
343            average_run_length: 0,
344            erased: TypedStats {
345                min: T::max_value(),
346                max: T::min_value(),
347                distinct: Some(DistinctInfo {
348                    distinct_values: HashMap::with_capacity_and_hasher(0, FxBuildHasher),
349                    distinct_count: 0,
350                    most_frequent_value: T::zero(),
351                    top_frequency: 0,
352                }),
353            }
354            .into(),
355        });
356    }
357
358    let validity = array
359        .as_ref()
360        .validity()?
361        .execute_mask(array.as_ref().len(), ctx)?;
362    let null_count = validity.false_count();
363    let value_count = validity.true_count();
364
365    let array_ref = array.as_ref();
366    let min = array_ref
367        .statistics()
368        .compute_as::<T>(Stat::Min, ctx)
369        .vortex_expect("min should be computed");
370
371    let max = array_ref
372        .statistics()
373        .compute_as::<T>(Stat::Max, ctx)
374        .vortex_expect("max should be computed");
375
376    // Initialize loop state.
377    let head_idx = validity
378        .first()
379        .vortex_expect("All null masks have been handled before");
380    let buffer = array.to_buffer::<T>();
381    let head = buffer[head_idx];
382
383    let mut loop_state = LoopState {
384        distinct: if count_distinct_values {
385            DistinctCounter::new(min, max, array.len())
386        } else {
387            DistinctCounter::Off
388        },
389        prev: head,
390        pending: 0,
391        runs: 1,
392    };
393
394    let sliced = buffer.slice(head_idx..array.len());
395    let (chunks, remainder) = sliced.as_slice().as_chunks::<64>();
396    let remainder_valid = match validity.bit_buffer() {
397        AllOr::All => {
398            for chunk in chunks {
399                inner_loop_nonnull(chunk, count_distinct_values, &mut loop_state)
400            }
401            (1u64 << remainder.len()) - 1
402        }
403        AllOr::None => unreachable!("All invalid arrays have been handled before"),
404        AllOr::Some(v) => {
405            let mask = v.slice(head_idx..array.len());
406            let words = mask.chunks();
407            // One validity word per chunk of 64 values.
408            for (chunk, valid) in chunks.iter().zip(words.iter()) {
409                match valid {
410                    // All nulls -> no stats to update.
411                    0 => {}
412                    // Inner loop for when validity check can be elided.
413                    u64::MAX => inner_loop_nonnull(chunk, count_distinct_values, &mut loop_state),
414                    // Inner loop for when we need to check validity.
415                    _ => inner_loop_nullable(chunk, count_distinct_values, valid, &mut loop_state),
416                }
417            }
418            words.remainder_bits()
419        }
420    };
421
422    // Pad the trailing values into a last chunk, whose padding is null.
423    if remainder_valid != 0 {
424        let mut last = [loop_state.prev; 64];
425        last[..remainder.len()].copy_from_slice(remainder);
426        inner_loop_nullable(
427            &last,
428            count_distinct_values,
429            remainder_valid,
430            &mut loop_state,
431        );
432    }
433
434    if count_distinct_values {
435        loop_state.flush();
436    }
437
438    let runs = loop_state.runs;
439    let distinct = loop_state.distinct.finish();
440
441    let typed = TypedStats { min, max, distinct };
442
443    let null_count = u32::try_from(null_count)?;
444    let value_count = u32::try_from(value_count)?;
445
446    Ok(IntegerStats {
447        null_count,
448        value_count,
449        average_run_length: value_count / runs,
450        erased: typed.into(),
451    })
452}
453
454/// Value ranges up to this many values may be counted in a dense array instead of a hash map.
455const DENSE_DISTINCT_MAX_RANGE: usize = 1 << 16;
456
457/// Value ranges up to this many values are always counted in a dense array, regardless of the array
458/// length. This covers every `u8` and `i8` array.
459const DENSE_DISTINCT_ALWAYS_RANGE: usize = 1 << 8;
460
461/// Accumulates the occurrences of each distinct valid value.
462enum DistinctCounter<T> {
463    /// Distinct values are not being counted.
464    Off,
465    /// Counts indexed by `value - min`, used when the value range is narrow. This avoids hashing
466    /// entirely.
467    Dense {
468        /// The minimum valid value.
469        min: T,
470        /// `min` cast to `usize`. Casting preserves values modulo `2^usize::BITS`, so the wrapping
471        /// difference of two cast values is the exact difference of any two values in the range.
472        min_index: usize,
473        /// The number of occurrences of `min + index`.
474        counts: Vec<u32>,
475    },
476    /// Counts keyed by value.
477    Hashed(HashMap<NativeValue<T>, u32, FxBuildHasher>),
478}
479
480impl<T: IntegerPType> DistinctCounter<T>
481where
482    NativeValue<T>: Eq + Hash,
483{
484    /// Chooses a dense counter when the valid values span a narrow range, otherwise a hash map.
485    fn new(min: T, max: T, len: usize) -> Self {
486        let range_len = match (min.to_i128(), max.to_i128()) {
487            (Some(min), Some(max)) => usize::try_from(max - min + 1).unwrap_or(usize::MAX),
488            _ => usize::MAX,
489        };
490
491        // A dense counter is only worthwhile when it is not much larger than the array itself.
492        if range_len <= DENSE_DISTINCT_ALWAYS_RANGE
493            || (range_len <= DENSE_DISTINCT_MAX_RANGE && range_len <= len)
494        {
495            Self::Dense {
496                min,
497                min_index: min.as_(),
498                counts: vec![0; range_len],
499            }
500        } else {
501            Self::Hashed(HashMap::with_capacity_and_hasher(len / 2, FxBuildHasher))
502        }
503    }
504
505    /// Records `count` occurrences of a valid value.
506    #[allow(clippy::inline_always)]
507    #[inline(always)]
508    fn add(&mut self, value: T, count: u32) {
509        match self {
510            Self::Off => {}
511            Self::Dense {
512                min_index, counts, ..
513            } => counts[value.as_().wrapping_sub(*min_index)] += count,
514            Self::Hashed(distinct_values) => {
515                *distinct_values.entry(NativeValue(value)).or_insert(0) += count
516            }
517        }
518    }
519
520    /// Returns the distinct value information, or `None` if counting was off.
521    fn finish(self) -> Option<DistinctInfo<T>> {
522        let distinct_values: HashMap<NativeValue<T>, u32, FxBuildHasher> = match self {
523            Self::Off => return None,
524            Self::Dense { min, counts, .. } => {
525                let min = min.to_i128().vortex_expect("integers fit in i128");
526                counts
527                    .iter()
528                    .enumerate()
529                    .filter(|&(_, &count)| count > 0)
530                    .map(|(index, &count)| {
531                        let value = <T as num_traits::NumCast>::from(min + index as i128)
532                            .vortex_expect("values between min and max fit in the type");
533                        (NativeValue(value), count)
534                    })
535                    .collect()
536            }
537            Self::Hashed(distinct_values) => distinct_values,
538        };
539
540        let (&top_value, &top_count) = distinct_values
541            .iter()
542            .max_by_key(|&(_, &count)| count)
543            .vortex_expect("we know this is non-empty");
544
545        Some(DistinctInfo {
546            distinct_count: u32::try_from(distinct_values.len())
547                .vortex_expect("there are more than `u32::MAX` distinct values"),
548            most_frequent_value: top_value.0,
549            top_frequency: top_count,
550            distinct_values,
551        })
552    }
553}
554
555/// Internal loop state for integer stats computation.
556struct LoopState<T> {
557    /// The previous value seen.
558    prev: T,
559    /// Occurrences of `prev` in the current run not yet added to `distinct`.
560    pending: u32,
561    /// The run count.
562    runs: u32,
563    /// The distinct values counter.
564    distinct: DistinctCounter<T>,
565}
566
567impl<T: IntegerPType> LoopState<T>
568where
569    NativeValue<T>: Eq + Hash,
570{
571    /// Adds the pending occurrences of `prev` to the distinct values counter.
572    #[allow(clippy::inline_always)]
573    #[inline(always)]
574    fn flush(&mut self) {
575        self.distinct.add(self.prev, self.pending);
576        self.pending = 0;
577    }
578
579    /// Records one value, hashing only when the value differs from `prev`.
580    #[allow(clippy::inline_always)]
581    #[inline(always)]
582    fn push<const COUNT_DISTINCT_VALUES: bool>(&mut self, value: T) {
583        if value != self.prev {
584            if COUNT_DISTINCT_VALUES {
585                self.flush();
586            }
587            self.prev = value;
588            self.runs += 1;
589        }
590        if COUNT_DISTINCT_VALUES {
591            self.pending += 1;
592        }
593    }
594}
595
596/// Inner loop for non-null chunks of 64 values.
597#[allow(clippy::inline_always)]
598#[inline(always)]
599fn inner_loop_nonnull<T: IntegerPType>(
600    values: &[T; 64],
601    count_distinct_values: bool,
602    state: &mut LoopState<T>,
603) where
604    NativeValue<T>: Eq + Hash,
605{
606    if count_distinct_values {
607        inner_loop_nonnull_impl::<T, true>(values, state);
608    } else {
609        inner_loop_nonnull_impl::<T, false>(values, state);
610    }
611}
612
613/// Processes one non-null chunk, monomorphized on whether distinct values are counted.
614#[allow(clippy::inline_always)]
615#[inline(always)]
616fn inner_loop_nonnull_impl<T: IntegerPType, const COUNT_DISTINCT_VALUES: bool>(
617    values: &[T; 64],
618    state: &mut LoopState<T>,
619) where
620    NativeValue<T>: Eq + Hash,
621{
622    // Branch-free count of value changes, including the change from the previous chunk. At
623    // most 64, so a `u8` accumulator lets the comparison use full-width byte lanes.
624    let transitions = u8::from(values[0] != state.prev)
625        + values
626            .iter()
627            .zip(&values[1..])
628            .map(|(a, b)| u8::from(a != b))
629            .sum::<u8>();
630
631    if COUNT_DISTINCT_VALUES {
632        if transitions == 0 {
633            state.pending += 64;
634            return;
635        }
636        for &value in values {
637            if value != state.prev {
638                state.flush();
639                state.prev = value;
640            }
641            state.pending += 1;
642        }
643    }
644
645    state.runs += u32::from(transitions);
646    state.prev = values[63];
647}
648
649/// Inner loop for chunks of 64 values with nulls, whose bits are unset in `valid`.
650#[allow(clippy::inline_always)]
651#[inline(always)]
652fn inner_loop_nullable<T: IntegerPType>(
653    values: &[T; 64],
654    count_distinct_values: bool,
655    valid: u64,
656    state: &mut LoopState<T>,
657) where
658    NativeValue<T>: Eq + Hash,
659{
660    if count_distinct_values {
661        // A filled null would count as an occurrence, so only the valid values are pushed.
662        let mut valid = valid;
663        while valid != 0 {
664            state.push::<true>(values[valid.trailing_zeros() as usize]);
665            valid &= valid - 1;
666        }
667    } else {
668        // A null filled with the valid value before it adds no run, so the chunk takes the
669        // branch-free loop of non-null chunks.
670        inner_loop_nonnull_impl::<T, false>(&forward_fill(values, valid, state.prev), state);
671    }
672}
673
674/// Returns `values` with each null, whose bit is unset in `valid`, replaced by the closest valid
675/// value before it, or by `prev`, the last valid value before the chunk. The cost is one store per
676/// null.
677#[allow(clippy::inline_always)]
678#[inline(always)]
679fn forward_fill<T: Copy>(values: &[T; 64], valid: u64, prev: T) -> [T; 64] {
680    let mut filled = *values;
681    let mut nulls = !valid;
682    while nulls != 0 {
683        let i = nulls.trailing_zeros() as usize;
684        filled[i] = if i == 0 { prev } else { filled[i - 1] };
685        nulls &= nulls - 1;
686    }
687    filled
688}
689
690#[cfg(test)]
691mod tests {
692    use std::iter;
693
694    use rstest::rstest;
695    use vortex_array::VortexSessionExecute;
696    use vortex_array::array_session;
697    use vortex_array::arrays::PrimitiveArray;
698    use vortex_array::validity::Validity;
699    use vortex_buffer::BitBuffer;
700    use vortex_buffer::Buffer;
701    use vortex_buffer::buffer;
702    use vortex_error::VortexResult;
703    use vortex_utils::aliases::hash_map::HashMap;
704
705    use super::ErasedStats;
706    use super::IntegerStats;
707    use super::typed_int_stats;
708
709    #[test]
710    fn test_naive_count_distinct_values() -> VortexResult<()> {
711        let mut ctx = array_session().create_execution_ctx();
712        let array = PrimitiveArray::new(buffer![217u8, 0], Validity::NonNullable);
713        let stats = typed_int_stats::<u8>(&array, true, &mut ctx)?;
714        assert_eq!(stats.distinct_count().unwrap(), 2);
715        Ok(())
716    }
717
718    #[test]
719    fn test_naive_count_distinct_values_nullable() -> VortexResult<()> {
720        let mut ctx = array_session().create_execution_ctx();
721        let array = PrimitiveArray::new(
722            buffer![217u8, 0],
723            Validity::from(BitBuffer::from(vec![true, false])),
724        );
725        let stats = typed_int_stats::<u8>(&array, true, &mut ctx)?;
726        assert_eq!(stats.distinct_count().unwrap(), 1);
727        Ok(())
728    }
729
730    #[test]
731    fn test_count_distinct_values() -> VortexResult<()> {
732        let mut ctx = array_session().create_execution_ctx();
733        let array = PrimitiveArray::new((0..128u8).collect::<Buffer<u8>>(), Validity::NonNullable);
734        let stats = typed_int_stats::<u8>(&array, true, &mut ctx)?;
735        assert_eq!(stats.distinct_count().unwrap(), 128);
736        Ok(())
737    }
738
739    #[test]
740    fn test_count_distinct_values_nullable() -> VortexResult<()> {
741        let mut ctx = array_session().create_execution_ctx();
742        let array = PrimitiveArray::new(
743            (0..128u8).collect::<Buffer<u8>>(),
744            Validity::from(BitBuffer::from_iter(
745                iter::repeat_n(vec![true, false], 64).flatten(),
746            )),
747        );
748        let stats = typed_int_stats::<u8>(&array, true, &mut ctx)?;
749        assert_eq!(stats.distinct_count().unwrap(), 64);
750        Ok(())
751    }
752
753    #[test]
754    fn dense_distinct_signed_full_range() -> VortexResult<()> {
755        let mut ctx = array_session().create_execution_ctx();
756        let values: Buffer<i8> = (i8::MIN..=i8::MAX)
757            .chain(i8::MIN..=i8::MAX)
758            .chain(iter::once(i8::MAX))
759            .collect();
760        let array = PrimitiveArray::new(values, Validity::NonNullable);
761
762        let stats = typed_int_stats::<i8>(&array, true, &mut ctx)?;
763        assert_eq!(stats.distinct_count(), Some(256));
764        assert_eq!(
765            stats.most_frequent_value_and_count(),
766            Some((i8::MAX.into(), 3))
767        );
768        Ok(())
769    }
770
771    #[test]
772    fn dense_distinct_ignores_values_under_nulls() -> VortexResult<()> {
773        let mut ctx = array_session().create_execution_ctx();
774        let array = PrimitiveArray::new(
775            buffer![1000i32, 1001, 1000, 5_000_000, 1003],
776            Validity::from_iter([true, true, true, false, true]),
777        );
778
779        let stats = typed_int_stats::<i32>(&array, true, &mut ctx)?;
780        assert_eq!(stats.distinct_count(), Some(3));
781        assert_eq!(
782            stats.most_frequent_value_and_count(),
783            Some((1000i32.into(), 2))
784        );
785        Ok(())
786    }
787
788    #[test]
789    fn wide_range_distinct() -> VortexResult<()> {
790        let mut ctx = array_session().create_execution_ctx();
791        let array = PrimitiveArray::new(buffer![0u64, u64::MAX, 0], Validity::NonNullable);
792
793        let stats = typed_int_stats::<u64>(&array, true, &mut ctx)?;
794        assert_eq!(stats.distinct_count(), Some(2));
795        assert_eq!(
796            stats.most_frequent_value_and_count(),
797            Some((0u64.into(), 2))
798        );
799        Ok(())
800    }
801
802    #[test]
803    fn test_integer_stats_leading_nulls() {
804        let mut ctx = array_session().create_execution_ctx();
805        let ints = PrimitiveArray::new(buffer![0, 1, 2], Validity::from_iter([false, true, true]));
806
807        let stats = IntegerStats::generate_opts(
808            &ints,
809            crate::stats::GenerateStatsOptions {
810                count_distinct_values: true,
811            },
812            &mut ctx,
813        );
814
815        assert_eq!(stats.value_count, 2);
816        assert_eq!(stats.null_count, 1);
817        assert_eq!(stats.average_run_length, 1);
818        assert_eq!(stats.distinct_count().unwrap(), 2);
819    }
820
821    /// Returns the per-value counts and the run count of the valid values, computed naively.
822    fn naive_stats(values: &[u32], valid: &[bool]) -> (HashMap<u32, u32>, u32) {
823        let mut counts = HashMap::default();
824        let mut runs = 0;
825        let mut prev = None;
826        for (&value, _) in values.iter().zip(valid).filter(|(_, ok)| **ok) {
827            *counts.entry(value).or_insert(0) += 1;
828            if prev.replace(value) != Some(value) {
829                runs += 1;
830            }
831        }
832        (counts, runs)
833    }
834
835    /// Checks the chunked loops against a naive reference. The run lengths cover aligned
836    /// constant chunks (64), runs that cross chunk boundaries (3, 100), and chunks where every
837    /// value changes (1). With nulls, `null_chunks` also nulls two whole chunks.
838    #[rstest]
839    fn test_matches_naive_reference(
840        #[values(1, 63, 64, 65, 64 * 20 + 17)] len: u32,
841        #[values(1, 3, 64, 100)] run_len: u32,
842        #[values(None, Some(97), Some(3))] null_every: Option<u32>,
843        #[values(false, true)] null_chunks: bool,
844        #[values(false, true)] count_distinct_values: bool,
845    ) -> VortexResult<()> {
846        let values: Vec<u32> = (0..len).map(|i| (i / run_len) % 16).collect();
847        let valid: Vec<bool> = (0..len)
848            .map(|i| {
849                null_every.is_none_or(|n| i % n != 0 && !(null_chunks && (64..192).contains(&i)))
850            })
851            .collect();
852        let (expected, runs) = naive_stats(&values, &valid);
853
854        let validity = match null_every {
855            None => Validity::NonNullable,
856            Some(_) => Validity::from(BitBuffer::from(valid)),
857        };
858        let array = PrimitiveArray::new(Buffer::from(values), validity);
859        let mut ctx = array_session().create_execution_ctx();
860        let stats = typed_int_stats::<u32>(&array, count_distinct_values, &mut ctx)?;
861
862        let ErasedStats::U32(typed) = stats.erased() else {
863            unreachable!()
864        };
865        if count_distinct_values {
866            let actual: HashMap<u32, u32> = typed
867                .distinct()
868                .map(|d| d.distinct_values().iter().map(|(k, &c)| (k.0, c)).collect())
869                .unwrap_or_default();
870            assert_eq!(actual, expected);
871        }
872        let value_count: u32 = expected.values().sum();
873        assert_eq!(stats.value_count, value_count);
874        if value_count > 0 {
875            assert_eq!(stats.average_run_length, value_count / runs);
876        }
877        Ok(())
878    }
879}