Skip to main content

datafusion_common/
stats.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! This module provides data structures to represent statistics
19
20use std::fmt::{self, Debug, Display};
21
22use crate::{Result, ScalarValue};
23
24use crate::error::_plan_err;
25use crate::utils::aggregate::precision_add;
26use arrow::datatypes::{DataType, Schema};
27
28/// Represents a value with a degree of certainty. `Precision` is used to
29/// propagate information the precision of statistical values.
30#[derive(Clone, PartialEq, Eq, Default, Copy)]
31pub enum Precision<T: Debug + Clone + PartialEq + Eq + PartialOrd> {
32    /// The exact value is known. Used for guaranteeing correctness.
33    ///
34    /// Comes from definitive sources such as:
35    /// - Parquet file metadata (row counts, byte sizes)
36    /// - In-memory RecordBatch data (actual row counts, byte sizes, null counts)
37    /// - and more...
38    Exact(T),
39    /// The value is not known exactly, but is likely close to this value.
40    /// Used for cost-based optimizations.
41    ///
42    /// Some operations that would result in `Inexact(T)` would be:
43    /// - Applying a filter (selectivity is unknown)
44    /// - Mixing exact and inexact values in arithmetic
45    /// - and more...
46    Inexact(T),
47    /// Nothing is known about the value. This is the default state.
48    ///
49    /// Acts as an absorbing element in arithmetic -> any operation
50    /// involving `Absent` yields `Absent`. [`Precision::to_inexact`]
51    /// on `Absent` returns `Absent`, not `Inexact` — it represents
52    /// a fundamentally different state.
53    ///
54    /// Common sources include:
55    /// - Data sources without statistics
56    /// - Parquet columns missing from file metadata
57    /// - Statistics that cannot be derived for an operation (e.g.,
58    ///   `distinct_count` after a union, `total_byte_size` for joins)
59    #[default]
60    Absent,
61}
62
63impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> Precision<T> {
64    /// If we have some value (exact or inexact), it returns that value.
65    /// Otherwise, it returns `None`.
66    pub fn get_value(&self) -> Option<&T> {
67        match self {
68            Precision::Exact(value) | Precision::Inexact(value) => Some(value),
69            Precision::Absent => None,
70        }
71    }
72
73    /// Transform the value in this [`Precision`] object, if one exists, using
74    /// the given function. Preserves the exactness state.
75    pub fn map<U, F>(self, f: F) -> Precision<U>
76    where
77        F: Fn(T) -> U,
78        U: Debug + Clone + PartialEq + Eq + PartialOrd,
79    {
80        match self {
81            Precision::Exact(val) => Precision::Exact(f(val)),
82            Precision::Inexact(val) => Precision::Inexact(f(val)),
83            _ => Precision::<U>::Absent,
84        }
85    }
86
87    /// Returns `Some(true)` if we have an exact value, `Some(false)` if we
88    /// have an inexact value, and `None` if there is no value.
89    pub fn is_exact(&self) -> Option<bool> {
90        match self {
91            Precision::Exact(_) => Some(true),
92            Precision::Inexact(_) => Some(false),
93            _ => None,
94        }
95    }
96
97    /// Returns the maximum of two (possibly inexact) values, conservatively
98    /// propagating exactness information. If one of the input values is
99    /// [`Precision::Absent`], the result is `Absent` too.
100    pub fn max(&self, other: &Precision<T>) -> Precision<T> {
101        match (self, other) {
102            (Precision::Exact(a), Precision::Exact(b)) => {
103                Precision::Exact(if a >= b { a.clone() } else { b.clone() })
104            }
105            (Precision::Inexact(a), Precision::Exact(b))
106            | (Precision::Exact(a), Precision::Inexact(b))
107            | (Precision::Inexact(a), Precision::Inexact(b)) => {
108                Precision::Inexact(if a >= b { a.clone() } else { b.clone() })
109            }
110            (_, _) => Precision::Absent,
111        }
112    }
113
114    /// Returns the minimum of two (possibly inexact) values, conservatively
115    /// propagating exactness information. If one of the input values is
116    /// [`Precision::Absent`], the result is `Absent` too.
117    pub fn min(&self, other: &Precision<T>) -> Precision<T> {
118        match (self, other) {
119            (Precision::Exact(a), Precision::Exact(b)) => {
120                Precision::Exact(if a >= b { b.clone() } else { a.clone() })
121            }
122            (Precision::Inexact(a), Precision::Exact(b))
123            | (Precision::Exact(a), Precision::Inexact(b))
124            | (Precision::Inexact(a), Precision::Inexact(b)) => {
125                Precision::Inexact(if a >= b { b.clone() } else { a.clone() })
126            }
127            (_, _) => Precision::Absent,
128        }
129    }
130
131    /// Demotes the precision state from exact to inexact (if present).
132    pub fn to_inexact(self) -> Self {
133        match self {
134            Precision::Exact(value) => Precision::Inexact(value),
135            _ => self,
136        }
137    }
138}
139
140impl Precision<usize> {
141    /// Calculates the sum of two (possibly inexact) [`usize`] values,
142    /// conservatively propagating exactness information. If one of the input
143    /// values is [`Precision::Absent`], the result is `Absent` too.
144    pub fn add(&self, other: &Precision<usize>) -> Precision<usize> {
145        match (self, other) {
146            (Precision::Exact(a), Precision::Exact(b)) => a.checked_add(*b).map_or_else(
147                || Precision::Inexact(a.saturating_add(*b)),
148                Precision::Exact,
149            ),
150            (Precision::Inexact(a), Precision::Exact(b))
151            | (Precision::Exact(a), Precision::Inexact(b))
152            | (Precision::Inexact(a), Precision::Inexact(b)) => {
153                Precision::Inexact(a.saturating_add(*b))
154            }
155            (_, _) => Precision::Absent,
156        }
157    }
158
159    /// Calculates the difference of two (possibly inexact) [`usize`] values,
160    /// conservatively propagating exactness information. If one of the input
161    /// values is [`Precision::Absent`], the result is `Absent` too.
162    pub fn sub(&self, other: &Precision<usize>) -> Precision<usize> {
163        match (self, other) {
164            (Precision::Exact(a), Precision::Exact(b)) => a.checked_sub(*b).map_or_else(
165                || Precision::Inexact(a.saturating_sub(*b)),
166                Precision::Exact,
167            ),
168            (Precision::Inexact(a), Precision::Exact(b))
169            | (Precision::Exact(a), Precision::Inexact(b))
170            | (Precision::Inexact(a), Precision::Inexact(b)) => {
171                Precision::Inexact(a.saturating_sub(*b))
172            }
173            (_, _) => Precision::Absent,
174        }
175    }
176
177    /// Calculates the multiplication of two (possibly inexact) [`usize`] values,
178    /// conservatively propagating exactness information. If one of the input
179    /// values is [`Precision::Absent`], the result is `Absent` too.
180    pub fn multiply(&self, other: &Precision<usize>) -> Precision<usize> {
181        match (self, other) {
182            (Precision::Exact(a), Precision::Exact(b)) => a.checked_mul(*b).map_or_else(
183                || Precision::Inexact(a.saturating_mul(*b)),
184                Precision::Exact,
185            ),
186            (Precision::Inexact(a), Precision::Exact(b))
187            | (Precision::Exact(a), Precision::Inexact(b))
188            | (Precision::Inexact(a), Precision::Inexact(b)) => {
189                Precision::Inexact(a.saturating_mul(*b))
190            }
191            (_, _) => Precision::Absent,
192        }
193    }
194
195    /// Return the estimate of applying a filter with estimated selectivity
196    /// `selectivity` to this Precision. A selectivity of `1.0` means that all
197    /// rows are selected. A selectivity of `0.5` means half the rows are
198    /// selected. An exact zero is preserved, since filtering an empty input
199    /// cannot produce rows; any other known value is demoted to inexact.
200    pub fn with_estimated_selectivity(self, selectivity: f64) -> Self {
201        if self == Precision::Exact(0) {
202            return self;
203        }
204        self.map(|v| ((v as f64 * selectivity).ceil()) as usize)
205            .to_inexact()
206    }
207}
208
209impl Precision<ScalarValue> {
210    fn sum_data_type(data_type: &DataType) -> DataType {
211        match data_type {
212            DataType::Int8 | DataType::Int16 | DataType::Int32 => DataType::Int64,
213            DataType::UInt8 | DataType::UInt16 | DataType::UInt32 => DataType::UInt64,
214            _ => data_type.clone(),
215        }
216    }
217
218    fn cast_scalar_to_sum_type(value: &ScalarValue) -> Result<ScalarValue> {
219        let source_type = value.data_type();
220        let target_type = Self::sum_data_type(&source_type);
221        if source_type == target_type {
222            Ok(value.clone())
223        } else {
224            value.cast_to(&target_type)
225        }
226    }
227
228    /// Calculates the sum of two (possibly inexact) [`ScalarValue`] values,
229    /// conservatively propagating exactness information. If one of the input
230    /// values is [`Precision::Absent`], the result is `Absent` too.
231    ///
232    /// Uses [`ScalarValue::add_checked`] so that integer overflow returns
233    /// an error (mapped to `Absent`) instead of silently wrapping.
234    ///
235    /// For performance-sensitive paths prefer `precision_add` which
236    /// avoids the Arrow array round-trip.
237    pub fn add(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
238        match (self, other) {
239            (Precision::Exact(a), Precision::Exact(b)) => a
240                .add_checked(b)
241                .map(Precision::Exact)
242                .unwrap_or(Precision::Absent),
243            (Precision::Inexact(a), Precision::Exact(b))
244            | (Precision::Exact(a), Precision::Inexact(b))
245            | (Precision::Inexact(a), Precision::Inexact(b)) => a
246                .add_checked(b)
247                .map(Precision::Inexact)
248                .unwrap_or(Precision::Absent),
249            (_, _) => Precision::Absent,
250        }
251    }
252
253    /// Casts integer values to the wider SQL `SUM` return type.
254    ///
255    /// This narrows overflow risk when `sum_value` statistics are merged:
256    /// `Int8/Int16/Int32 -> Int64` and `UInt8/UInt16/UInt32 -> UInt64`.
257    pub fn cast_to_sum_type(&self) -> Precision<ScalarValue> {
258        match (self.is_exact(), self.get_value()) {
259            (Some(true), Some(value)) => Self::cast_scalar_to_sum_type(value)
260                .map(Precision::Exact)
261                .unwrap_or(Precision::Absent),
262            (Some(false), Some(value)) => Self::cast_scalar_to_sum_type(value)
263                .map(Precision::Inexact)
264                .unwrap_or(Precision::Absent),
265            (_, _) => Precision::Absent,
266        }
267    }
268
269    /// SUM-style addition with integer widening to match SQL `SUM` return
270    /// types for smaller integral inputs.
271    pub fn add_for_sum(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
272        let mut lhs = self.cast_to_sum_type();
273        let rhs = other.cast_to_sum_type();
274        precision_add(&mut lhs, &rhs);
275        lhs
276    }
277
278    /// Calculates the difference of two (possibly inexact) [`ScalarValue`] values,
279    /// conservatively propagating exactness information. If one of the input
280    /// values is [`Precision::Absent`], the result is `Absent` too.
281    pub fn sub(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
282        match (self, other) {
283            (Precision::Exact(a), Precision::Exact(b)) => {
284                a.sub(b).map(Precision::Exact).unwrap_or(Precision::Absent)
285            }
286            (Precision::Inexact(a), Precision::Exact(b))
287            | (Precision::Exact(a), Precision::Inexact(b))
288            | (Precision::Inexact(a), Precision::Inexact(b)) => a
289                .sub(b)
290                .map(Precision::Inexact)
291                .unwrap_or(Precision::Absent),
292            (_, _) => Precision::Absent,
293        }
294    }
295
296    /// Calculates the multiplication of two (possibly inexact) [`ScalarValue`] values,
297    /// conservatively propagating exactness information. If one of the input
298    /// values is [`Precision::Absent`], the result is `Absent` too.
299    pub fn multiply(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
300        match (self, other) {
301            (Precision::Exact(a), Precision::Exact(b)) => a
302                .mul_checked(b)
303                .map(Precision::Exact)
304                .unwrap_or(Precision::Absent),
305            (Precision::Inexact(a), Precision::Exact(b))
306            | (Precision::Exact(a), Precision::Inexact(b))
307            | (Precision::Inexact(a), Precision::Inexact(b)) => a
308                .mul_checked(b)
309                .map(Precision::Inexact)
310                .unwrap_or(Precision::Absent),
311            (_, _) => Precision::Absent,
312        }
313    }
314
315    /// Casts the value to the given data type, propagating exactness information.
316    pub fn cast_to(&self, data_type: &DataType) -> Result<Precision<ScalarValue>> {
317        match self {
318            Precision::Exact(value) => value.cast_to(data_type).map(Precision::Exact),
319            Precision::Inexact(value) => value.cast_to(data_type).map(Precision::Inexact),
320            Precision::Absent => Ok(Precision::Absent),
321        }
322    }
323}
324
325impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> From<Option<T>> for Precision<T> {
326    fn from(option: Option<T>) -> Self {
327        option.map_or(Precision::Absent, Precision::Exact)
328    }
329}
330
331impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> Debug for Precision<T> {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        match self {
334            Precision::Exact(inner) => write!(f, "Exact({inner:?})"),
335            Precision::Inexact(inner) => write!(f, "Inexact({inner:?})"),
336            Precision::Absent => write!(f, "Absent"),
337        }
338    }
339}
340
341impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> Display for Precision<T> {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        match self {
344            Precision::Exact(inner) => write!(f, "Exact({inner:?})"),
345            Precision::Inexact(inner) => write!(f, "Inexact({inner:?})"),
346            Precision::Absent => write!(f, "Absent"),
347        }
348    }
349}
350
351impl From<Precision<usize>> for Precision<ScalarValue> {
352    fn from(value: Precision<usize>) -> Self {
353        match value {
354            Precision::Exact(v) => Precision::Exact(ScalarValue::UInt64(Some(v as u64))),
355            Precision::Inexact(v) => {
356                Precision::Inexact(ScalarValue::UInt64(Some(v as u64)))
357            }
358            Precision::Absent => Precision::Absent,
359        }
360    }
361}
362
363/// Statistics for a relation
364/// Fields are optional and can be inexact because the sources
365/// sometimes provide approximate estimates for performance reasons
366/// and the transformations output are not always predictable.
367#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct Statistics {
369    /// The number of rows estimated to be scanned.
370    pub num_rows: Precision<usize>,
371    /// The total bytes of the output data.
372    ///
373    /// Note that this is not the same as the total bytes that may be scanned,
374    /// processed, etc.
375    /// E.g. we may read 1GB of data from a Parquet file but the Arrow data
376    /// the node produces may be 2GB; it's this 2GB that is tracked here.
377    pub total_byte_size: Precision<usize>,
378    /// Statistics on a column level.
379    ///
380    /// It must contains a [`ColumnStatistics`] for each field in the schema of
381    /// the table to which the [`Statistics`] refer.
382    pub column_statistics: Vec<ColumnStatistics>,
383}
384
385/// Fallback to use when NDV overlap can not be estimated from column bounds.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
387pub enum NdvFallback {
388    /// Use the larger input NDV. This is the conservative default for
389    /// related fragments such as files from the same table.
390    #[default]
391    Max,
392    /// Sum the input NDVs. This is a conservative upper bound for
393    /// independent inputs such as `UNION ALL`.
394    Sum,
395}
396
397impl NdvFallback {
398    fn merge(self, left: usize, right: usize) -> usize {
399        match self {
400            Self::Max => usize::max(left, right),
401            Self::Sum => left.saturating_add(right),
402        }
403    }
404}
405
406impl Default for Statistics {
407    /// Returns a new [`Statistics`] instance with all fields set to unknown
408    /// and no columns.
409    fn default() -> Self {
410        Self {
411            num_rows: Precision::Absent,
412            total_byte_size: Precision::Absent,
413            column_statistics: vec![],
414        }
415    }
416}
417
418impl Statistics {
419    /// Returns a [`Statistics`] instance for the given schema by assigning
420    /// unknown statistics to each column in the schema.
421    pub fn new_unknown(schema: &Schema) -> Self {
422        Self {
423            num_rows: Precision::Absent,
424            total_byte_size: Precision::Absent,
425            column_statistics: Statistics::unknown_column(schema),
426        }
427    }
428
429    /// Calculates `total_byte_size` based on the schema and `num_rows`.
430    /// If any of the columns has non-primitive width, or `num_rows` is unknown,
431    /// the previous `total_byte_size` is kept but downgraded to inexact rather
432    /// than discarded.
433    pub fn calculate_total_byte_size(&mut self, schema: &Schema) {
434        let mut row_size = Some(0);
435        for field in schema.fields() {
436            match field.data_type().primitive_width() {
437                Some(width) => {
438                    row_size = row_size.map(|s| s + width);
439                }
440                None => {
441                    row_size = None;
442                    break;
443                }
444            }
445        }
446        match (row_size, &self.num_rows) {
447            (None, _) | (Some(_), Precision::Absent) => {
448                self.total_byte_size = self.total_byte_size.to_inexact();
449            }
450            (Some(size), _) => {
451                self.total_byte_size = self.num_rows.multiply(&Precision::Exact(size));
452            }
453        }
454    }
455
456    /// Returns an unbounded `ColumnStatistics` for each field in the schema.
457    pub fn unknown_column(schema: &Schema) -> Vec<ColumnStatistics> {
458        schema
459            .fields()
460            .iter()
461            .map(|_| ColumnStatistics::new_unknown())
462            .collect()
463    }
464
465    /// Set the number of rows
466    pub fn with_num_rows(mut self, num_rows: Precision<usize>) -> Self {
467        self.num_rows = num_rows;
468        self
469    }
470
471    /// Set the total size, in bytes
472    pub fn with_total_byte_size(mut self, total_byte_size: Precision<usize>) -> Self {
473        self.total_byte_size = total_byte_size;
474        self
475    }
476
477    /// Add a column to the column statistics
478    pub fn add_column_statistics(mut self, column_stats: ColumnStatistics) -> Self {
479        self.column_statistics.push(column_stats);
480        self
481    }
482
483    /// If the exactness of a [`Statistics`] instance is lost, this function relaxes
484    /// the exactness of all information by converting them [`Precision::Inexact`].
485    pub fn to_inexact(mut self) -> Self {
486        self.num_rows = self.num_rows.to_inexact();
487        self.total_byte_size = self.total_byte_size.to_inexact();
488        self.column_statistics = self
489            .column_statistics
490            .into_iter()
491            .map(|s| s.to_inexact())
492            .collect();
493        self
494    }
495
496    /// Project the statistics to the given column indices.
497    ///
498    /// For example, if we had statistics for columns `{"a", "b", "c"}`,
499    /// projecting to `vec![2, 1]` would return statistics for columns `{"c",
500    /// "b"}`.
501    pub fn project(self, projection: Option<&impl AsRef<[usize]>>) -> Self {
502        let projection = projection.map(AsRef::as_ref);
503        self.project_impl(projection)
504    }
505
506    fn project_impl(mut self, projection: Option<&[usize]>) -> Self {
507        let Some(projection) = projection.map(AsRef::as_ref) else {
508            return self;
509        };
510
511        #[expect(clippy::large_enum_variant)]
512        enum Slot {
513            /// The column is taken and put into the specified statistics location
514            Taken(usize),
515            /// The original columns is present
516            Present(ColumnStatistics),
517        }
518
519        // Convert to Vec<Slot> so we can avoid copying the statistics
520        let mut columns: Vec<_> = std::mem::take(&mut self.column_statistics)
521            .into_iter()
522            .map(Slot::Present)
523            .collect();
524
525        for idx in projection.iter() {
526            let next_idx = self.column_statistics.len();
527            let slot = std::mem::replace(
528                columns.get_mut(*idx).expect("projection out of bounds"),
529                Slot::Taken(next_idx),
530            );
531            match slot {
532                // The column was there, so just move it
533                Slot::Present(col) => self.column_statistics.push(col),
534                // The column was taken, so copy from the previous location
535                Slot::Taken(prev_idx) => self
536                    .column_statistics
537                    .push(self.column_statistics[prev_idx].clone()),
538            }
539        }
540
541        self
542    }
543
544    /// Calculates the statistics after applying `fetch` and `skip` operations.
545    ///
546    /// Here, `self` denotes per-partition statistics. Use the `n_partitions`
547    /// parameter to compute global statistics in a multi-partition setting.
548    pub fn with_fetch(
549        mut self,
550        fetch: Option<usize>,
551        skip: usize,
552        n_partitions: usize,
553    ) -> Result<Self> {
554        if fetch.is_none() && skip == 0 {
555            return Ok(self);
556        }
557
558        let fetch_val = fetch.unwrap_or(usize::MAX);
559
560        // Get the ratio of rows after / rows before on a per-partition basis
561        let num_rows_before = self.num_rows;
562
563        self.num_rows = match self {
564            Statistics {
565                num_rows: Precision::Exact(nr),
566                ..
567            }
568            | Statistics {
569                num_rows: Precision::Inexact(nr),
570                ..
571            } => {
572                // Here, the inexact case gives us an estimate of the number of rows.
573                if nr <= skip {
574                    // All input data will be skipped. Preserve the exactness of
575                    // the input estimate: if the input was inexact, the
576                    // resulting zero is also inexact.
577                    check_num_rows(Some(0), self.num_rows.is_exact().unwrap())
578                } else if nr <= fetch_val && skip == 0 {
579                    // If the input does not reach the `fetch` globally, and `skip`
580                    // is zero (meaning the input and output are identical), return
581                    // input stats as is.
582                    // TODO: Can input stats still be used, but adjusted, when `skip`
583                    //       is non-zero?
584                    return Ok(self);
585                } else if nr - skip <= fetch_val {
586                    // After `skip` input rows are skipped, the remaining rows are
587                    // less than or equal to the `fetch` values, so `num_rows` must
588                    // equal the remaining rows.
589                    check_num_rows(
590                        (nr - skip).checked_mul(n_partitions),
591                        // We know that we have an estimate for the number of rows:
592                        self.num_rows.is_exact().unwrap(),
593                    )
594                } else {
595                    // At this point we know that we were given a `fetch` value
596                    // as the `None` case would go into the branch above. Since
597                    // the input has more rows than `fetch + skip`, the number
598                    // of rows will be the `fetch`, other statistics will have to be downgraded to inexact.
599                    check_num_rows(
600                        fetch_val.checked_mul(n_partitions),
601                        // We know that we have an estimate for the number of rows:
602                        self.num_rows.is_exact().unwrap(),
603                    )
604                }
605            }
606            Statistics {
607                num_rows: Precision::Absent,
608                ..
609            } => check_num_rows(fetch.and_then(|v| v.checked_mul(n_partitions)), false),
610        };
611        let ratio: Option<f64> = match (num_rows_before, self.num_rows) {
612            (
613                Precision::Exact(nr_before) | Precision::Inexact(nr_before),
614                Precision::Exact(nr_after) | Precision::Inexact(nr_after),
615            ) => {
616                if nr_before == 0 {
617                    Some(0.0)
618                } else {
619                    Some(nr_after as f64 / nr_before as f64)
620                }
621            }
622            _ => None,
623        };
624        self.column_statistics = self
625            .column_statistics
626            .into_iter()
627            .map(|cs| {
628                let mut cs = cs.to_inexact();
629                // Scale byte_size by the row ratio
630                cs.byte_size = match (cs.byte_size, ratio) {
631                    (Precision::Exact(n) | Precision::Inexact(n), Some(ratio)) => {
632                        Precision::Inexact((n as f64 * ratio) as usize)
633                    }
634                    _ => Precision::Absent,
635                };
636                // NDV can never exceed the number of rows
637                if let Some(&rows) = self.num_rows.get_value() {
638                    cs.distinct_count = cs.distinct_count.min(&Precision::Inexact(rows));
639                }
640                cs
641            })
642            .collect();
643
644        // Compute total_byte_size as sum of column byte_size values if all are present,
645        // otherwise fall back to scaling the original total_byte_size
646        let sum_scan_bytes: Option<usize> = self
647            .column_statistics
648            .iter()
649            .map(|cs| cs.byte_size.get_value().copied())
650            .try_fold(0usize, |acc, val| val.map(|v| acc + v));
651
652        self.total_byte_size = match sum_scan_bytes {
653            Some(sum) => Precision::Inexact(sum),
654            None => {
655                // Fall back to scaling original total_byte_size if not all columns have byte_size
656                match (&self.total_byte_size, ratio) {
657                    (Precision::Exact(n) | Precision::Inexact(n), Some(ratio)) => {
658                        Precision::Inexact((*n as f64 * ratio) as usize)
659                    }
660                    _ => Precision::Absent,
661                }
662            }
663        };
664        Ok(self)
665    }
666
667    /// Summarize zero or more statistics into a single `Statistics` instance.
668    ///
669    /// The method assumes that all statistics are for the same schema.
670    /// If not, maybe you can call `SchemaMapper::map_column_statistics` to make them consistent.
671    ///
672    /// This method uses [`NdvFallback::Max`] when `distinct_count` overlap
673    /// can not be estimated from column bounds.
674    ///
675    /// Returns an error if the statistics do not match the specified schemas.
676    ///
677    /// # Example
678    /// ```
679    /// # use datafusion_common::{ColumnStatistics, ScalarValue, Statistics};
680    /// # use arrow::datatypes::{Field, Schema, DataType};
681    /// # use datafusion_common::stats::Precision;
682    /// let stats1 = Statistics::default()
683    ///     .with_num_rows(Precision::Exact(10))
684    ///     .add_column_statistics(
685    ///         ColumnStatistics::new_unknown()
686    ///             .with_min_value(Precision::Exact(ScalarValue::from(1)))
687    ///             .with_max_value(Precision::Exact(ScalarValue::from(100)))
688    ///             .with_sum_value(Precision::Exact(ScalarValue::from(500))),
689    ///     );
690    ///
691    /// let stats2 = Statistics::default()
692    ///     .with_num_rows(Precision::Exact(20))
693    ///     .add_column_statistics(
694    ///         ColumnStatistics::new_unknown()
695    ///             .with_min_value(Precision::Exact(ScalarValue::from(5)))
696    ///             .with_max_value(Precision::Exact(ScalarValue::from(200)))
697    ///             .with_sum_value(Precision::Exact(ScalarValue::from(1000))),
698    ///     );
699    ///
700    /// let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
701    /// let merged = Statistics::try_merge_iter(
702    ///     &[stats1, stats2],
703    ///     &schema,
704    /// ).unwrap();
705    ///
706    /// assert_eq!(merged.num_rows, Precision::Exact(30));
707    /// assert_eq!(merged.column_statistics[0].min_value,
708    ///     Precision::Exact(ScalarValue::from(1)));
709    /// assert_eq!(merged.column_statistics[0].max_value,
710    ///     Precision::Exact(ScalarValue::from(200)));
711    /// assert_eq!(merged.column_statistics[0].sum_value,
712    ///     Precision::Exact(ScalarValue::Int64(Some(1500))));
713    /// ```
714    pub fn try_merge_iter<'a, I>(items: I, schema: &Schema) -> Result<Statistics>
715    where
716        I: IntoIterator<Item = &'a Statistics>,
717    {
718        Self::try_merge_iter_with_ndv_fallback(items, schema, NdvFallback::Max)
719    }
720
721    /// Same as [`Statistics::try_merge_iter`], but lets callers choose the
722    /// fallback used when `distinct_count` overlap can not be estimated.
723    pub fn try_merge_iter_with_ndv_fallback<'a, I>(
724        items: I,
725        schema: &Schema,
726        ndv_fallback: NdvFallback,
727    ) -> Result<Statistics>
728    where
729        I: IntoIterator<Item = &'a Statistics>,
730    {
731        let mut items = items.into_iter();
732        let Some(first) = items.next() else {
733            return Ok(Statistics::new_unknown(schema));
734        };
735        let Some(second) = items.next() else {
736            return Ok(first.clone());
737        };
738
739        let num_cols = first.column_statistics.len();
740        let mut num_rows = first.num_rows;
741        let mut total_byte_size = first.total_byte_size;
742        let mut column_statistics = first.column_statistics.clone();
743        for col_stats in &mut column_statistics {
744            cast_sum_value_to_sum_type_in_place(&mut col_stats.sum_value);
745        }
746
747        // Merge the remaining items in a single pass.
748        for (i, stat) in std::iter::once(second).chain(items).enumerate() {
749            if stat.column_statistics.len() != num_cols {
750                return _plan_err!(
751                    "Cannot merge statistics with different number of columns: {} vs {} (item {})",
752                    num_cols,
753                    stat.column_statistics.len(),
754                    i + 1
755                );
756            }
757            num_rows = num_rows.add(&stat.num_rows);
758            total_byte_size = total_byte_size.add(&stat.total_byte_size);
759
760            // Uses precision_add for sum (reuses the lhs accumulator for
761            // direct numeric addition), while preserving the NDV update
762            // ordering required by estimate_ndv_with_overlap.
763            for (col_stats, item_cs) in
764                column_statistics.iter_mut().zip(&stat.column_statistics)
765            {
766                col_stats.null_count = col_stats.null_count.add(&item_cs.null_count);
767
768                // NDV must be computed before min/max update (needs pre-merge ranges)
769                col_stats.distinct_count = match (
770                    col_stats.distinct_count.get_value(),
771                    item_cs.distinct_count.get_value(),
772                ) {
773                    (Some(&l), Some(&r)) => Precision::Inexact(
774                        estimate_ndv_with_overlap(col_stats, item_cs, l, r)
775                            .unwrap_or_else(|| ndv_fallback.merge(l, r)),
776                    ),
777                    _ => Precision::Absent,
778                };
779                precision_min(&mut col_stats.min_value, &item_cs.min_value);
780                precision_max(&mut col_stats.max_value, &item_cs.max_value);
781                precision_add_for_sum_in_place(
782                    &mut col_stats.sum_value,
783                    &item_cs.sum_value,
784                );
785                col_stats.byte_size = col_stats.byte_size.add(&item_cs.byte_size);
786            }
787        }
788
789        Ok(Statistics {
790            num_rows,
791            total_byte_size,
792            column_statistics,
793        })
794    }
795}
796
797/// Estimates the combined number of distinct values (NDV) when merging two
798/// column statistics, using range overlap to avoid double-counting shared values.
799///
800/// Assumes values are distributed uniformly within each input's
801/// `[min, max]` range (the standard assumption when only summary
802/// statistics are available). Under uniformity the fraction of an input's
803/// distinct values that land in a sub-range equals the fraction of
804/// the range that sub-range covers.
805///
806/// The combined value space is split into three disjoint regions:
807///
808/// ```text
809///   |-- only A --|-- overlap --|-- only B --|
810/// ```
811///
812/// * **Only in A/B** - values outside the other input's range
813///   contribute `(1 - overlap_a) * NDV_a` and `(1 - overlap_b) * NDV_b`.
814/// * **Overlap** - both inputs may produce values here. We take
815///   `max(overlap_a * NDV_a, overlap_b * NDV_b)` rather than the
816///   sum because values in the same sub-range are likely shared
817///   (the smaller set is assumed to be a subset of the larger).
818///
819/// The formula ranges between `[max(NDV_a, NDV_b), NDV_a + NDV_b]`,
820/// from full overlap to no overlap.
821///
822/// ```text
823/// NDV = max(overlap_a * NDV_a, overlap_b * NDV_b)   [intersection]
824///     + (1 - overlap_a) * NDV_a                      [only in A]
825///     + (1 - overlap_b) * NDV_b                      [only in B]
826/// ```
827///
828/// Returns `None` when min/max are absent or distance is unsupported
829/// (e.g. strings), in which case the caller should fall back to a simpler
830/// estimate.
831pub fn estimate_ndv_with_overlap(
832    left: &ColumnStatistics,
833    right: &ColumnStatistics,
834    ndv_left: usize,
835    ndv_right: usize,
836) -> Option<usize> {
837    let left_min = left.min_value.get_value()?;
838    let left_max = left.max_value.get_value()?;
839    let right_min = right.min_value.get_value()?;
840    let right_max = right.max_value.get_value()?;
841
842    let range_left = left_max.distance_u64(left_min)?;
843    let range_right = right_max.distance_u64(right_min)?;
844
845    // Constant columns (range == 0) can't use the proportional overlap
846    // formula below, so check interval overlap directly instead.
847    if range_left == 0 || range_right == 0 {
848        let overlaps = left_min <= right_max && right_min <= left_max;
849        return Some(if overlaps {
850            usize::max(ndv_left, ndv_right)
851        } else {
852            ndv_left + ndv_right
853        });
854    }
855
856    let overlap_min = if left_min >= right_min {
857        left_min
858    } else {
859        right_min
860    };
861    let overlap_max = if left_max <= right_max {
862        left_max
863    } else {
864        right_max
865    };
866
867    // Disjoint ranges: no overlap, NDVs are additive
868    if overlap_min > overlap_max {
869        return Some(ndv_left + ndv_right);
870    }
871
872    let overlap_range = overlap_max.distance_u64(overlap_min)? as f64;
873
874    let overlap_left = overlap_range / range_left as f64;
875    let overlap_right = overlap_range / range_right as f64;
876
877    let intersection = f64::max(
878        overlap_left * ndv_left as f64,
879        overlap_right * ndv_right as f64,
880    );
881    let only_left = (1.0 - overlap_left) * ndv_left as f64;
882    let only_right = (1.0 - overlap_right) * ndv_right as f64;
883
884    Some((intersection + only_left + only_right).round() as usize)
885}
886
887/// Returns the minimum precision while not allocating a new value,
888/// mirrors the semantics of `PartialOrd`.
889#[inline]
890fn precision_min<T>(lhs: &mut Precision<T>, rhs: &Precision<T>)
891where
892    T: Debug + Clone + PartialEq + Eq + PartialOrd,
893{
894    *lhs = match (std::mem::take(lhs), rhs) {
895        (Precision::Exact(left), Precision::Exact(right)) => {
896            if left <= *right {
897                Precision::Exact(left)
898            } else {
899                Precision::Exact(right.clone())
900            }
901        }
902        (Precision::Exact(left), Precision::Inexact(right))
903        | (Precision::Inexact(left), Precision::Exact(right))
904        | (Precision::Inexact(left), Precision::Inexact(right)) => {
905            if left <= *right {
906                Precision::Inexact(left)
907            } else {
908                Precision::Inexact(right.clone())
909            }
910        }
911        (_, _) => Precision::Absent,
912    };
913}
914
915/// Returns the maximum precision while not allocating a new value,
916/// mirrors the semantics of `PartialOrd`.
917#[inline]
918fn precision_max<T>(lhs: &mut Precision<T>, rhs: &Precision<T>)
919where
920    T: Debug + Clone + PartialEq + Eq + PartialOrd,
921{
922    *lhs = match (std::mem::take(lhs), rhs) {
923        (Precision::Exact(left), Precision::Exact(right)) => {
924            if left >= *right {
925                Precision::Exact(left)
926            } else {
927                Precision::Exact(right.clone())
928            }
929        }
930        (Precision::Exact(left), Precision::Inexact(right))
931        | (Precision::Inexact(left), Precision::Exact(right))
932        | (Precision::Inexact(left), Precision::Inexact(right)) => {
933            if left >= *right {
934                Precision::Inexact(left)
935            } else {
936                Precision::Inexact(right.clone())
937            }
938        }
939        (_, _) => Precision::Absent,
940    };
941}
942
943#[inline]
944fn cast_sum_value_to_sum_type_in_place(value: &mut Precision<ScalarValue>) {
945    let (is_exact, inner) = match std::mem::take(value) {
946        Precision::Exact(v) => (true, v),
947        Precision::Inexact(v) => (false, v),
948        Precision::Absent => return,
949    };
950    let source_type = inner.data_type();
951    let target_type = Precision::<ScalarValue>::sum_data_type(&source_type);
952
953    let wrap_precision_fn: fn(ScalarValue) -> Precision<ScalarValue> = if is_exact {
954        Precision::Exact
955    } else {
956        Precision::Inexact
957    };
958
959    *value = if source_type == target_type {
960        wrap_precision_fn(inner)
961    } else {
962        inner
963            .cast_to(&target_type)
964            .map(wrap_precision_fn)
965            .unwrap_or(Precision::Absent)
966    };
967}
968
969#[inline]
970fn precision_add_for_sum_in_place(
971    lhs: &mut Precision<ScalarValue>,
972    rhs: &Precision<ScalarValue>,
973) {
974    let (value, wrap_fn): (&ScalarValue, fn(ScalarValue) -> Precision<ScalarValue>) =
975        match rhs {
976            Precision::Exact(v) => (v, Precision::Exact),
977            Precision::Inexact(v) => (v, Precision::Inexact),
978            Precision::Absent => {
979                *lhs = Precision::Absent;
980                return;
981            }
982        };
983    let source_type = value.data_type();
984    let target_type = Precision::<ScalarValue>::sum_data_type(&source_type);
985    if source_type == target_type {
986        precision_add(lhs, rhs);
987    } else {
988        let rhs = value
989            .cast_to(&target_type)
990            .map(wrap_fn)
991            .unwrap_or(Precision::Absent);
992        precision_add(lhs, &rhs);
993    }
994}
995
996/// Creates an estimate of the number of rows in the output using the given
997/// optional value and exactness flag.
998fn check_num_rows(value: Option<usize>, is_exact: bool) -> Precision<usize> {
999    if let Some(value) = value {
1000        if is_exact {
1001            Precision::Exact(value)
1002        } else {
1003            // If the input stats are inexact, so are the output stats.
1004            Precision::Inexact(value)
1005        }
1006    } else {
1007        // If the estimate is not available (e.g. due to an overflow), we can
1008        // not produce a reliable estimate.
1009        Precision::Absent
1010    }
1011}
1012
1013impl Display for Statistics {
1014    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1015        // string of column statistics
1016        let column_stats = self
1017            .column_statistics
1018            .iter()
1019            .enumerate()
1020            .map(|(i, cs)| {
1021                let s = format!("(Col[{i}]:");
1022                let s = if cs.min_value != Precision::Absent {
1023                    format!("{} Min={}", s, cs.min_value)
1024                } else {
1025                    s
1026                };
1027                let s = if cs.max_value != Precision::Absent {
1028                    format!("{} Max={}", s, cs.max_value)
1029                } else {
1030                    s
1031                };
1032                let s = if cs.sum_value != Precision::Absent {
1033                    format!("{} Sum={}", s, cs.sum_value)
1034                } else {
1035                    s
1036                };
1037                let s = if cs.null_count != Precision::Absent {
1038                    format!("{} Null={}", s, cs.null_count)
1039                } else {
1040                    s
1041                };
1042                let s = if cs.distinct_count != Precision::Absent {
1043                    format!("{} Distinct={}", s, cs.distinct_count)
1044                } else {
1045                    s
1046                };
1047                let s = if cs.byte_size != Precision::Absent {
1048                    format!("{} ScanBytes={}", s, cs.byte_size)
1049                } else {
1050                    s
1051                };
1052
1053                s + ")"
1054            })
1055            .collect::<Vec<_>>()
1056            .join(",");
1057
1058        write!(
1059            f,
1060            "Rows={}, Bytes={}, [{}]",
1061            self.num_rows, self.total_byte_size, column_stats
1062        )?;
1063
1064        Ok(())
1065    }
1066}
1067
1068/// Statistics for a column within a relation
1069#[derive(Clone, Debug, PartialEq, Eq, Default)]
1070pub struct ColumnStatistics {
1071    /// Number of null values on column
1072    pub null_count: Precision<usize>,
1073    /// Maximum value of column
1074    pub max_value: Precision<ScalarValue>,
1075    /// Minimum value of column
1076    pub min_value: Precision<ScalarValue>,
1077    /// Sum value of a column.
1078    ///
1079    /// For integral columns, values should be kept in SUM-compatible widened
1080    /// types (`Int8/Int16/Int32 -> Int64`, `UInt8/UInt16/UInt32 -> UInt64`) to
1081    /// reduce overflow risk during statistics propagation.
1082    ///
1083    /// Callers should prefer [`ColumnStatistics::with_sum_value`] for setting
1084    /// this field and [`Precision<ScalarValue>::add_for_sum`] /
1085    /// [`Precision<ScalarValue>::cast_to_sum_type`] for sum arithmetic.
1086    pub sum_value: Precision<ScalarValue>,
1087    /// Number of distinct values
1088    pub distinct_count: Precision<usize>,
1089    /// Estimated size of this column's data in bytes for the output.
1090    ///
1091    /// Note that this is not the same as the total bytes that may be scanned,
1092    /// processed, etc.
1093    ///
1094    /// E.g. we may read 1GB of data from a Parquet file but the Arrow data
1095    /// the node produces may be 2GB; it's this 2GB that is tracked here.
1096    ///
1097    /// Currently this is accurately calculated for primitive types only.
1098    /// For complex types (like Utf8, List, Struct, etc), this value may be
1099    /// absent or inexact (e.g. estimated from the size of the data in the source Parquet files).
1100    ///
1101    /// This value is automatically scaled when operations like limits or
1102    /// filters reduce the number of rows (see [`Statistics::with_fetch`]).
1103    pub byte_size: Precision<usize>,
1104}
1105
1106impl ColumnStatistics {
1107    /// Column contains a single non null value (e.g constant).
1108    pub fn is_singleton(&self) -> bool {
1109        match (&self.min_value, &self.max_value) {
1110            // Min and max values are the same and not infinity.
1111            (Precision::Exact(min), Precision::Exact(max)) => {
1112                !min.is_null() && !max.is_null() && (min == max)
1113            }
1114            (_, _) => false,
1115        }
1116    }
1117
1118    /// Returns a [`ColumnStatistics`] instance having all [`Precision::Absent`] parameters.
1119    pub fn new_unknown() -> Self {
1120        Self {
1121            null_count: Precision::Absent,
1122            max_value: Precision::Absent,
1123            min_value: Precision::Absent,
1124            sum_value: Precision::Absent,
1125            distinct_count: Precision::Absent,
1126            byte_size: Precision::Absent,
1127        }
1128    }
1129
1130    /// Set the null count
1131    pub fn with_null_count(mut self, null_count: Precision<usize>) -> Self {
1132        self.null_count = null_count;
1133        self
1134    }
1135
1136    /// Set the max value
1137    pub fn with_max_value(mut self, max_value: Precision<ScalarValue>) -> Self {
1138        self.max_value = max_value;
1139        self
1140    }
1141
1142    /// Set the min value
1143    pub fn with_min_value(mut self, min_value: Precision<ScalarValue>) -> Self {
1144        self.min_value = min_value;
1145        self
1146    }
1147
1148    /// Set the sum value
1149    pub fn with_sum_value(mut self, sum_value: Precision<ScalarValue>) -> Self {
1150        self.sum_value = match sum_value {
1151            Precision::Exact(value) => {
1152                Precision::<ScalarValue>::cast_scalar_to_sum_type(&value)
1153                    .map(Precision::Exact)
1154                    .unwrap_or(Precision::Absent)
1155            }
1156            Precision::Inexact(value) => {
1157                Precision::<ScalarValue>::cast_scalar_to_sum_type(&value)
1158                    .map(Precision::Inexact)
1159                    .unwrap_or(Precision::Absent)
1160            }
1161            Precision::Absent => Precision::Absent,
1162        };
1163        self
1164    }
1165
1166    /// Set the distinct count
1167    pub fn with_distinct_count(mut self, distinct_count: Precision<usize>) -> Self {
1168        self.distinct_count = distinct_count;
1169        self
1170    }
1171
1172    /// Set the scan byte size
1173    /// This should initially be set to the total size of the column.
1174    pub fn with_byte_size(mut self, byte_size: Precision<usize>) -> Self {
1175        self.byte_size = byte_size;
1176        self
1177    }
1178
1179    /// If the exactness of a [`ColumnStatistics`] instance is lost, this
1180    /// function relaxes the exactness of all information by converting them
1181    /// [`Precision::Inexact`].
1182    pub fn to_inexact(mut self) -> Self {
1183        self.null_count = self.null_count.to_inexact();
1184        self.max_value = self.max_value.to_inexact();
1185        self.min_value = self.min_value.to_inexact();
1186        self.sum_value = self.sum_value.to_inexact();
1187        self.distinct_count = self.distinct_count.to_inexact();
1188        self.byte_size = self.byte_size.to_inexact();
1189        self
1190    }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    use super::*;
1196    use crate::assert_contains;
1197    use arrow::datatypes::Field;
1198    use std::sync::Arc;
1199
1200    #[test]
1201    fn test_get_value() {
1202        let exact_precision = Precision::Exact(42);
1203        let inexact_precision = Precision::Inexact(23);
1204        let absent_precision = Precision::<i32>::Absent;
1205
1206        assert_eq!(*exact_precision.get_value().unwrap(), 42);
1207        assert_eq!(*inexact_precision.get_value().unwrap(), 23);
1208        assert_eq!(absent_precision.get_value(), None);
1209    }
1210
1211    #[test]
1212    fn test_with_estimated_selectivity() {
1213        // Filtering an empty input cannot produce rows, so the zero stays exact.
1214        assert_eq!(
1215            Precision::Exact(0).with_estimated_selectivity(0.5),
1216            Precision::Exact(0)
1217        );
1218        assert_eq!(
1219            Precision::Exact(0).with_estimated_selectivity(1.0),
1220            Precision::Exact(0)
1221        );
1222
1223        // Any other known value is scaled and demoted, since the selectivity is
1224        // itself an estimate.
1225        assert_eq!(
1226            Precision::Exact(100).with_estimated_selectivity(0.5),
1227            Precision::Inexact(50)
1228        );
1229        assert_eq!(
1230            Precision::Exact(100).with_estimated_selectivity(1.0),
1231            Precision::Inexact(100)
1232        );
1233        assert_eq!(
1234            Precision::Exact(3).with_estimated_selectivity(0.5),
1235            Precision::Inexact(2)
1236        );
1237
1238        // An inexact zero is an estimate, not a proof, and stays inexact.
1239        assert_eq!(
1240            Precision::Inexact(0).with_estimated_selectivity(0.5),
1241            Precision::Inexact(0)
1242        );
1243        assert_eq!(
1244            Precision::<usize>::Absent.with_estimated_selectivity(0.5),
1245            Precision::Absent
1246        );
1247    }
1248
1249    #[test]
1250    fn test_map() {
1251        let exact_precision = Precision::Exact(42);
1252        let inexact_precision = Precision::Inexact(23);
1253        let absent_precision = Precision::Absent;
1254
1255        let squared = |x| x * x;
1256
1257        assert_eq!(exact_precision.map(squared), Precision::Exact(1764));
1258        assert_eq!(inexact_precision.map(squared), Precision::Inexact(529));
1259        assert_eq!(absent_precision.map(squared), Precision::Absent);
1260    }
1261
1262    #[test]
1263    fn test_is_exact() {
1264        let exact_precision = Precision::Exact(42);
1265        let inexact_precision = Precision::Inexact(23);
1266        let absent_precision = Precision::<i32>::Absent;
1267
1268        assert_eq!(exact_precision.is_exact(), Some(true));
1269        assert_eq!(inexact_precision.is_exact(), Some(false));
1270        assert_eq!(absent_precision.is_exact(), None);
1271    }
1272
1273    #[test]
1274    fn test_max() {
1275        let precision1 = Precision::Exact(42);
1276        let precision2 = Precision::Inexact(23);
1277        let precision3 = Precision::Exact(30);
1278        let absent_precision = Precision::Absent;
1279
1280        assert_eq!(precision1.max(&precision2), Precision::Inexact(42));
1281        assert_eq!(precision1.max(&precision3), Precision::Exact(42));
1282        assert_eq!(precision2.max(&precision3), Precision::Inexact(30));
1283        assert_eq!(precision1.max(&absent_precision), Precision::Absent);
1284    }
1285
1286    #[test]
1287    fn test_min() {
1288        let precision1 = Precision::Exact(42);
1289        let precision2 = Precision::Inexact(23);
1290        let precision3 = Precision::Exact(30);
1291        let absent_precision = Precision::Absent;
1292
1293        assert_eq!(precision1.min(&precision2), Precision::Inexact(23));
1294        assert_eq!(precision1.min(&precision3), Precision::Exact(30));
1295        assert_eq!(precision2.min(&precision3), Precision::Inexact(23));
1296        assert_eq!(precision1.min(&absent_precision), Precision::Absent);
1297    }
1298
1299    #[test]
1300    fn test_to_inexact() {
1301        let exact_precision = Precision::Exact(42);
1302        let inexact_precision = Precision::Inexact(42);
1303        let absent_precision = Precision::<i32>::Absent;
1304
1305        assert_eq!(exact_precision.to_inexact(), inexact_precision);
1306        assert_eq!(inexact_precision.to_inexact(), inexact_precision);
1307        assert_eq!(absent_precision.to_inexact(), absent_precision);
1308    }
1309
1310    #[test]
1311    fn test_add() {
1312        let precision1 = Precision::Exact(42);
1313        let precision2 = Precision::Inexact(23);
1314        let precision3 = Precision::Exact(30);
1315        let absent_precision = Precision::Absent;
1316        let precision_max_exact = Precision::Exact(usize::MAX);
1317        let precision_max_inexact = Precision::Exact(usize::MAX);
1318
1319        assert_eq!(precision1.add(&precision2), Precision::Inexact(65));
1320        assert_eq!(precision1.add(&precision3), Precision::Exact(72));
1321        assert_eq!(precision2.add(&precision3), Precision::Inexact(53));
1322        assert_eq!(precision1.add(&absent_precision), Precision::Absent);
1323        assert_eq!(
1324            precision_max_exact.add(&precision1),
1325            Precision::Inexact(usize::MAX)
1326        );
1327        assert_eq!(
1328            precision_max_inexact.add(&precision1),
1329            Precision::Inexact(usize::MAX)
1330        );
1331    }
1332
1333    #[test]
1334    fn test_add_scalar() {
1335        let precision = Precision::Exact(ScalarValue::Int32(Some(42)));
1336
1337        assert_eq!(
1338            precision.add(&Precision::Exact(ScalarValue::Int32(Some(23)))),
1339            Precision::Exact(ScalarValue::Int32(Some(65))),
1340        );
1341        assert_eq!(
1342            precision.add(&Precision::Inexact(ScalarValue::Int32(Some(23)))),
1343            Precision::Inexact(ScalarValue::Int32(Some(65))),
1344        );
1345        assert_eq!(
1346            precision.add(&Precision::Exact(ScalarValue::Int32(None))),
1347            // As per behavior of ScalarValue::add
1348            Precision::Exact(ScalarValue::Int32(None)),
1349        );
1350        assert_eq!(precision.add(&Precision::Absent), Precision::Absent);
1351    }
1352
1353    #[test]
1354    fn test_add_for_sum_scalar_integer_widening() {
1355        let precision = Precision::Exact(ScalarValue::Int32(Some(42)));
1356
1357        assert_eq!(
1358            precision.add_for_sum(&Precision::Exact(ScalarValue::Int32(Some(23)))),
1359            Precision::Exact(ScalarValue::Int64(Some(65))),
1360        );
1361        assert_eq!(
1362            precision.add_for_sum(&Precision::Inexact(ScalarValue::Int32(Some(23)))),
1363            Precision::Inexact(ScalarValue::Int64(Some(65))),
1364        );
1365    }
1366
1367    #[test]
1368    fn test_add_for_sum_prevents_int32_overflow() {
1369        let lhs = Precision::Exact(ScalarValue::Int32(Some(i32::MAX)));
1370        let rhs = Precision::Exact(ScalarValue::Int32(Some(1)));
1371
1372        assert_eq!(
1373            lhs.add_for_sum(&rhs),
1374            Precision::Exact(ScalarValue::Int64(Some(i64::from(i32::MAX) + 1))),
1375        );
1376    }
1377
1378    #[test]
1379    fn test_add_for_sum_scalar_unsigned_integer_widening() {
1380        let precision = Precision::Exact(ScalarValue::UInt32(Some(42)));
1381
1382        assert_eq!(
1383            precision.add_for_sum(&Precision::Exact(ScalarValue::UInt32(Some(23)))),
1384            Precision::Exact(ScalarValue::UInt64(Some(65))),
1385        );
1386        assert_eq!(
1387            precision.add_for_sum(&Precision::Inexact(ScalarValue::UInt32(Some(23)))),
1388            Precision::Inexact(ScalarValue::UInt64(Some(65))),
1389        );
1390    }
1391
1392    #[test]
1393    fn test_sub() {
1394        let precision1 = Precision::Exact(42);
1395        let precision2 = Precision::Inexact(23);
1396        let precision3 = Precision::Exact(30);
1397        let absent_precision = Precision::Absent;
1398
1399        assert_eq!(precision1.sub(&precision2), Precision::Inexact(19));
1400        assert_eq!(precision1.sub(&precision3), Precision::Exact(12));
1401        assert_eq!(precision2.sub(&precision1), Precision::Inexact(0));
1402        assert_eq!(precision3.sub(&precision1), Precision::Inexact(0));
1403        assert_eq!(precision1.sub(&absent_precision), Precision::Absent);
1404    }
1405
1406    #[test]
1407    fn test_sub_scalar() {
1408        let precision = Precision::Exact(ScalarValue::Int32(Some(42)));
1409
1410        assert_eq!(
1411            precision.sub(&Precision::Exact(ScalarValue::Int32(Some(23)))),
1412            Precision::Exact(ScalarValue::Int32(Some(19))),
1413        );
1414        assert_eq!(
1415            precision.sub(&Precision::Inexact(ScalarValue::Int32(Some(23)))),
1416            Precision::Inexact(ScalarValue::Int32(Some(19))),
1417        );
1418        assert_eq!(
1419            precision.sub(&Precision::Exact(ScalarValue::Int32(None))),
1420            // As per behavior of ScalarValue::sub
1421            Precision::Exact(ScalarValue::Int32(None)),
1422        );
1423        assert_eq!(precision.sub(&Precision::Absent), Precision::Absent);
1424    }
1425
1426    #[test]
1427    fn test_multiply() {
1428        let precision1 = Precision::Exact(6);
1429        let precision2 = Precision::Inexact(3);
1430        let precision3 = Precision::Exact(5);
1431        let precision_max_exact = Precision::Exact(usize::MAX);
1432        let precision_max_inexact = Precision::Exact(usize::MAX);
1433        let absent_precision = Precision::Absent;
1434
1435        assert_eq!(precision1.multiply(&precision2), Precision::Inexact(18));
1436        assert_eq!(precision1.multiply(&precision3), Precision::Exact(30));
1437        assert_eq!(precision2.multiply(&precision3), Precision::Inexact(15));
1438        assert_eq!(precision1.multiply(&absent_precision), Precision::Absent);
1439        assert_eq!(
1440            precision_max_exact.multiply(&precision1),
1441            Precision::Inexact(usize::MAX)
1442        );
1443        assert_eq!(
1444            precision_max_inexact.multiply(&precision1),
1445            Precision::Inexact(usize::MAX)
1446        );
1447    }
1448
1449    #[test]
1450    fn test_multiply_scalar() {
1451        let precision = Precision::Exact(ScalarValue::Int32(Some(6)));
1452
1453        assert_eq!(
1454            precision.multiply(&Precision::Exact(ScalarValue::Int32(Some(5)))),
1455            Precision::Exact(ScalarValue::Int32(Some(30))),
1456        );
1457        assert_eq!(
1458            precision.multiply(&Precision::Inexact(ScalarValue::Int32(Some(5)))),
1459            Precision::Inexact(ScalarValue::Int32(Some(30))),
1460        );
1461        assert_eq!(
1462            precision.multiply(&Precision::Exact(ScalarValue::Int32(None))),
1463            // As per behavior of ScalarValue::mul_checked
1464            Precision::Exact(ScalarValue::Int32(None)),
1465        );
1466        assert_eq!(precision.multiply(&Precision::Absent), Precision::Absent);
1467    }
1468
1469    #[test]
1470    fn test_cast_to() {
1471        // Valid
1472        assert_eq!(
1473            Precision::Exact(ScalarValue::Int32(Some(42)))
1474                .cast_to(&DataType::Int64)
1475                .unwrap(),
1476            Precision::Exact(ScalarValue::Int64(Some(42))),
1477        );
1478        assert_eq!(
1479            Precision::Inexact(ScalarValue::Int32(Some(42)))
1480                .cast_to(&DataType::Int64)
1481                .unwrap(),
1482            Precision::Inexact(ScalarValue::Int64(Some(42))),
1483        );
1484        // Null
1485        assert_eq!(
1486            Precision::Exact(ScalarValue::Int32(None))
1487                .cast_to(&DataType::Int64)
1488                .unwrap(),
1489            Precision::Exact(ScalarValue::Int64(None)),
1490        );
1491        // Overflow returns error
1492        assert!(
1493            Precision::Exact(ScalarValue::Int32(Some(256)))
1494                .cast_to(&DataType::Int8)
1495                .is_err()
1496        );
1497    }
1498
1499    #[test]
1500    fn test_precision_cloning() {
1501        // Precision<usize> is copy
1502        let precision: Precision<usize> = Precision::Exact(42);
1503        let p2 = precision;
1504        assert_eq!(precision, p2);
1505
1506        // Precision<ScalarValue> is not copy (requires .clone())
1507        let precision: Precision<ScalarValue> =
1508            Precision::Exact(ScalarValue::Int64(Some(42)));
1509        let p2 = precision.clone();
1510        assert_eq!(precision, p2);
1511    }
1512
1513    #[test]
1514    fn test_project_none() {
1515        let projection: Option<Vec<usize>> = None;
1516        let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
1517        assert_eq!(stats, make_stats(vec![10, 20, 30]));
1518    }
1519
1520    #[test]
1521    fn test_project_empty() {
1522        let projection = Some(vec![]);
1523        let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
1524        assert_eq!(stats, make_stats(vec![]));
1525    }
1526
1527    #[test]
1528    fn test_project_swap() {
1529        let projection = Some(vec![2, 1]);
1530        let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
1531        assert_eq!(stats, make_stats(vec![30, 20]));
1532    }
1533
1534    #[test]
1535    fn test_project_repeated() {
1536        let projection = Some(vec![1, 2, 1, 1, 0, 2]);
1537        let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
1538        assert_eq!(stats, make_stats(vec![20, 30, 20, 20, 10, 30]));
1539    }
1540
1541    // Make a Statistics structure with the specified null counts for each column
1542    fn make_stats(counts: impl IntoIterator<Item = usize>) -> Statistics {
1543        Statistics {
1544            num_rows: Precision::Exact(42),
1545            total_byte_size: Precision::Exact(500),
1546            column_statistics: counts.into_iter().map(col_stats_i64).collect(),
1547        }
1548    }
1549
1550    fn col_stats_i64(null_count: usize) -> ColumnStatistics {
1551        ColumnStatistics {
1552            null_count: Precision::Exact(null_count),
1553            max_value: Precision::Exact(ScalarValue::Int64(Some(42))),
1554            min_value: Precision::Exact(ScalarValue::Int64(Some(64))),
1555            sum_value: Precision::Exact(ScalarValue::Int64(Some(4600))),
1556            distinct_count: Precision::Exact(100),
1557            byte_size: Precision::Exact(800),
1558        }
1559    }
1560
1561    fn make_single_i64_ndv_stats(
1562        distinct_count: Precision<usize>,
1563        min_value: Option<i64>,
1564        max_value: Option<i64>,
1565    ) -> Statistics {
1566        let to_precision = |value| Precision::Exact(ScalarValue::Int64(Some(value)));
1567
1568        Statistics::default()
1569            .with_num_rows(Precision::Exact(10))
1570            .add_column_statistics(
1571                ColumnStatistics::new_unknown()
1572                    .with_distinct_count(distinct_count)
1573                    .with_min_value(
1574                        min_value.map(to_precision).unwrap_or(Precision::Absent),
1575                    )
1576                    .with_max_value(
1577                        max_value.map(to_precision).unwrap_or(Precision::Absent),
1578                    ),
1579            )
1580    }
1581
1582    fn merge_single_i64_ndv_distinct_count(
1583        left: Statistics,
1584        right: Statistics,
1585        ndv_fallback: NdvFallback,
1586    ) -> Precision<usize> {
1587        let schema = Schema::new(vec![Field::new("a", DataType::Int64, true)]);
1588
1589        Statistics::try_merge_iter_with_ndv_fallback(
1590            [&left, &right],
1591            &schema,
1592            ndv_fallback,
1593        )
1594        .unwrap()
1595        .column_statistics[0]
1596            .distinct_count
1597    }
1598
1599    #[test]
1600    fn test_try_merge() {
1601        // Create a schema with two columns
1602        let schema = Arc::new(Schema::new(vec![
1603            Field::new("col1", DataType::Int32, false),
1604            Field::new("col2", DataType::Int32, false),
1605        ]));
1606
1607        // Create items with statistics
1608        let stats1 = Statistics {
1609            num_rows: Precision::Exact(10),
1610            total_byte_size: Precision::Exact(100),
1611            column_statistics: vec![
1612                ColumnStatistics {
1613                    null_count: Precision::Exact(1),
1614                    max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
1615                    min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
1616                    sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
1617                    distinct_count: Precision::Absent,
1618                    byte_size: Precision::Exact(40),
1619                },
1620                ColumnStatistics {
1621                    null_count: Precision::Exact(2),
1622                    max_value: Precision::Exact(ScalarValue::Int32(Some(200))),
1623                    min_value: Precision::Exact(ScalarValue::Int32(Some(10))),
1624                    sum_value: Precision::Exact(ScalarValue::Int32(Some(1000))),
1625                    distinct_count: Precision::Absent,
1626                    byte_size: Precision::Exact(40),
1627                },
1628            ],
1629        };
1630
1631        let stats2 = Statistics {
1632            num_rows: Precision::Exact(15),
1633            total_byte_size: Precision::Exact(150),
1634            column_statistics: vec![
1635                ColumnStatistics {
1636                    null_count: Precision::Exact(2),
1637                    max_value: Precision::Exact(ScalarValue::Int32(Some(120))),
1638                    min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
1639                    sum_value: Precision::Exact(ScalarValue::Int32(Some(600))),
1640                    distinct_count: Precision::Absent,
1641                    byte_size: Precision::Exact(60),
1642                },
1643                ColumnStatistics {
1644                    null_count: Precision::Exact(3),
1645                    max_value: Precision::Exact(ScalarValue::Int32(Some(180))),
1646                    min_value: Precision::Exact(ScalarValue::Int32(Some(5))),
1647                    sum_value: Precision::Exact(ScalarValue::Int32(Some(1200))),
1648                    distinct_count: Precision::Absent,
1649                    byte_size: Precision::Exact(60),
1650                },
1651            ],
1652        };
1653
1654        let items = vec![stats1, stats2];
1655
1656        let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
1657
1658        // Verify the results
1659        assert_eq!(summary_stats.num_rows, Precision::Exact(25)); // 10 + 15
1660        assert_eq!(summary_stats.total_byte_size, Precision::Exact(250)); // 100 + 150
1661
1662        // Verify column statistics
1663        let col1_stats = &summary_stats.column_statistics[0];
1664        assert_eq!(col1_stats.null_count, Precision::Exact(3)); // 1 + 2
1665        assert_eq!(
1666            col1_stats.max_value,
1667            Precision::Exact(ScalarValue::Int32(Some(120)))
1668        );
1669        assert_eq!(
1670            col1_stats.min_value,
1671            Precision::Exact(ScalarValue::Int32(Some(-10)))
1672        );
1673        assert_eq!(
1674            col1_stats.sum_value,
1675            Precision::Exact(ScalarValue::Int64(Some(1100)))
1676        ); // 500 + 600
1677
1678        let col2_stats = &summary_stats.column_statistics[1];
1679        assert_eq!(col2_stats.null_count, Precision::Exact(5)); // 2 + 3
1680        assert_eq!(
1681            col2_stats.max_value,
1682            Precision::Exact(ScalarValue::Int32(Some(200)))
1683        );
1684        assert_eq!(
1685            col2_stats.min_value,
1686            Precision::Exact(ScalarValue::Int32(Some(5)))
1687        );
1688        assert_eq!(
1689            col2_stats.sum_value,
1690            Precision::Exact(ScalarValue::Int64(Some(2200)))
1691        ); // 1000 + 1200
1692    }
1693
1694    #[test]
1695    fn test_try_merge_mixed_precision() {
1696        // Create a schema with one column
1697        let schema = Arc::new(Schema::new(vec![Field::new(
1698            "col1",
1699            DataType::Int32,
1700            false,
1701        )]));
1702
1703        // Create items with different precision levels
1704        let stats1 = Statistics {
1705            num_rows: Precision::Exact(10),
1706            total_byte_size: Precision::Inexact(100),
1707            column_statistics: vec![ColumnStatistics {
1708                null_count: Precision::Exact(1),
1709                max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
1710                min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
1711                sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
1712                distinct_count: Precision::Absent,
1713                byte_size: Precision::Exact(40),
1714            }],
1715        };
1716
1717        let stats2 = Statistics {
1718            num_rows: Precision::Inexact(15),
1719            total_byte_size: Precision::Exact(150),
1720            column_statistics: vec![ColumnStatistics {
1721                null_count: Precision::Inexact(2),
1722                max_value: Precision::Inexact(ScalarValue::Int32(Some(120))),
1723                min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
1724                sum_value: Precision::Absent,
1725                distinct_count: Precision::Absent,
1726                byte_size: Precision::Inexact(60),
1727            }],
1728        };
1729
1730        let items = vec![stats1, stats2];
1731
1732        let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
1733
1734        assert_eq!(summary_stats.num_rows, Precision::Inexact(25));
1735        assert_eq!(summary_stats.total_byte_size, Precision::Inexact(250));
1736
1737        let col_stats = &summary_stats.column_statistics[0];
1738        assert_eq!(col_stats.null_count, Precision::Inexact(3));
1739        assert_eq!(
1740            col_stats.max_value,
1741            Precision::Inexact(ScalarValue::Int32(Some(120)))
1742        );
1743        assert_eq!(
1744            col_stats.min_value,
1745            Precision::Inexact(ScalarValue::Int32(Some(-10)))
1746        );
1747        assert_eq!(col_stats.sum_value, Precision::Absent);
1748    }
1749
1750    #[test]
1751    fn test_try_merge_empty() {
1752        let schema = Arc::new(Schema::new(vec![Field::new(
1753            "col1",
1754            DataType::Int32,
1755            false,
1756        )]));
1757
1758        // Empty collection
1759        let items: Vec<Statistics> = vec![];
1760
1761        let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
1762
1763        // Verify default values for empty collection
1764        assert_eq!(summary_stats.num_rows, Precision::Absent);
1765        assert_eq!(summary_stats.total_byte_size, Precision::Absent);
1766        assert_eq!(summary_stats.column_statistics.len(), 1);
1767        assert_eq!(
1768            summary_stats.column_statistics[0].null_count,
1769            Precision::Absent
1770        );
1771    }
1772
1773    #[test]
1774    fn test_try_merge_mismatched_size() {
1775        // Create a schema with one column
1776        let schema = Arc::new(Schema::new(vec![Field::new(
1777            "col1",
1778            DataType::Int32,
1779            false,
1780        )]));
1781
1782        // No column statistics
1783        let stats1 = Statistics::default();
1784
1785        let stats2 =
1786            Statistics::default().add_column_statistics(ColumnStatistics::new_unknown());
1787
1788        let items = vec![stats1, stats2];
1789
1790        let e = Statistics::try_merge_iter(&items, &schema).unwrap_err();
1791        assert_contains!(
1792            e.to_string(),
1793            "Error during planning: Cannot merge statistics with different number of columns: 0 vs 1"
1794        );
1795    }
1796
1797    #[test]
1798    fn test_try_merge_distinct_count_absent() {
1799        // Create statistics with known distinct counts
1800        let stats1 = Statistics::default()
1801            .with_num_rows(Precision::Exact(10))
1802            .with_total_byte_size(Precision::Exact(100))
1803            .add_column_statistics(
1804                ColumnStatistics::new_unknown()
1805                    .with_null_count(Precision::Exact(0))
1806                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(1))))
1807                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(10))))
1808                    .with_distinct_count(Precision::Exact(5)),
1809            );
1810
1811        let stats2 = Statistics::default()
1812            .with_num_rows(Precision::Exact(15))
1813            .with_total_byte_size(Precision::Exact(150))
1814            .add_column_statistics(
1815                ColumnStatistics::new_unknown()
1816                    .with_null_count(Precision::Exact(0))
1817                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(5))))
1818                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(20))))
1819                    .with_distinct_count(Precision::Exact(7)),
1820            );
1821
1822        // Merge statistics
1823        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
1824        let merged_stats =
1825            Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
1826
1827        // Verify the results
1828        assert_eq!(merged_stats.num_rows, Precision::Exact(25));
1829        assert_eq!(merged_stats.total_byte_size, Precision::Exact(250));
1830
1831        let col_stats = &merged_stats.column_statistics[0];
1832        assert_eq!(col_stats.null_count, Precision::Exact(0));
1833        assert_eq!(
1834            col_stats.min_value,
1835            Precision::Exact(ScalarValue::Int32(Some(1)))
1836        );
1837        assert_eq!(
1838            col_stats.max_value,
1839            Precision::Exact(ScalarValue::Int32(Some(20)))
1840        );
1841        // Overlap-based NDV: ranges [1,10] and [5,20], overlap [5,10]
1842        // range_left=9, range_right=15, overlap=5
1843        // overlap_left=5*(5/9)=2.78, overlap_right=7*(5/15)=2.33
1844        // result = max(2.78, 2.33) + (5-2.78) + (7-2.33) = 9.67 -> 10
1845        assert_eq!(col_stats.distinct_count, Precision::Inexact(10));
1846    }
1847
1848    #[test]
1849    fn test_try_merge_ndv_disjoint_ranges() {
1850        let stats1 = Statistics::default()
1851            .with_num_rows(Precision::Exact(10))
1852            .add_column_statistics(
1853                ColumnStatistics::new_unknown()
1854                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(0))))
1855                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(10))))
1856                    .with_distinct_count(Precision::Exact(5)),
1857            );
1858        let stats2 = Statistics::default()
1859            .with_num_rows(Precision::Exact(10))
1860            .add_column_statistics(
1861                ColumnStatistics::new_unknown()
1862                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(20))))
1863                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(30))))
1864                    .with_distinct_count(Precision::Exact(8)),
1865            );
1866
1867        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
1868        let merged = Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
1869        // No overlap -> sum of NDVs
1870        assert_eq!(
1871            merged.column_statistics[0].distinct_count,
1872            Precision::Inexact(13)
1873        );
1874    }
1875
1876    #[test]
1877    fn test_try_merge_ndv_identical_ranges() {
1878        let stats1 = Statistics::default()
1879            .with_num_rows(Precision::Exact(100))
1880            .add_column_statistics(
1881                ColumnStatistics::new_unknown()
1882                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(0))))
1883                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(100))))
1884                    .with_distinct_count(Precision::Exact(50)),
1885            );
1886        let stats2 = Statistics::default()
1887            .with_num_rows(Precision::Exact(100))
1888            .add_column_statistics(
1889                ColumnStatistics::new_unknown()
1890                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(0))))
1891                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(100))))
1892                    .with_distinct_count(Precision::Exact(30)),
1893            );
1894
1895        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
1896        let merged = Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
1897        // Full overlap -> max(50, 30) = 50
1898        assert_eq!(
1899            merged.column_statistics[0].distinct_count,
1900            Precision::Inexact(50)
1901        );
1902    }
1903
1904    #[test]
1905    fn test_try_merge_ndv_partial_overlap() {
1906        let stats1 = Statistics::default()
1907            .with_num_rows(Precision::Exact(100))
1908            .add_column_statistics(
1909                ColumnStatistics::new_unknown()
1910                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(0))))
1911                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(100))))
1912                    .with_distinct_count(Precision::Exact(80)),
1913            );
1914        let stats2 = Statistics::default()
1915            .with_num_rows(Precision::Exact(100))
1916            .add_column_statistics(
1917                ColumnStatistics::new_unknown()
1918                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(50))))
1919                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(150))))
1920                    .with_distinct_count(Precision::Exact(60)),
1921            );
1922
1923        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
1924        let merged = Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
1925        // overlap=[50,100], range_left=100, range_right=100, overlap_range=50
1926        // overlap_left=80*(50/100)=40, overlap_right=60*(50/100)=30
1927        // result = max(40,30) + (80-40) + (60-30) = 40 + 40 + 30 = 110
1928        assert_eq!(
1929            merged.column_statistics[0].distinct_count,
1930            Precision::Inexact(110)
1931        );
1932    }
1933
1934    #[test]
1935    fn test_try_merge_ndv_missing_min_max() {
1936        let stats1 = Statistics::default()
1937            .with_num_rows(Precision::Exact(10))
1938            .add_column_statistics(
1939                ColumnStatistics::new_unknown().with_distinct_count(Precision::Exact(5)),
1940            );
1941        let stats2 = Statistics::default()
1942            .with_num_rows(Precision::Exact(10))
1943            .add_column_statistics(
1944                ColumnStatistics::new_unknown().with_distinct_count(Precision::Exact(8)),
1945            );
1946
1947        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
1948        let merged = Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
1949        // No min/max -> default fallback is max
1950        assert_eq!(
1951            merged.column_statistics[0].distinct_count,
1952            Precision::Inexact(8)
1953        );
1954    }
1955
1956    #[test]
1957    fn test_try_merge_ndv_non_numeric_types() {
1958        let stats1 = Statistics::default()
1959            .with_num_rows(Precision::Exact(10))
1960            .add_column_statistics(
1961                ColumnStatistics::new_unknown()
1962                    .with_min_value(Precision::Exact(ScalarValue::Utf8(Some(
1963                        "aaa".to_string(),
1964                    ))))
1965                    .with_max_value(Precision::Exact(ScalarValue::Utf8(Some(
1966                        "zzz".to_string(),
1967                    ))))
1968                    .with_distinct_count(Precision::Exact(5)),
1969            );
1970        let stats2 = Statistics::default()
1971            .with_num_rows(Precision::Exact(10))
1972            .add_column_statistics(
1973                ColumnStatistics::new_unknown()
1974                    .with_min_value(Precision::Exact(ScalarValue::Utf8(Some(
1975                        "bbb".to_string(),
1976                    ))))
1977                    .with_max_value(Precision::Exact(ScalarValue::Utf8(Some(
1978                        "yyy".to_string(),
1979                    ))))
1980                    .with_distinct_count(Precision::Exact(8)),
1981            );
1982
1983        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, true)]);
1984        let merged = Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
1985        // distance() unsupported for strings -> default fallback is max
1986        assert_eq!(
1987            merged.column_statistics[0].distinct_count,
1988            Precision::Inexact(8)
1989        );
1990    }
1991
1992    #[test]
1993    fn test_try_merge_ndv_non_numeric_types_sum_fallback() {
1994        let stats1 = Statistics::default()
1995            .with_num_rows(Precision::Exact(10))
1996            .add_column_statistics(
1997                ColumnStatistics::new_unknown()
1998                    .with_min_value(Precision::Exact(ScalarValue::Utf8(Some(
1999                        "aaa".to_string(),
2000                    ))))
2001                    .with_max_value(Precision::Exact(ScalarValue::Utf8(Some(
2002                        "zzz".to_string(),
2003                    ))))
2004                    .with_distinct_count(Precision::Exact(5)),
2005            );
2006        let stats2 = Statistics::default()
2007            .with_num_rows(Precision::Exact(10))
2008            .add_column_statistics(
2009                ColumnStatistics::new_unknown()
2010                    .with_min_value(Precision::Exact(ScalarValue::Utf8(Some(
2011                        "bbb".to_string(),
2012                    ))))
2013                    .with_max_value(Precision::Exact(ScalarValue::Utf8(Some(
2014                        "yyy".to_string(),
2015                    ))))
2016                    .with_distinct_count(Precision::Exact(8)),
2017            );
2018
2019        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, true)]);
2020        let merged = Statistics::try_merge_iter_with_ndv_fallback(
2021            [&stats1, &stats2],
2022            &schema,
2023            NdvFallback::Sum,
2024        )
2025        .unwrap();
2026
2027        // distance() unsupported for strings -> sum fallback is caller-selected
2028        assert_eq!(
2029            merged.column_statistics[0].distinct_count,
2030            Precision::Inexact(13)
2031        );
2032    }
2033
2034    #[test]
2035    fn test_try_merge_ndv_constant_columns() {
2036        // Same constant: [5,5]+[5,5] -> max
2037        let stats1 = Statistics::default()
2038            .with_num_rows(Precision::Exact(10))
2039            .add_column_statistics(
2040                ColumnStatistics::new_unknown()
2041                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(5))))
2042                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(5))))
2043                    .with_distinct_count(Precision::Exact(1)),
2044            );
2045        let stats2 = Statistics::default()
2046            .with_num_rows(Precision::Exact(10))
2047            .add_column_statistics(
2048                ColumnStatistics::new_unknown()
2049                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(5))))
2050                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(5))))
2051                    .with_distinct_count(Precision::Exact(1)),
2052            );
2053
2054        let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]);
2055        let merged = Statistics::try_merge_iter([&stats1, &stats2], &schema).unwrap();
2056        assert_eq!(
2057            merged.column_statistics[0].distinct_count,
2058            Precision::Inexact(1)
2059        );
2060
2061        // Different constants: [5,5]+[10,10] -> sum
2062        let stats3 = Statistics::default()
2063            .with_num_rows(Precision::Exact(10))
2064            .add_column_statistics(
2065                ColumnStatistics::new_unknown()
2066                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(5))))
2067                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(5))))
2068                    .with_distinct_count(Precision::Exact(1)),
2069            );
2070        let stats4 = Statistics::default()
2071            .with_num_rows(Precision::Exact(10))
2072            .add_column_statistics(
2073                ColumnStatistics::new_unknown()
2074                    .with_min_value(Precision::Exact(ScalarValue::Int32(Some(10))))
2075                    .with_max_value(Precision::Exact(ScalarValue::Int32(Some(10))))
2076                    .with_distinct_count(Precision::Exact(1)),
2077            );
2078
2079        let merged = Statistics::try_merge_iter([&stats3, &stats4], &schema).unwrap();
2080        assert_eq!(
2081            merged.column_statistics[0].distinct_count,
2082            Precision::Inexact(2)
2083        );
2084    }
2085
2086    #[test]
2087    fn test_try_merge_ndv_original_union_edge_cases() {
2088        struct NdvTestCase {
2089            name: &'static str,
2090            left_ndv: Precision<usize>,
2091            left_min: Option<i64>,
2092            left_max: Option<i64>,
2093            right_ndv: Precision<usize>,
2094            right_min: Option<i64>,
2095            right_max: Option<i64>,
2096            expected: Precision<usize>,
2097        }
2098
2099        let cases = vec![
2100            NdvTestCase {
2101                name: "disjoint ranges",
2102                left_ndv: Precision::Exact(5),
2103                left_min: Some(0),
2104                left_max: Some(10),
2105                right_ndv: Precision::Exact(3),
2106                right_min: Some(20),
2107                right_max: Some(30),
2108                expected: Precision::Inexact(8),
2109            },
2110            NdvTestCase {
2111                name: "identical ranges",
2112                left_ndv: Precision::Exact(10),
2113                left_min: Some(0),
2114                left_max: Some(100),
2115                right_ndv: Precision::Exact(8),
2116                right_min: Some(0),
2117                right_max: Some(100),
2118                expected: Precision::Inexact(10),
2119            },
2120            NdvTestCase {
2121                name: "partial overlap",
2122                left_ndv: Precision::Exact(100),
2123                left_min: Some(0),
2124                left_max: Some(100),
2125                right_ndv: Precision::Exact(50),
2126                right_min: Some(50),
2127                right_max: Some(150),
2128                expected: Precision::Inexact(125),
2129            },
2130            NdvTestCase {
2131                name: "right contained in left",
2132                left_ndv: Precision::Exact(100),
2133                left_min: Some(0),
2134                left_max: Some(100),
2135                right_ndv: Precision::Exact(50),
2136                right_min: Some(25),
2137                right_max: Some(75),
2138                expected: Precision::Inexact(100),
2139            },
2140            NdvTestCase {
2141                name: "same constant value",
2142                left_ndv: Precision::Exact(1),
2143                left_min: Some(5),
2144                left_max: Some(5),
2145                right_ndv: Precision::Exact(1),
2146                right_min: Some(5),
2147                right_max: Some(5),
2148                expected: Precision::Inexact(1),
2149            },
2150            NdvTestCase {
2151                name: "different constant values",
2152                left_ndv: Precision::Exact(1),
2153                left_min: Some(5),
2154                left_max: Some(5),
2155                right_ndv: Precision::Exact(1),
2156                right_min: Some(10),
2157                right_max: Some(10),
2158                expected: Precision::Inexact(2),
2159            },
2160            NdvTestCase {
2161                name: "left constant within right range",
2162                left_ndv: Precision::Exact(1),
2163                left_min: Some(5),
2164                left_max: Some(5),
2165                right_ndv: Precision::Exact(10),
2166                right_min: Some(0),
2167                right_max: Some(10),
2168                expected: Precision::Inexact(10),
2169            },
2170            NdvTestCase {
2171                name: "left constant outside right range",
2172                left_ndv: Precision::Exact(1),
2173                left_min: Some(20),
2174                left_max: Some(20),
2175                right_ndv: Precision::Exact(10),
2176                right_min: Some(0),
2177                right_max: Some(10),
2178                expected: Precision::Inexact(11),
2179            },
2180            NdvTestCase {
2181                name: "right constant within left range",
2182                left_ndv: Precision::Exact(10),
2183                left_min: Some(0),
2184                left_max: Some(10),
2185                right_ndv: Precision::Exact(1),
2186                right_min: Some(5),
2187                right_max: Some(5),
2188                expected: Precision::Inexact(10),
2189            },
2190            NdvTestCase {
2191                name: "right constant outside left range",
2192                left_ndv: Precision::Exact(10),
2193                left_min: Some(0),
2194                left_max: Some(10),
2195                right_ndv: Precision::Exact(1),
2196                right_min: Some(20),
2197                right_max: Some(20),
2198                expected: Precision::Inexact(11),
2199            },
2200            NdvTestCase {
2201                name: "missing bounds exact plus exact",
2202                left_ndv: Precision::Exact(10),
2203                left_min: None,
2204                left_max: None,
2205                right_ndv: Precision::Exact(5),
2206                right_min: None,
2207                right_max: None,
2208                expected: Precision::Inexact(15),
2209            },
2210            NdvTestCase {
2211                name: "missing bounds exact plus inexact",
2212                left_ndv: Precision::Exact(10),
2213                left_min: None,
2214                left_max: None,
2215                right_ndv: Precision::Inexact(5),
2216                right_min: None,
2217                right_max: None,
2218                expected: Precision::Inexact(15),
2219            },
2220            NdvTestCase {
2221                name: "missing bounds inexact plus inexact",
2222                left_ndv: Precision::Inexact(7),
2223                left_min: None,
2224                left_max: None,
2225                right_ndv: Precision::Inexact(3),
2226                right_min: None,
2227                right_max: None,
2228                expected: Precision::Inexact(10),
2229            },
2230            NdvTestCase {
2231                name: "exact plus absent",
2232                left_ndv: Precision::Exact(10),
2233                left_min: None,
2234                left_max: None,
2235                right_ndv: Precision::Absent,
2236                right_min: None,
2237                right_max: None,
2238                expected: Precision::Absent,
2239            },
2240            NdvTestCase {
2241                name: "inexact plus absent",
2242                left_ndv: Precision::Inexact(4),
2243                left_min: None,
2244                left_max: None,
2245                right_ndv: Precision::Absent,
2246                right_min: None,
2247                right_max: None,
2248                expected: Precision::Absent,
2249            },
2250        ];
2251
2252        for case in cases {
2253            let actual = merge_single_i64_ndv_distinct_count(
2254                make_single_i64_ndv_stats(case.left_ndv, case.left_min, case.left_max),
2255                make_single_i64_ndv_stats(case.right_ndv, case.right_min, case.right_max),
2256                NdvFallback::Sum,
2257            );
2258
2259            assert_eq!(actual, case.expected, "case {} failed", case.name);
2260        }
2261    }
2262
2263    #[test]
2264    fn test_with_fetch_basic_preservation() {
2265        // Test that column statistics and byte size are preserved (as inexact) when applying fetch
2266        let original_stats = Statistics {
2267            num_rows: Precision::Exact(1000),
2268            total_byte_size: Precision::Exact(8000),
2269            column_statistics: vec![
2270                ColumnStatistics {
2271                    null_count: Precision::Exact(10),
2272                    max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
2273                    min_value: Precision::Exact(ScalarValue::Int32(Some(0))),
2274                    sum_value: Precision::Exact(ScalarValue::Int32(Some(5050))),
2275                    distinct_count: Precision::Exact(50),
2276                    byte_size: Precision::Exact(4000),
2277                },
2278                ColumnStatistics {
2279                    null_count: Precision::Exact(20),
2280                    max_value: Precision::Exact(ScalarValue::Int64(Some(200))),
2281                    min_value: Precision::Exact(ScalarValue::Int64(Some(10))),
2282                    sum_value: Precision::Exact(ScalarValue::Int64(Some(10100))),
2283                    distinct_count: Precision::Exact(75),
2284                    byte_size: Precision::Exact(8000),
2285                },
2286            ],
2287        };
2288
2289        // Apply fetch of 100 rows (10% of original)
2290        let result = original_stats.clone().with_fetch(Some(100), 0, 1).unwrap();
2291
2292        // Check num_rows
2293        assert_eq!(result.num_rows, Precision::Exact(100));
2294
2295        // Check total_byte_size is computed as sum of scaled column byte_size values
2296        // Column 1: 4000 * 0.1 = 400, Column 2: 8000 * 0.1 = 800, Sum = 1200
2297        assert_eq!(result.total_byte_size, Precision::Inexact(1200));
2298
2299        // Check column statistics are preserved but marked as inexact
2300        assert_eq!(result.column_statistics.len(), 2);
2301
2302        // First column
2303        assert_eq!(
2304            result.column_statistics[0].null_count,
2305            Precision::Inexact(10)
2306        );
2307        assert_eq!(
2308            result.column_statistics[0].max_value,
2309            Precision::Inexact(ScalarValue::Int32(Some(100)))
2310        );
2311        assert_eq!(
2312            result.column_statistics[0].min_value,
2313            Precision::Inexact(ScalarValue::Int32(Some(0)))
2314        );
2315        assert_eq!(
2316            result.column_statistics[0].sum_value,
2317            Precision::Inexact(ScalarValue::Int32(Some(5050)))
2318        );
2319        assert_eq!(
2320            result.column_statistics[0].distinct_count,
2321            Precision::Inexact(50)
2322        );
2323
2324        // Second column
2325        assert_eq!(
2326            result.column_statistics[1].null_count,
2327            Precision::Inexact(20)
2328        );
2329        assert_eq!(
2330            result.column_statistics[1].max_value,
2331            Precision::Inexact(ScalarValue::Int64(Some(200)))
2332        );
2333        assert_eq!(
2334            result.column_statistics[1].min_value,
2335            Precision::Inexact(ScalarValue::Int64(Some(10)))
2336        );
2337        assert_eq!(
2338            result.column_statistics[1].sum_value,
2339            Precision::Inexact(ScalarValue::Int64(Some(10100)))
2340        );
2341        assert_eq!(
2342            result.column_statistics[1].distinct_count,
2343            Precision::Inexact(75)
2344        );
2345    }
2346
2347    #[test]
2348    fn test_with_fetch_inexact_input() {
2349        // Test that inexact input statistics remain inexact
2350        let original_stats = Statistics {
2351            num_rows: Precision::Inexact(1000),
2352            total_byte_size: Precision::Inexact(8000),
2353            column_statistics: vec![ColumnStatistics {
2354                null_count: Precision::Inexact(10),
2355                max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
2356                min_value: Precision::Inexact(ScalarValue::Int32(Some(0))),
2357                sum_value: Precision::Inexact(ScalarValue::Int32(Some(5050))),
2358                distinct_count: Precision::Inexact(50),
2359                byte_size: Precision::Inexact(4000),
2360            }],
2361        };
2362
2363        let result = original_stats.clone().with_fetch(Some(500), 0, 1).unwrap();
2364
2365        // Check num_rows is inexact
2366        assert_eq!(result.num_rows, Precision::Inexact(500));
2367
2368        // Check total_byte_size is computed as sum of scaled column byte_size values
2369        // Column 1: 4000 * 0.5 = 2000, Sum = 2000
2370        assert_eq!(result.total_byte_size, Precision::Inexact(2000));
2371
2372        // Column stats remain inexact
2373        assert_eq!(
2374            result.column_statistics[0].null_count,
2375            Precision::Inexact(10)
2376        );
2377    }
2378
2379    #[test]
2380    fn test_with_fetch_skip_all_rows() {
2381        // Test when skip >= num_rows (all rows are skipped)
2382        let original_stats = Statistics {
2383            num_rows: Precision::Exact(100),
2384            total_byte_size: Precision::Exact(800),
2385            column_statistics: vec![col_stats_i64(10)],
2386        };
2387
2388        let result = original_stats.clone().with_fetch(Some(50), 100, 1).unwrap();
2389
2390        assert_eq!(result.num_rows, Precision::Exact(0));
2391        // When ratio is 0/100 = 0, byte size should be 0
2392        assert_eq!(result.total_byte_size, Precision::Inexact(0));
2393    }
2394
2395    #[test]
2396    fn test_with_fetch_skip_all_rows_inexact() {
2397        // When the input num_rows is Inexact (an upper-bound estimate), an
2398        // `nr <= skip` outcome must remain Inexact: the estimate could be
2399        // wrong, so we cannot promote 0 to Exact.
2400        let original_stats = Statistics {
2401            num_rows: Precision::Inexact(0),
2402            total_byte_size: Precision::Inexact(0),
2403            column_statistics: vec![col_stats_i64(10)],
2404        };
2405
2406        let result = original_stats.clone().with_fetch(None, 0, 1).unwrap();
2407
2408        assert_eq!(result.num_rows, Precision::Inexact(0));
2409    }
2410
2411    #[test]
2412    fn test_with_fetch_no_limit() {
2413        // Test when fetch is None and skip is 0 (no limit applied)
2414        let original_stats = Statistics {
2415            num_rows: Precision::Exact(100),
2416            total_byte_size: Precision::Exact(800),
2417            column_statistics: vec![col_stats_i64(10)],
2418        };
2419
2420        let result = original_stats.clone().with_fetch(None, 0, 1).unwrap();
2421
2422        // Stats should be unchanged when no fetch and no skip
2423        assert_eq!(result.num_rows, Precision::Exact(100));
2424        assert_eq!(result.total_byte_size, Precision::Exact(800));
2425    }
2426
2427    #[test]
2428    fn test_with_fetch_no_limit_preserves_absent_num_rows() {
2429        let original_stats = Statistics {
2430            num_rows: Precision::Absent,
2431            total_byte_size: Precision::Exact(800),
2432            column_statistics: vec![col_stats_i64(10)],
2433        };
2434
2435        let result = original_stats.clone().with_fetch(None, 0, 1).unwrap();
2436
2437        assert_eq!(result, original_stats);
2438    }
2439
2440    #[test]
2441    fn test_with_fetch_absent_num_rows_does_not_zero_byte_size() {
2442        let original_stats = Statistics {
2443            num_rows: Precision::Absent,
2444            total_byte_size: Precision::Exact(800),
2445            column_statistics: vec![col_stats_i64(10)],
2446        };
2447
2448        let result = original_stats.with_fetch(Some(1), 0, 1).unwrap();
2449
2450        assert_eq!(result.num_rows, Precision::Inexact(1));
2451        assert_eq!(result.total_byte_size, Precision::Absent);
2452        assert_eq!(result.column_statistics[0].byte_size, Precision::Absent);
2453        assert_eq!(
2454            result.column_statistics[0].distinct_count,
2455            Precision::Inexact(1)
2456        );
2457    }
2458
2459    #[test]
2460    fn test_with_fetch_with_skip() {
2461        // Test with both skip and fetch
2462        let original_stats = Statistics {
2463            num_rows: Precision::Exact(1000),
2464            total_byte_size: Precision::Exact(8000),
2465            column_statistics: vec![col_stats_i64(10)],
2466        };
2467
2468        // Skip 200, fetch 300, so we get rows 200-500
2469        let result = original_stats
2470            .clone()
2471            .with_fetch(Some(300), 200, 1)
2472            .unwrap();
2473
2474        assert_eq!(result.num_rows, Precision::Exact(300));
2475        // Column 1: byte_size 800 * (300/500) = 240, Sum = 240
2476        assert_eq!(result.total_byte_size, Precision::Inexact(240));
2477    }
2478
2479    #[test]
2480    fn test_with_fetch_multi_partition() {
2481        // Test with multiple partitions
2482        let original_stats = Statistics {
2483            num_rows: Precision::Exact(1000), // per partition
2484            total_byte_size: Precision::Exact(8000),
2485            column_statistics: vec![col_stats_i64(10)],
2486        };
2487
2488        // Fetch 100 per partition, 4 partitions = 400 total
2489        let result = original_stats.clone().with_fetch(Some(100), 0, 4).unwrap();
2490
2491        assert_eq!(result.num_rows, Precision::Exact(400));
2492        // Column 1: byte_size 800 * 0.4 = 320, Sum = 320
2493        assert_eq!(result.total_byte_size, Precision::Inexact(320));
2494    }
2495
2496    #[test]
2497    fn test_with_fetch_absent_stats() {
2498        // Test with absent statistics
2499        let original_stats = Statistics {
2500            num_rows: Precision::Absent,
2501            total_byte_size: Precision::Absent,
2502            column_statistics: vec![ColumnStatistics {
2503                null_count: Precision::Absent,
2504                max_value: Precision::Absent,
2505                min_value: Precision::Absent,
2506                sum_value: Precision::Absent,
2507                distinct_count: Precision::Absent,
2508                byte_size: Precision::Absent,
2509            }],
2510        };
2511
2512        let result = original_stats.clone().with_fetch(Some(100), 0, 1).unwrap();
2513
2514        // With absent input stats, output should be inexact estimate
2515        assert_eq!(result.num_rows, Precision::Inexact(100));
2516        assert_eq!(result.total_byte_size, Precision::Absent);
2517        // Column stats should remain absent
2518        assert_eq!(result.column_statistics[0].null_count, Precision::Absent);
2519    }
2520
2521    #[test]
2522    fn test_with_fetch_fetch_exceeds_rows() {
2523        // Test when fetch is larger than available rows after skip
2524        let original_stats = Statistics {
2525            num_rows: Precision::Exact(100),
2526            total_byte_size: Precision::Exact(800),
2527            column_statistics: vec![col_stats_i64(10)],
2528        };
2529
2530        // Skip 50, fetch 100, but only 50 rows remain
2531        let result = original_stats.clone().with_fetch(Some(100), 50, 1).unwrap();
2532
2533        assert_eq!(result.num_rows, Precision::Exact(50));
2534        // 50/100 = 0.5, so 800 * 0.5 = 400
2535        assert_eq!(result.total_byte_size, Precision::Inexact(400));
2536    }
2537
2538    #[test]
2539    fn test_with_fetch_preserves_all_column_stats() {
2540        // Comprehensive test that all column statistic fields are preserved
2541        let original_col_stats = ColumnStatistics {
2542            null_count: Precision::Exact(42),
2543            max_value: Precision::Exact(ScalarValue::Int32(Some(999))),
2544            min_value: Precision::Exact(ScalarValue::Int32(Some(-100))),
2545            sum_value: Precision::Exact(ScalarValue::Int32(Some(123456))),
2546            distinct_count: Precision::Exact(789),
2547            byte_size: Precision::Exact(4000),
2548        };
2549
2550        let original_stats = Statistics {
2551            num_rows: Precision::Exact(1000),
2552            total_byte_size: Precision::Exact(8000),
2553            column_statistics: vec![original_col_stats.clone()],
2554        };
2555
2556        let result = original_stats.with_fetch(Some(250), 0, 1).unwrap();
2557
2558        let result_col_stats = &result.column_statistics[0];
2559
2560        // All values should be preserved but marked as inexact
2561        assert_eq!(result_col_stats.null_count, Precision::Inexact(42));
2562        assert_eq!(
2563            result_col_stats.max_value,
2564            Precision::Inexact(ScalarValue::Int32(Some(999)))
2565        );
2566        assert_eq!(
2567            result_col_stats.min_value,
2568            Precision::Inexact(ScalarValue::Int32(Some(-100)))
2569        );
2570        assert_eq!(
2571            result_col_stats.sum_value,
2572            Precision::Inexact(ScalarValue::Int32(Some(123456)))
2573        );
2574        // NDV is capped at the new row count (250) since 789 > 250
2575        assert_eq!(result_col_stats.distinct_count, Precision::Inexact(250));
2576    }
2577
2578    #[test]
2579    fn test_byte_size_to_inexact() {
2580        let col_stats = ColumnStatistics {
2581            null_count: Precision::Exact(10),
2582            max_value: Precision::Absent,
2583            min_value: Precision::Absent,
2584            sum_value: Precision::Absent,
2585            distinct_count: Precision::Absent,
2586            byte_size: Precision::Exact(5000),
2587        };
2588
2589        let inexact = col_stats.to_inexact();
2590        assert_eq!(inexact.byte_size, Precision::Inexact(5000));
2591    }
2592
2593    #[test]
2594    fn test_with_byte_size_builder() {
2595        let col_stats =
2596            ColumnStatistics::new_unknown().with_byte_size(Precision::Exact(8192));
2597        assert_eq!(col_stats.byte_size, Precision::Exact(8192));
2598    }
2599
2600    #[test]
2601    fn test_with_sum_value_builder_widens_small_integers() {
2602        let col_stats = ColumnStatistics::new_unknown()
2603            .with_sum_value(Precision::Exact(ScalarValue::UInt32(Some(123))));
2604        assert_eq!(
2605            col_stats.sum_value,
2606            Precision::Exact(ScalarValue::UInt64(Some(123)))
2607        );
2608    }
2609
2610    #[test]
2611    fn test_with_fetch_scales_byte_size() {
2612        // Test that byte_size is scaled by the row ratio in with_fetch
2613        let original_stats = Statistics {
2614            num_rows: Precision::Exact(1000),
2615            total_byte_size: Precision::Exact(8000),
2616            column_statistics: vec![
2617                ColumnStatistics {
2618                    null_count: Precision::Exact(10),
2619                    max_value: Precision::Absent,
2620                    min_value: Precision::Absent,
2621                    sum_value: Precision::Absent,
2622                    distinct_count: Precision::Absent,
2623                    byte_size: Precision::Exact(4000),
2624                },
2625                ColumnStatistics {
2626                    null_count: Precision::Exact(20),
2627                    max_value: Precision::Absent,
2628                    min_value: Precision::Absent,
2629                    sum_value: Precision::Absent,
2630                    distinct_count: Precision::Absent,
2631                    byte_size: Precision::Exact(8000),
2632                },
2633            ],
2634        };
2635
2636        // Apply fetch of 100 rows (10% of original)
2637        let result = original_stats.with_fetch(Some(100), 0, 1).unwrap();
2638
2639        // byte_size should be scaled: 4000 * 0.1 = 400, 8000 * 0.1 = 800
2640        assert_eq!(
2641            result.column_statistics[0].byte_size,
2642            Precision::Inexact(400)
2643        );
2644        assert_eq!(
2645            result.column_statistics[1].byte_size,
2646            Precision::Inexact(800)
2647        );
2648
2649        // total_byte_size should be computed as sum of byte_size values: 400 + 800 = 1200
2650        assert_eq!(result.total_byte_size, Precision::Inexact(1200));
2651    }
2652
2653    #[test]
2654    fn test_with_fetch_total_byte_size_fallback() {
2655        // Test that total_byte_size falls back to scaling when not all columns have byte_size
2656        let original_stats = Statistics {
2657            num_rows: Precision::Exact(1000),
2658            total_byte_size: Precision::Exact(8000),
2659            column_statistics: vec![
2660                ColumnStatistics {
2661                    null_count: Precision::Exact(10),
2662                    max_value: Precision::Absent,
2663                    min_value: Precision::Absent,
2664                    sum_value: Precision::Absent,
2665                    distinct_count: Precision::Absent,
2666                    byte_size: Precision::Exact(4000),
2667                },
2668                ColumnStatistics {
2669                    null_count: Precision::Exact(20),
2670                    max_value: Precision::Absent,
2671                    min_value: Precision::Absent,
2672                    sum_value: Precision::Absent,
2673                    distinct_count: Precision::Absent,
2674                    byte_size: Precision::Absent, // One column has no byte_size
2675                },
2676            ],
2677        };
2678
2679        // Apply fetch of 100 rows (10% of original)
2680        let result = original_stats.with_fetch(Some(100), 0, 1).unwrap();
2681
2682        // total_byte_size should fall back to scaling: 8000 * 0.1 = 800
2683        assert_eq!(result.total_byte_size, Precision::Inexact(800));
2684    }
2685
2686    #[test]
2687    fn test_with_fetch_caps_ndv_at_row_count() {
2688        // NDV=500 but after LIMIT 10, NDV should be capped at 10
2689        let stats = Statistics {
2690            num_rows: Precision::Exact(1000),
2691            total_byte_size: Precision::Exact(8000),
2692            column_statistics: vec![ColumnStatistics {
2693                distinct_count: Precision::Inexact(500),
2694                ..Default::default()
2695            }],
2696        };
2697
2698        let result = stats.with_fetch(Some(10), 0, 1).unwrap();
2699        assert_eq!(result.num_rows, Precision::Exact(10));
2700        assert_eq!(
2701            result.column_statistics[0].distinct_count,
2702            Precision::Inexact(10)
2703        );
2704    }
2705
2706    #[test]
2707    fn test_with_fetch_caps_ndv_with_skip() {
2708        // 1000 rows, NDV=500, OFFSET 5 LIMIT 10
2709        // with_fetch computes num_rows = min(1000 - 5, 10) = 10
2710        // NDV should be capped at 10
2711        let stats = Statistics {
2712            num_rows: Precision::Exact(1000),
2713            total_byte_size: Precision::Exact(8000),
2714            column_statistics: vec![ColumnStatistics {
2715                distinct_count: Precision::Inexact(500),
2716                ..Default::default()
2717            }],
2718        };
2719
2720        let result = stats.with_fetch(Some(10), 5, 1).unwrap();
2721        assert_eq!(result.num_rows, Precision::Exact(10));
2722        assert_eq!(
2723            result.column_statistics[0].distinct_count,
2724            Precision::Inexact(10)
2725        );
2726    }
2727
2728    #[test]
2729    fn test_with_fetch_caps_ndv_with_large_skip() {
2730        // 1000 rows, NDV=500, OFFSET 995 LIMIT 100
2731        // with_fetch computes num_rows = min(1000 - 995, 100) = 5
2732        // NDV should be capped at 5
2733        let stats = Statistics {
2734            num_rows: Precision::Exact(1000),
2735            total_byte_size: Precision::Exact(8000),
2736            column_statistics: vec![ColumnStatistics {
2737                distinct_count: Precision::Inexact(500),
2738                ..Default::default()
2739            }],
2740        };
2741
2742        let result = stats.with_fetch(Some(100), 995, 1).unwrap();
2743        assert_eq!(result.num_rows, Precision::Exact(5));
2744        assert_eq!(
2745            result.column_statistics[0].distinct_count,
2746            Precision::Inexact(5)
2747        );
2748    }
2749
2750    #[test]
2751    fn test_with_fetch_ndv_below_row_count_unchanged() {
2752        // NDV=5 and LIMIT 10: NDV should stay at 5
2753        let stats = Statistics {
2754            num_rows: Precision::Exact(1000),
2755            total_byte_size: Precision::Exact(8000),
2756            column_statistics: vec![ColumnStatistics {
2757                distinct_count: Precision::Inexact(5),
2758                ..Default::default()
2759            }],
2760        };
2761
2762        let result = stats.with_fetch(Some(10), 0, 1).unwrap();
2763        assert_eq!(result.num_rows, Precision::Exact(10));
2764        assert_eq!(
2765            result.column_statistics[0].distinct_count,
2766            Precision::Inexact(5)
2767        );
2768    }
2769
2770    #[test]
2771    fn test_try_merge_iter_basic() {
2772        let schema = Arc::new(Schema::new(vec![
2773            Field::new("col1", DataType::Int32, false),
2774            Field::new("col2", DataType::Int32, false),
2775        ]));
2776
2777        let stats1 = Statistics {
2778            num_rows: Precision::Exact(10),
2779            total_byte_size: Precision::Exact(100),
2780            column_statistics: vec![
2781                ColumnStatistics {
2782                    null_count: Precision::Exact(1),
2783                    max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
2784                    min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
2785                    sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
2786                    distinct_count: Precision::Absent,
2787                    byte_size: Precision::Exact(40),
2788                },
2789                ColumnStatistics {
2790                    null_count: Precision::Exact(2),
2791                    max_value: Precision::Exact(ScalarValue::Int32(Some(200))),
2792                    min_value: Precision::Exact(ScalarValue::Int32(Some(10))),
2793                    sum_value: Precision::Exact(ScalarValue::Int32(Some(1000))),
2794                    distinct_count: Precision::Absent,
2795                    byte_size: Precision::Exact(40),
2796                },
2797            ],
2798        };
2799
2800        let stats2 = Statistics {
2801            num_rows: Precision::Exact(15),
2802            total_byte_size: Precision::Exact(150),
2803            column_statistics: vec![
2804                ColumnStatistics {
2805                    null_count: Precision::Exact(2),
2806                    max_value: Precision::Exact(ScalarValue::Int32(Some(120))),
2807                    min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
2808                    sum_value: Precision::Exact(ScalarValue::Int32(Some(600))),
2809                    distinct_count: Precision::Absent,
2810                    byte_size: Precision::Exact(60),
2811                },
2812                ColumnStatistics {
2813                    null_count: Precision::Exact(3),
2814                    max_value: Precision::Exact(ScalarValue::Int32(Some(180))),
2815                    min_value: Precision::Exact(ScalarValue::Int32(Some(5))),
2816                    sum_value: Precision::Exact(ScalarValue::Int32(Some(1200))),
2817                    distinct_count: Precision::Absent,
2818                    byte_size: Precision::Exact(60),
2819                },
2820            ],
2821        };
2822
2823        let items = vec![&stats1, &stats2];
2824        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
2825
2826        assert_eq!(summary_stats.num_rows, Precision::Exact(25));
2827        assert_eq!(summary_stats.total_byte_size, Precision::Exact(250));
2828
2829        let col1_stats = &summary_stats.column_statistics[0];
2830        assert_eq!(col1_stats.null_count, Precision::Exact(3));
2831        assert_eq!(
2832            col1_stats.max_value,
2833            Precision::Exact(ScalarValue::Int32(Some(120)))
2834        );
2835        assert_eq!(
2836            col1_stats.min_value,
2837            Precision::Exact(ScalarValue::Int32(Some(-10)))
2838        );
2839        assert_eq!(
2840            col1_stats.sum_value,
2841            Precision::Exact(ScalarValue::Int64(Some(1100)))
2842        );
2843
2844        let col2_stats = &summary_stats.column_statistics[1];
2845        assert_eq!(col2_stats.null_count, Precision::Exact(5));
2846        assert_eq!(
2847            col2_stats.max_value,
2848            Precision::Exact(ScalarValue::Int32(Some(200)))
2849        );
2850        assert_eq!(
2851            col2_stats.min_value,
2852            Precision::Exact(ScalarValue::Int32(Some(5)))
2853        );
2854        assert_eq!(
2855            col2_stats.sum_value,
2856            Precision::Exact(ScalarValue::Int64(Some(2200)))
2857        );
2858    }
2859
2860    #[test]
2861    fn test_try_merge_iter_mixed_precision() {
2862        let schema = Arc::new(Schema::new(vec![Field::new(
2863            "col1",
2864            DataType::Int32,
2865            false,
2866        )]));
2867
2868        let stats1 = Statistics {
2869            num_rows: Precision::Exact(10),
2870            total_byte_size: Precision::Inexact(100),
2871            column_statistics: vec![ColumnStatistics {
2872                null_count: Precision::Exact(1),
2873                max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
2874                min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
2875                sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
2876                distinct_count: Precision::Absent,
2877                byte_size: Precision::Exact(40),
2878            }],
2879        };
2880
2881        let stats2 = Statistics {
2882            num_rows: Precision::Inexact(15),
2883            total_byte_size: Precision::Exact(150),
2884            column_statistics: vec![ColumnStatistics {
2885                null_count: Precision::Inexact(2),
2886                max_value: Precision::Inexact(ScalarValue::Int32(Some(120))),
2887                min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
2888                sum_value: Precision::Absent,
2889                distinct_count: Precision::Absent,
2890                byte_size: Precision::Inexact(60),
2891            }],
2892        };
2893
2894        let items = vec![&stats1, &stats2];
2895        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
2896
2897        assert_eq!(summary_stats.num_rows, Precision::Inexact(25));
2898        assert_eq!(summary_stats.total_byte_size, Precision::Inexact(250));
2899
2900        let col_stats = &summary_stats.column_statistics[0];
2901        assert_eq!(col_stats.null_count, Precision::Inexact(3));
2902        assert_eq!(
2903            col_stats.max_value,
2904            Precision::Inexact(ScalarValue::Int32(Some(120)))
2905        );
2906        assert_eq!(
2907            col_stats.min_value,
2908            Precision::Inexact(ScalarValue::Int32(Some(-10)))
2909        );
2910        // sum_value becomes Absent because stats2 has Absent sum
2911        assert_eq!(col_stats.sum_value, Precision::Absent);
2912    }
2913
2914    #[test]
2915    fn test_try_merge_iter_empty() {
2916        let schema = Arc::new(Schema::new(vec![Field::new(
2917            "col1",
2918            DataType::Int32,
2919            false,
2920        )]));
2921
2922        let items: Vec<&Statistics> = vec![];
2923        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
2924
2925        assert_eq!(summary_stats.num_rows, Precision::Absent);
2926        assert_eq!(summary_stats.total_byte_size, Precision::Absent);
2927        assert_eq!(summary_stats.column_statistics.len(), 1);
2928        assert_eq!(
2929            summary_stats.column_statistics[0].null_count,
2930            Precision::Absent
2931        );
2932    }
2933
2934    #[test]
2935    fn test_try_merge_iter_single_item() {
2936        let schema = Arc::new(Schema::new(vec![Field::new(
2937            "col1",
2938            DataType::Int32,
2939            false,
2940        )]));
2941
2942        let stats = Statistics {
2943            num_rows: Precision::Exact(10),
2944            total_byte_size: Precision::Exact(100),
2945            column_statistics: vec![ColumnStatistics {
2946                null_count: Precision::Exact(1),
2947                max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
2948                min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
2949                sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
2950                distinct_count: Precision::Exact(10),
2951                byte_size: Precision::Exact(40),
2952            }],
2953        };
2954
2955        let items = vec![&stats];
2956        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
2957
2958        assert_eq!(summary_stats, stats);
2959    }
2960
2961    #[test]
2962    fn test_try_merge_iter_mismatched_columns() {
2963        let schema = Arc::new(Schema::new(vec![Field::new(
2964            "col1",
2965            DataType::Int32,
2966            false,
2967        )]));
2968
2969        let stats1 = Statistics::default();
2970        let stats2 =
2971            Statistics::default().add_column_statistics(ColumnStatistics::new_unknown());
2972
2973        let items = vec![&stats1, &stats2];
2974        let e = Statistics::try_merge_iter(items, &schema).unwrap_err();
2975        assert_contains!(
2976            e.to_string(),
2977            "Cannot merge statistics with different number of columns: 0 vs 1"
2978        );
2979    }
2980
2981    #[test]
2982    fn test_try_merge_iter_three_items() {
2983        // Verify that merging three items works correctly
2984        let schema = Arc::new(Schema::new(vec![Field::new(
2985            "col1",
2986            DataType::Int64,
2987            false,
2988        )]));
2989
2990        let stats1 = Statistics {
2991            num_rows: Precision::Exact(10),
2992            total_byte_size: Precision::Exact(100),
2993            column_statistics: vec![ColumnStatistics {
2994                null_count: Precision::Exact(1),
2995                max_value: Precision::Exact(ScalarValue::Int64(Some(100))),
2996                min_value: Precision::Exact(ScalarValue::Int64(Some(10))),
2997                sum_value: Precision::Exact(ScalarValue::Int64(Some(500))),
2998                distinct_count: Precision::Exact(8),
2999                byte_size: Precision::Exact(80),
3000            }],
3001        };
3002
3003        let stats2 = Statistics {
3004            num_rows: Precision::Exact(20),
3005            total_byte_size: Precision::Exact(200),
3006            column_statistics: vec![ColumnStatistics {
3007                null_count: Precision::Exact(2),
3008                max_value: Precision::Exact(ScalarValue::Int64(Some(200))),
3009                min_value: Precision::Exact(ScalarValue::Int64(Some(5))),
3010                sum_value: Precision::Exact(ScalarValue::Int64(Some(1000))),
3011                distinct_count: Precision::Exact(15),
3012                byte_size: Precision::Exact(160),
3013            }],
3014        };
3015
3016        let stats3 = Statistics {
3017            num_rows: Precision::Exact(30),
3018            total_byte_size: Precision::Exact(300),
3019            column_statistics: vec![ColumnStatistics {
3020                null_count: Precision::Exact(3),
3021                max_value: Precision::Exact(ScalarValue::Int64(Some(150))),
3022                min_value: Precision::Exact(ScalarValue::Int64(Some(1))),
3023                sum_value: Precision::Exact(ScalarValue::Int64(Some(2000))),
3024                distinct_count: Precision::Exact(25),
3025                byte_size: Precision::Exact(240),
3026            }],
3027        };
3028
3029        let items = vec![&stats1, &stats2, &stats3];
3030        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
3031
3032        assert_eq!(summary_stats.num_rows, Precision::Exact(60));
3033        assert_eq!(summary_stats.total_byte_size, Precision::Exact(600));
3034
3035        let col_stats = &summary_stats.column_statistics[0];
3036        assert_eq!(col_stats.null_count, Precision::Exact(6));
3037        assert_eq!(
3038            col_stats.max_value,
3039            Precision::Exact(ScalarValue::Int64(Some(200)))
3040        );
3041        assert_eq!(
3042            col_stats.min_value,
3043            Precision::Exact(ScalarValue::Int64(Some(1)))
3044        );
3045        assert_eq!(
3046            col_stats.sum_value,
3047            Precision::Exact(ScalarValue::Int64(Some(3500)))
3048        );
3049        assert_eq!(col_stats.byte_size, Precision::Exact(480));
3050        // Overlap-based NDV merge (pairwise left-to-right):
3051        // stats1+stats2: [10,100]+[5,200] -> NDV=16, then +stats3: [5,200]+[1,150] -> NDV=29
3052        assert_eq!(col_stats.distinct_count, Precision::Inexact(29));
3053    }
3054
3055    #[test]
3056    fn test_try_merge_iter_float_types() {
3057        let schema = Arc::new(Schema::new(vec![Field::new(
3058            "col1",
3059            DataType::Float64,
3060            false,
3061        )]));
3062
3063        let stats1 = Statistics {
3064            num_rows: Precision::Exact(10),
3065            total_byte_size: Precision::Exact(80),
3066            column_statistics: vec![ColumnStatistics {
3067                null_count: Precision::Exact(0),
3068                max_value: Precision::Exact(ScalarValue::Float64(Some(99.9))),
3069                min_value: Precision::Exact(ScalarValue::Float64(Some(1.1))),
3070                sum_value: Precision::Exact(ScalarValue::Float64(Some(500.5))),
3071                distinct_count: Precision::Absent,
3072                byte_size: Precision::Exact(80),
3073            }],
3074        };
3075
3076        let stats2 = Statistics {
3077            num_rows: Precision::Exact(10),
3078            total_byte_size: Precision::Exact(80),
3079            column_statistics: vec![ColumnStatistics {
3080                null_count: Precision::Exact(0),
3081                max_value: Precision::Exact(ScalarValue::Float64(Some(200.0))),
3082                min_value: Precision::Exact(ScalarValue::Float64(Some(0.5))),
3083                sum_value: Precision::Exact(ScalarValue::Float64(Some(1000.0))),
3084                distinct_count: Precision::Absent,
3085                byte_size: Precision::Exact(80),
3086            }],
3087        };
3088
3089        let items = vec![&stats1, &stats2];
3090        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
3091
3092        let col_stats = &summary_stats.column_statistics[0];
3093        assert_eq!(
3094            col_stats.max_value,
3095            Precision::Exact(ScalarValue::Float64(Some(200.0)))
3096        );
3097        assert_eq!(
3098            col_stats.min_value,
3099            Precision::Exact(ScalarValue::Float64(Some(0.5)))
3100        );
3101        assert_eq!(
3102            col_stats.sum_value,
3103            Precision::Exact(ScalarValue::Float64(Some(1500.5)))
3104        );
3105    }
3106
3107    #[test]
3108    fn test_try_merge_iter_string_types() {
3109        let schema =
3110            Arc::new(Schema::new(vec![Field::new("col1", DataType::Utf8, false)]));
3111
3112        let stats1 = Statistics {
3113            num_rows: Precision::Exact(10),
3114            total_byte_size: Precision::Exact(100),
3115            column_statistics: vec![ColumnStatistics {
3116                null_count: Precision::Exact(0),
3117                max_value: Precision::Exact(ScalarValue::Utf8(Some("dog".to_string()))),
3118                min_value: Precision::Exact(ScalarValue::Utf8(Some("ant".to_string()))),
3119                sum_value: Precision::Absent,
3120                distinct_count: Precision::Absent,
3121                byte_size: Precision::Exact(100),
3122            }],
3123        };
3124
3125        let stats2 = Statistics {
3126            num_rows: Precision::Exact(10),
3127            total_byte_size: Precision::Exact(100),
3128            column_statistics: vec![ColumnStatistics {
3129                null_count: Precision::Exact(0),
3130                max_value: Precision::Exact(ScalarValue::Utf8(Some("zebra".to_string()))),
3131                min_value: Precision::Exact(ScalarValue::Utf8(Some("bat".to_string()))),
3132                sum_value: Precision::Absent,
3133                distinct_count: Precision::Absent,
3134                byte_size: Precision::Exact(100),
3135            }],
3136        };
3137
3138        let items = vec![&stats1, &stats2];
3139        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
3140
3141        let col_stats = &summary_stats.column_statistics[0];
3142        assert_eq!(
3143            col_stats.max_value,
3144            Precision::Exact(ScalarValue::Utf8(Some("zebra".to_string())))
3145        );
3146        assert_eq!(
3147            col_stats.min_value,
3148            Precision::Exact(ScalarValue::Utf8(Some("ant".to_string())))
3149        );
3150        assert_eq!(col_stats.sum_value, Precision::Absent);
3151    }
3152
3153    #[test]
3154    fn test_try_merge_iter_all_inexact() {
3155        let schema = Arc::new(Schema::new(vec![Field::new(
3156            "col1",
3157            DataType::Int32,
3158            false,
3159        )]));
3160
3161        let stats1 = Statistics {
3162            num_rows: Precision::Inexact(10),
3163            total_byte_size: Precision::Inexact(100),
3164            column_statistics: vec![ColumnStatistics {
3165                null_count: Precision::Inexact(1),
3166                max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
3167                min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
3168                sum_value: Precision::Inexact(ScalarValue::Int32(Some(500))),
3169                distinct_count: Precision::Absent,
3170                byte_size: Precision::Inexact(40),
3171            }],
3172        };
3173
3174        let stats2 = Statistics {
3175            num_rows: Precision::Inexact(20),
3176            total_byte_size: Precision::Inexact(200),
3177            column_statistics: vec![ColumnStatistics {
3178                null_count: Precision::Inexact(2),
3179                max_value: Precision::Inexact(ScalarValue::Int32(Some(200))),
3180                min_value: Precision::Inexact(ScalarValue::Int32(Some(-5))),
3181                sum_value: Precision::Inexact(ScalarValue::Int32(Some(1000))),
3182                distinct_count: Precision::Absent,
3183                byte_size: Precision::Inexact(60),
3184            }],
3185        };
3186
3187        let items = vec![&stats1, &stats2];
3188        let summary_stats = Statistics::try_merge_iter(items, &schema).unwrap();
3189
3190        assert_eq!(summary_stats.num_rows, Precision::Inexact(30));
3191        assert_eq!(summary_stats.total_byte_size, Precision::Inexact(300));
3192
3193        let col_stats = &summary_stats.column_statistics[0];
3194        assert_eq!(col_stats.null_count, Precision::Inexact(3));
3195        assert_eq!(
3196            col_stats.max_value,
3197            Precision::Inexact(ScalarValue::Int32(Some(200)))
3198        );
3199        assert_eq!(
3200            col_stats.min_value,
3201            Precision::Inexact(ScalarValue::Int32(Some(-5)))
3202        );
3203        assert_eq!(
3204            col_stats.sum_value,
3205            Precision::Inexact(ScalarValue::Int64(Some(1500)))
3206        );
3207    }
3208
3209    #[test]
3210    fn test_precision_min_in_place() {
3211        // Exact vs Exact: keeps the smaller
3212        let mut lhs = Precision::Exact(10);
3213        precision_min(&mut lhs, &Precision::Exact(20));
3214        assert_eq!(lhs, Precision::Exact(10));
3215
3216        let mut lhs = Precision::Exact(20);
3217        precision_min(&mut lhs, &Precision::Exact(10));
3218        assert_eq!(lhs, Precision::Exact(10));
3219
3220        // Equal exact values
3221        let mut lhs = Precision::Exact(5);
3222        precision_min(&mut lhs, &Precision::Exact(5));
3223        assert_eq!(lhs, Precision::Exact(5));
3224
3225        // Mixed exact/inexact: result is Inexact with smaller value
3226        let mut lhs = Precision::Exact(10);
3227        precision_min(&mut lhs, &Precision::Inexact(20));
3228        assert_eq!(lhs, Precision::Inexact(10));
3229
3230        let mut lhs = Precision::Inexact(10);
3231        precision_min(&mut lhs, &Precision::Exact(5));
3232        assert_eq!(lhs, Precision::Inexact(5));
3233
3234        // Inexact vs Inexact
3235        let mut lhs = Precision::Inexact(30);
3236        precision_min(&mut lhs, &Precision::Inexact(20));
3237        assert_eq!(lhs, Precision::Inexact(20));
3238
3239        // Absent makes result Absent
3240        let mut lhs = Precision::Exact(10);
3241        precision_min(&mut lhs, &Precision::Absent);
3242        assert_eq!(lhs, Precision::Absent);
3243
3244        let mut lhs = Precision::<i32>::Absent;
3245        precision_min(&mut lhs, &Precision::Exact(10));
3246        assert_eq!(lhs, Precision::Absent);
3247    }
3248
3249    #[test]
3250    fn test_precision_max_in_place() {
3251        // Exact vs Exact: keeps the larger
3252        let mut lhs = Precision::Exact(10);
3253        precision_max(&mut lhs, &Precision::Exact(20));
3254        assert_eq!(lhs, Precision::Exact(20));
3255
3256        let mut lhs = Precision::Exact(20);
3257        precision_max(&mut lhs, &Precision::Exact(10));
3258        assert_eq!(lhs, Precision::Exact(20));
3259
3260        // Equal exact values
3261        let mut lhs = Precision::Exact(5);
3262        precision_max(&mut lhs, &Precision::Exact(5));
3263        assert_eq!(lhs, Precision::Exact(5));
3264
3265        // Mixed exact/inexact: result is Inexact with larger value
3266        let mut lhs = Precision::Exact(10);
3267        precision_max(&mut lhs, &Precision::Inexact(20));
3268        assert_eq!(lhs, Precision::Inexact(20));
3269
3270        let mut lhs = Precision::Inexact(10);
3271        precision_max(&mut lhs, &Precision::Exact(5));
3272        assert_eq!(lhs, Precision::Inexact(10));
3273
3274        // Inexact vs Inexact
3275        let mut lhs = Precision::Inexact(20);
3276        precision_max(&mut lhs, &Precision::Inexact(30));
3277        assert_eq!(lhs, Precision::Inexact(30));
3278
3279        // Absent makes result Absent
3280        let mut lhs = Precision::Exact(10);
3281        precision_max(&mut lhs, &Precision::Absent);
3282        assert_eq!(lhs, Precision::Absent);
3283
3284        let mut lhs = Precision::<i32>::Absent;
3285        precision_max(&mut lhs, &Precision::Exact(10));
3286        assert_eq!(lhs, Precision::Absent);
3287    }
3288
3289    #[test]
3290    fn test_cast_sum_value_to_sum_type_in_place_widens_int32() {
3291        let mut value = Precision::Exact(ScalarValue::Int32(Some(42)));
3292        cast_sum_value_to_sum_type_in_place(&mut value);
3293        assert_eq!(value, Precision::Exact(ScalarValue::Int64(Some(42))));
3294    }
3295
3296    #[test]
3297    fn test_cast_sum_value_to_sum_type_in_place_preserves_int64() {
3298        // Int64 is already the sum type for Int64, no widening needed
3299        let mut value = Precision::Exact(ScalarValue::Int64(Some(100)));
3300        cast_sum_value_to_sum_type_in_place(&mut value);
3301        assert_eq!(value, Precision::Exact(ScalarValue::Int64(Some(100))));
3302    }
3303
3304    #[test]
3305    fn test_cast_sum_value_to_sum_type_in_place_inexact() {
3306        let mut value = Precision::Inexact(ScalarValue::Int32(Some(42)));
3307        cast_sum_value_to_sum_type_in_place(&mut value);
3308        assert_eq!(value, Precision::Inexact(ScalarValue::Int64(Some(42))));
3309    }
3310
3311    #[test]
3312    fn test_cast_sum_value_to_sum_type_in_place_absent() {
3313        let mut value = Precision::<ScalarValue>::Absent;
3314        cast_sum_value_to_sum_type_in_place(&mut value);
3315        assert_eq!(value, Precision::Absent);
3316    }
3317
3318    #[test]
3319    fn test_precision_add_for_sum_in_place_same_type() {
3320        // Int64 + Int64: no widening needed, straight add
3321        let mut lhs = Precision::Exact(ScalarValue::Int64(Some(10)));
3322        let rhs = Precision::Exact(ScalarValue::Int64(Some(20)));
3323        precision_add_for_sum_in_place(&mut lhs, &rhs);
3324        assert_eq!(lhs, Precision::Exact(ScalarValue::Int64(Some(30))));
3325    }
3326
3327    #[test]
3328    fn test_precision_add_for_sum_in_place_widens_rhs() {
3329        // lhs is already Int64 (widened), rhs is Int32 -> gets cast to Int64
3330        let mut lhs = Precision::Exact(ScalarValue::Int64(Some(10)));
3331        let rhs = Precision::Exact(ScalarValue::Int32(Some(5)));
3332        precision_add_for_sum_in_place(&mut lhs, &rhs);
3333        assert_eq!(lhs, Precision::Exact(ScalarValue::Int64(Some(15))));
3334    }
3335
3336    #[test]
3337    fn test_precision_add_for_sum_in_place_inexact() {
3338        let mut lhs = Precision::Inexact(ScalarValue::Int64(Some(10)));
3339        let rhs = Precision::Inexact(ScalarValue::Int32(Some(5)));
3340        precision_add_for_sum_in_place(&mut lhs, &rhs);
3341        assert_eq!(lhs, Precision::Inexact(ScalarValue::Int64(Some(15))));
3342    }
3343
3344    #[test]
3345    fn test_precision_add_for_sum_in_place_absent_rhs() {
3346        let mut lhs = Precision::Exact(ScalarValue::Int64(Some(10)));
3347        precision_add_for_sum_in_place(&mut lhs, &Precision::Absent);
3348        assert_eq!(lhs, Precision::Absent);
3349    }
3350
3351    #[test]
3352    fn test_calculate_total_byte_size() {
3353        let primitive_schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
3354        let non_primitive_schema =
3355            Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
3356
3357        // All-primitive schema with a known row count computes an exact size.
3358        let mut stats = Statistics::new_unknown(&primitive_schema);
3359        stats.num_rows = Precision::Exact(10);
3360        stats.calculate_total_byte_size(&primitive_schema);
3361        assert_eq!(stats.total_byte_size, Precision::Exact(40));
3362
3363        // All-primitive schema with an unknown row count keeps a previously
3364        // known `total_byte_size`, downgraded to inexact, instead of
3365        // discarding it to `Absent`.
3366        let mut stats = Statistics::new_unknown(&primitive_schema);
3367        stats.total_byte_size = Precision::Exact(1234);
3368        stats.calculate_total_byte_size(&primitive_schema);
3369        assert_eq!(stats.total_byte_size, Precision::Inexact(1234));
3370
3371        // Non-primitive schema always downgrades any existing
3372        // `total_byte_size` to inexact, regardless of `num_rows`.
3373        let mut stats = Statistics::new_unknown(&non_primitive_schema);
3374        stats.num_rows = Precision::Exact(10);
3375        stats.total_byte_size = Precision::Exact(999);
3376        stats.calculate_total_byte_size(&non_primitive_schema);
3377        assert_eq!(stats.total_byte_size, Precision::Inexact(999));
3378    }
3379}