blob: da298c20ebcb4f268cd1e895c75ecce8f9547b2d [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! This module provides data structures to represent statistics
use std::fmt::{self, Debug, Display};
use crate::{Result, ScalarValue};
use crate::error::_plan_err;
use arrow::datatypes::{DataType, Schema};
/// Represents a value with a degree of certainty. `Precision` is used to
/// propagate information the precision of statistical values.
#[derive(Clone, PartialEq, Eq, Default, Copy)]
pub enum Precision<T: Debug + Clone + PartialEq + Eq + PartialOrd> {
/// The exact value is known
Exact(T),
/// The value is not known exactly, but is likely close to this value
Inexact(T),
/// Nothing is known about the value
#[default]
Absent,
}
impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> Precision<T> {
/// If we have some value (exact or inexact), it returns that value.
/// Otherwise, it returns `None`.
pub fn get_value(&self) -> Option<&T> {
match self {
Precision::Exact(value) | Precision::Inexact(value) => Some(value),
Precision::Absent => None,
}
}
/// Transform the value in this [`Precision`] object, if one exists, using
/// the given function. Preserves the exactness state.
pub fn map<U, F>(self, f: F) -> Precision<U>
where
F: Fn(T) -> U,
U: Debug + Clone + PartialEq + Eq + PartialOrd,
{
match self {
Precision::Exact(val) => Precision::Exact(f(val)),
Precision::Inexact(val) => Precision::Inexact(f(val)),
_ => Precision::<U>::Absent,
}
}
/// Returns `Some(true)` if we have an exact value, `Some(false)` if we
/// have an inexact value, and `None` if there is no value.
pub fn is_exact(&self) -> Option<bool> {
match self {
Precision::Exact(_) => Some(true),
Precision::Inexact(_) => Some(false),
_ => None,
}
}
/// Returns the maximum of two (possibly inexact) values, conservatively
/// propagating exactness information. If one of the input values is
/// [`Precision::Absent`], the result is `Absent` too.
pub fn max(&self, other: &Precision<T>) -> Precision<T> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => {
Precision::Exact(if a >= b { a.clone() } else { b.clone() })
}
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => {
Precision::Inexact(if a >= b { a.clone() } else { b.clone() })
}
(_, _) => Precision::Absent,
}
}
/// Returns the minimum of two (possibly inexact) values, conservatively
/// propagating exactness information. If one of the input values is
/// [`Precision::Absent`], the result is `Absent` too.
pub fn min(&self, other: &Precision<T>) -> Precision<T> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => {
Precision::Exact(if a >= b { b.clone() } else { a.clone() })
}
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => {
Precision::Inexact(if a >= b { b.clone() } else { a.clone() })
}
(_, _) => Precision::Absent,
}
}
/// Demotes the precision state from exact to inexact (if present).
pub fn to_inexact(self) -> Self {
match self {
Precision::Exact(value) => Precision::Inexact(value),
_ => self,
}
}
}
impl Precision<usize> {
/// Calculates the sum of two (possibly inexact) [`usize`] values,
/// conservatively propagating exactness information. If one of the input
/// values is [`Precision::Absent`], the result is `Absent` too.
pub fn add(&self, other: &Precision<usize>) -> Precision<usize> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => a.checked_add(*b).map_or_else(
|| Precision::Inexact(a.saturating_add(*b)),
Precision::Exact,
),
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => {
Precision::Inexact(a.saturating_add(*b))
}
(_, _) => Precision::Absent,
}
}
/// Calculates the difference of two (possibly inexact) [`usize`] values,
/// conservatively propagating exactness information. If one of the input
/// values is [`Precision::Absent`], the result is `Absent` too.
pub fn sub(&self, other: &Precision<usize>) -> Precision<usize> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => a.checked_sub(*b).map_or_else(
|| Precision::Inexact(a.saturating_sub(*b)),
Precision::Exact,
),
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => {
Precision::Inexact(a.saturating_sub(*b))
}
(_, _) => Precision::Absent,
}
}
/// Calculates the multiplication of two (possibly inexact) [`usize`] values,
/// conservatively propagating exactness information. If one of the input
/// values is [`Precision::Absent`], the result is `Absent` too.
pub fn multiply(&self, other: &Precision<usize>) -> Precision<usize> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => a.checked_mul(*b).map_or_else(
|| Precision::Inexact(a.saturating_mul(*b)),
Precision::Exact,
),
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => {
Precision::Inexact(a.saturating_mul(*b))
}
(_, _) => Precision::Absent,
}
}
/// Return the estimate of applying a filter with estimated selectivity
/// `selectivity` to this Precision. A selectivity of `1.0` means that all
/// rows are selected. A selectivity of `0.5` means half the rows are
/// selected. Will always return inexact statistics.
pub fn with_estimated_selectivity(self, selectivity: f64) -> Self {
self.map(|v| ((v as f64 * selectivity).ceil()) as usize)
.to_inexact()
}
}
impl Precision<ScalarValue> {
/// Calculates the sum of two (possibly inexact) [`ScalarValue`] values,
/// conservatively propagating exactness information. If one of the input
/// values is [`Precision::Absent`], the result is `Absent` too.
pub fn add(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => {
a.add(b).map(Precision::Exact).unwrap_or(Precision::Absent)
}
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => a
.add(b)
.map(Precision::Inexact)
.unwrap_or(Precision::Absent),
(_, _) => Precision::Absent,
}
}
/// Calculates the difference of two (possibly inexact) [`ScalarValue`] values,
/// conservatively propagating exactness information. If one of the input
/// values is [`Precision::Absent`], the result is `Absent` too.
pub fn sub(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => {
a.sub(b).map(Precision::Exact).unwrap_or(Precision::Absent)
}
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => a
.sub(b)
.map(Precision::Inexact)
.unwrap_or(Precision::Absent),
(_, _) => Precision::Absent,
}
}
/// Calculates the multiplication of two (possibly inexact) [`ScalarValue`] values,
/// conservatively propagating exactness information. If one of the input
/// values is [`Precision::Absent`], the result is `Absent` too.
pub fn multiply(&self, other: &Precision<ScalarValue>) -> Precision<ScalarValue> {
match (self, other) {
(Precision::Exact(a), Precision::Exact(b)) => a
.mul_checked(b)
.map(Precision::Exact)
.unwrap_or(Precision::Absent),
(Precision::Inexact(a), Precision::Exact(b))
| (Precision::Exact(a), Precision::Inexact(b))
| (Precision::Inexact(a), Precision::Inexact(b)) => a
.mul_checked(b)
.map(Precision::Inexact)
.unwrap_or(Precision::Absent),
(_, _) => Precision::Absent,
}
}
/// Casts the value to the given data type, propagating exactness information.
pub fn cast_to(&self, data_type: &DataType) -> Result<Precision<ScalarValue>> {
match self {
Precision::Exact(value) => value.cast_to(data_type).map(Precision::Exact),
Precision::Inexact(value) => value.cast_to(data_type).map(Precision::Inexact),
Precision::Absent => Ok(Precision::Absent),
}
}
}
impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> Debug for Precision<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Precision::Exact(inner) => write!(f, "Exact({inner:?})"),
Precision::Inexact(inner) => write!(f, "Inexact({inner:?})"),
Precision::Absent => write!(f, "Absent"),
}
}
}
impl<T: Debug + Clone + PartialEq + Eq + PartialOrd> Display for Precision<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Precision::Exact(inner) => write!(f, "Exact({inner:?})"),
Precision::Inexact(inner) => write!(f, "Inexact({inner:?})"),
Precision::Absent => write!(f, "Absent"),
}
}
}
impl From<Precision<usize>> for Precision<ScalarValue> {
fn from(value: Precision<usize>) -> Self {
match value {
Precision::Exact(v) => Precision::Exact(ScalarValue::UInt64(Some(v as u64))),
Precision::Inexact(v) => {
Precision::Inexact(ScalarValue::UInt64(Some(v as u64)))
}
Precision::Absent => Precision::Absent,
}
}
}
/// Statistics for a relation
/// Fields are optional and can be inexact because the sources
/// sometimes provide approximate estimates for performance reasons
/// and the transformations output are not always predictable.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Statistics {
/// The number of table rows.
pub num_rows: Precision<usize>,
/// Total bytes of the table rows.
pub total_byte_size: Precision<usize>,
/// Statistics on a column level.
///
/// It must contains a [`ColumnStatistics`] for each field in the schema of
/// the table to which the [`Statistics`] refer.
pub column_statistics: Vec<ColumnStatistics>,
}
impl Default for Statistics {
/// Returns a new [`Statistics`] instance with all fields set to unknown
/// and no columns.
fn default() -> Self {
Self {
num_rows: Precision::Absent,
total_byte_size: Precision::Absent,
column_statistics: vec![],
}
}
}
impl Statistics {
/// Returns a [`Statistics`] instance for the given schema by assigning
/// unknown statistics to each column in the schema.
pub fn new_unknown(schema: &Schema) -> Self {
Self {
num_rows: Precision::Absent,
total_byte_size: Precision::Absent,
column_statistics: Statistics::unknown_column(schema),
}
}
/// Returns an unbounded `ColumnStatistics` for each field in the schema.
pub fn unknown_column(schema: &Schema) -> Vec<ColumnStatistics> {
schema
.fields()
.iter()
.map(|_| ColumnStatistics::new_unknown())
.collect()
}
/// Set the number of rows
pub fn with_num_rows(mut self, num_rows: Precision<usize>) -> Self {
self.num_rows = num_rows;
self
}
/// Set the total size, in bytes
pub fn with_total_byte_size(mut self, total_byte_size: Precision<usize>) -> Self {
self.total_byte_size = total_byte_size;
self
}
/// Add a column to the column statistics
pub fn add_column_statistics(mut self, column_stats: ColumnStatistics) -> Self {
self.column_statistics.push(column_stats);
self
}
/// If the exactness of a [`Statistics`] instance is lost, this function relaxes
/// the exactness of all information by converting them [`Precision::Inexact`].
pub fn to_inexact(mut self) -> Self {
self.num_rows = self.num_rows.to_inexact();
self.total_byte_size = self.total_byte_size.to_inexact();
self.column_statistics = self
.column_statistics
.into_iter()
.map(|s| s.to_inexact())
.collect();
self
}
/// Project the statistics to the given column indices.
///
/// For example, if we had statistics for columns `{"a", "b", "c"}`,
/// projecting to `vec![2, 1]` would return statistics for columns `{"c",
/// "b"}`.
pub fn project(mut self, projection: Option<&Vec<usize>>) -> Self {
let Some(projection) = projection else {
return self;
};
#[allow(clippy::large_enum_variant)]
enum Slot {
/// The column is taken and put into the specified statistics location
Taken(usize),
/// The original columns is present
Present(ColumnStatistics),
}
// Convert to Vec<Slot> so we can avoid copying the statistics
let mut columns: Vec<_> = std::mem::take(&mut self.column_statistics)
.into_iter()
.map(Slot::Present)
.collect();
for idx in projection {
let next_idx = self.column_statistics.len();
let slot = std::mem::replace(
columns.get_mut(*idx).expect("projection out of bounds"),
Slot::Taken(next_idx),
);
match slot {
// The column was there, so just move it
Slot::Present(col) => self.column_statistics.push(col),
// The column was taken, so copy from the previous location
Slot::Taken(prev_idx) => self
.column_statistics
.push(self.column_statistics[prev_idx].clone()),
}
}
self
}
/// Calculates the statistics after applying `fetch` and `skip` operations.
///
/// Here, `self` denotes per-partition statistics. Use the `n_partitions`
/// parameter to compute global statistics in a multi-partition setting.
pub fn with_fetch(
mut self,
fetch: Option<usize>,
skip: usize,
n_partitions: usize,
) -> Result<Self> {
let fetch_val = fetch.unwrap_or(usize::MAX);
// Get the ratio of rows after / rows before on a per-partition basis
let num_rows_before = self.num_rows;
self.num_rows = match self {
Statistics {
num_rows: Precision::Exact(nr),
..
}
| Statistics {
num_rows: Precision::Inexact(nr),
..
} => {
// Here, the inexact case gives us an upper bound on the number of rows.
if nr <= skip {
// All input data will be skipped:
Precision::Exact(0)
} else if nr <= fetch_val && skip == 0 {
// If the input does not reach the `fetch` globally, and `skip`
// is zero (meaning the input and output are identical), return
// input stats as is.
// TODO: Can input stats still be used, but adjusted, when `skip`
// is non-zero?
return Ok(self);
} else if nr - skip <= fetch_val {
// After `skip` input rows are skipped, the remaining rows are
// less than or equal to the `fetch` values, so `num_rows` must
// equal the remaining rows.
check_num_rows(
(nr - skip).checked_mul(n_partitions),
// We know that we have an estimate for the number of rows:
self.num_rows.is_exact().unwrap(),
)
} else {
// At this point we know that we were given a `fetch` value
// as the `None` case would go into the branch above. Since
// the input has more rows than `fetch + skip`, the number
// of rows will be the `fetch`, other statistics will have to be downgraded to inexact.
check_num_rows(
fetch_val.checked_mul(n_partitions),
// We know that we have an estimate for the number of rows:
self.num_rows.is_exact().unwrap(),
)
}
}
Statistics {
num_rows: Precision::Absent,
..
} => check_num_rows(fetch.and_then(|v| v.checked_mul(n_partitions)), false),
};
let ratio: f64 = match (num_rows_before, self.num_rows) {
(
Precision::Exact(nr_before) | Precision::Inexact(nr_before),
Precision::Exact(nr_after) | Precision::Inexact(nr_after),
) => {
if nr_before == 0 {
0.0
} else {
nr_after as f64 / nr_before as f64
}
}
_ => 0.0,
};
self.column_statistics = self
.column_statistics
.into_iter()
.map(ColumnStatistics::to_inexact)
.collect();
// Adjust the total_byte_size for the ratio of rows before and after, also marking it as inexact
self.total_byte_size = match &self.total_byte_size {
Precision::Exact(n) | Precision::Inexact(n) => {
let adjusted = (*n as f64 * ratio) as usize;
Precision::Inexact(adjusted)
}
Precision::Absent => Precision::Absent,
};
Ok(self)
}
/// Summarize zero or more statistics into a single `Statistics` instance.
///
/// The method assumes that all statistics are for the same schema.
/// If not, maybe you can call `SchemaMapper::map_column_statistics` to make them consistent.
///
/// Returns an error if the statistics do not match the specified schemas.
pub fn try_merge_iter<'a, I>(items: I, schema: &Schema) -> Result<Statistics>
where
I: IntoIterator<Item = &'a Statistics>,
{
let mut items = items.into_iter();
let Some(init) = items.next() else {
return Ok(Statistics::new_unknown(schema));
};
items.try_fold(init.clone(), |acc: Statistics, item_stats: &Statistics| {
acc.try_merge(item_stats)
})
}
/// Merge this Statistics value with another Statistics value.
///
/// Returns an error if the statistics do not match (different schemas).
///
/// # Example
/// ```
/// # use datafusion_common::{ColumnStatistics, ScalarValue, Statistics};
/// # use arrow::datatypes::{Field, Schema, DataType};
/// # use datafusion_common::stats::Precision;
/// let stats1 = Statistics::default()
/// .with_num_rows(Precision::Exact(1))
/// .with_total_byte_size(Precision::Exact(2))
/// .add_column_statistics(
/// ColumnStatistics::new_unknown()
/// .with_null_count(Precision::Exact(3))
/// .with_min_value(Precision::Exact(ScalarValue::from(4)))
/// .with_max_value(Precision::Exact(ScalarValue::from(5))),
/// );
///
/// let stats2 = Statistics::default()
/// .with_num_rows(Precision::Exact(10))
/// .with_total_byte_size(Precision::Inexact(20))
/// .add_column_statistics(
/// ColumnStatistics::new_unknown()
/// // absent null count
/// .with_min_value(Precision::Exact(ScalarValue::from(40)))
/// .with_max_value(Precision::Exact(ScalarValue::from(50))),
/// );
///
/// let merged_stats = stats1.try_merge(&stats2).unwrap();
/// let expected_stats = Statistics::default()
/// .with_num_rows(Precision::Exact(11))
/// .with_total_byte_size(Precision::Inexact(22)) // inexact in stats2 --> inexact
/// .add_column_statistics(
/// ColumnStatistics::new_unknown()
/// .with_null_count(Precision::Absent) // missing from stats2 --> absent
/// .with_min_value(Precision::Exact(ScalarValue::from(4)))
/// .with_max_value(Precision::Exact(ScalarValue::from(50))),
/// );
///
/// assert_eq!(merged_stats, expected_stats)
/// ```
pub fn try_merge(self, other: &Statistics) -> Result<Self> {
let Self {
mut num_rows,
mut total_byte_size,
mut column_statistics,
} = self;
// Accumulate statistics for subsequent items
num_rows = num_rows.add(&other.num_rows);
total_byte_size = total_byte_size.add(&other.total_byte_size);
if column_statistics.len() != other.column_statistics.len() {
return _plan_err!(
"Cannot merge statistics with different number of columns: {} vs {}",
column_statistics.len(),
other.column_statistics.len()
);
}
for (item_col_stats, col_stats) in other
.column_statistics
.iter()
.zip(column_statistics.iter_mut())
{
col_stats.null_count = col_stats.null_count.add(&item_col_stats.null_count);
col_stats.max_value = col_stats.max_value.max(&item_col_stats.max_value);
col_stats.min_value = col_stats.min_value.min(&item_col_stats.min_value);
col_stats.sum_value = col_stats.sum_value.add(&item_col_stats.sum_value);
col_stats.distinct_count = Precision::Absent;
}
Ok(Statistics {
num_rows,
total_byte_size,
column_statistics,
})
}
}
/// Creates an estimate of the number of rows in the output using the given
/// optional value and exactness flag.
fn check_num_rows(value: Option<usize>, is_exact: bool) -> Precision<usize> {
if let Some(value) = value {
if is_exact {
Precision::Exact(value)
} else {
// If the input stats are inexact, so are the output stats.
Precision::Inexact(value)
}
} else {
// If the estimate is not available (e.g. due to an overflow), we can
// not produce a reliable estimate.
Precision::Absent
}
}
impl Display for Statistics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// string of column statistics
let column_stats = self
.column_statistics
.iter()
.enumerate()
.map(|(i, cs)| {
let s = format!("(Col[{i}]:");
let s = if cs.min_value != Precision::Absent {
format!("{} Min={}", s, cs.min_value)
} else {
s
};
let s = if cs.max_value != Precision::Absent {
format!("{} Max={}", s, cs.max_value)
} else {
s
};
let s = if cs.sum_value != Precision::Absent {
format!("{} Sum={}", s, cs.sum_value)
} else {
s
};
let s = if cs.null_count != Precision::Absent {
format!("{} Null={}", s, cs.null_count)
} else {
s
};
let s = if cs.distinct_count != Precision::Absent {
format!("{} Distinct={}", s, cs.distinct_count)
} else {
s
};
s + ")"
})
.collect::<Vec<_>>()
.join(",");
write!(
f,
"Rows={}, Bytes={}, [{}]",
self.num_rows, self.total_byte_size, column_stats
)?;
Ok(())
}
}
/// Statistics for a column within a relation
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ColumnStatistics {
/// Number of null values on column
pub null_count: Precision<usize>,
/// Maximum value of column
pub max_value: Precision<ScalarValue>,
/// Minimum value of column
pub min_value: Precision<ScalarValue>,
/// Sum value of a column
pub sum_value: Precision<ScalarValue>,
/// Number of distinct values
pub distinct_count: Precision<usize>,
}
impl ColumnStatistics {
/// Column contains a single non null value (e.g constant).
pub fn is_singleton(&self) -> bool {
match (&self.min_value, &self.max_value) {
// Min and max values are the same and not infinity.
(Precision::Exact(min), Precision::Exact(max)) => {
!min.is_null() && !max.is_null() && (min == max)
}
(_, _) => false,
}
}
/// Returns a [`ColumnStatistics`] instance having all [`Precision::Absent`] parameters.
pub fn new_unknown() -> Self {
Self {
null_count: Precision::Absent,
max_value: Precision::Absent,
min_value: Precision::Absent,
sum_value: Precision::Absent,
distinct_count: Precision::Absent,
}
}
/// Set the null count
pub fn with_null_count(mut self, null_count: Precision<usize>) -> Self {
self.null_count = null_count;
self
}
/// Set the max value
pub fn with_max_value(mut self, max_value: Precision<ScalarValue>) -> Self {
self.max_value = max_value;
self
}
/// Set the min value
pub fn with_min_value(mut self, min_value: Precision<ScalarValue>) -> Self {
self.min_value = min_value;
self
}
/// Set the sum value
pub fn with_sum_value(mut self, sum_value: Precision<ScalarValue>) -> Self {
self.sum_value = sum_value;
self
}
/// Set the distinct count
pub fn with_distinct_count(mut self, distinct_count: Precision<usize>) -> Self {
self.distinct_count = distinct_count;
self
}
/// If the exactness of a [`ColumnStatistics`] instance is lost, this
/// function relaxes the exactness of all information by converting them
/// [`Precision::Inexact`].
pub fn to_inexact(mut self) -> Self {
self.null_count = self.null_count.to_inexact();
self.max_value = self.max_value.to_inexact();
self.min_value = self.min_value.to_inexact();
self.sum_value = self.sum_value.to_inexact();
self.distinct_count = self.distinct_count.to_inexact();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_contains;
use arrow::datatypes::Field;
use std::sync::Arc;
#[test]
fn test_get_value() {
let exact_precision = Precision::Exact(42);
let inexact_precision = Precision::Inexact(23);
let absent_precision = Precision::<i32>::Absent;
assert_eq!(*exact_precision.get_value().unwrap(), 42);
assert_eq!(*inexact_precision.get_value().unwrap(), 23);
assert_eq!(absent_precision.get_value(), None);
}
#[test]
fn test_map() {
let exact_precision = Precision::Exact(42);
let inexact_precision = Precision::Inexact(23);
let absent_precision = Precision::Absent;
let squared = |x| x * x;
assert_eq!(exact_precision.map(squared), Precision::Exact(1764));
assert_eq!(inexact_precision.map(squared), Precision::Inexact(529));
assert_eq!(absent_precision.map(squared), Precision::Absent);
}
#[test]
fn test_is_exact() {
let exact_precision = Precision::Exact(42);
let inexact_precision = Precision::Inexact(23);
let absent_precision = Precision::<i32>::Absent;
assert_eq!(exact_precision.is_exact(), Some(true));
assert_eq!(inexact_precision.is_exact(), Some(false));
assert_eq!(absent_precision.is_exact(), None);
}
#[test]
fn test_max() {
let precision1 = Precision::Exact(42);
let precision2 = Precision::Inexact(23);
let precision3 = Precision::Exact(30);
let absent_precision = Precision::Absent;
assert_eq!(precision1.max(&precision2), Precision::Inexact(42));
assert_eq!(precision1.max(&precision3), Precision::Exact(42));
assert_eq!(precision2.max(&precision3), Precision::Inexact(30));
assert_eq!(precision1.max(&absent_precision), Precision::Absent);
}
#[test]
fn test_min() {
let precision1 = Precision::Exact(42);
let precision2 = Precision::Inexact(23);
let precision3 = Precision::Exact(30);
let absent_precision = Precision::Absent;
assert_eq!(precision1.min(&precision2), Precision::Inexact(23));
assert_eq!(precision1.min(&precision3), Precision::Exact(30));
assert_eq!(precision2.min(&precision3), Precision::Inexact(23));
assert_eq!(precision1.min(&absent_precision), Precision::Absent);
}
#[test]
fn test_to_inexact() {
let exact_precision = Precision::Exact(42);
let inexact_precision = Precision::Inexact(42);
let absent_precision = Precision::<i32>::Absent;
assert_eq!(exact_precision.to_inexact(), inexact_precision);
assert_eq!(inexact_precision.to_inexact(), inexact_precision);
assert_eq!(absent_precision.to_inexact(), absent_precision);
}
#[test]
fn test_add() {
let precision1 = Precision::Exact(42);
let precision2 = Precision::Inexact(23);
let precision3 = Precision::Exact(30);
let absent_precision = Precision::Absent;
let precision_max_exact = Precision::Exact(usize::MAX);
let precision_max_inexact = Precision::Exact(usize::MAX);
assert_eq!(precision1.add(&precision2), Precision::Inexact(65));
assert_eq!(precision1.add(&precision3), Precision::Exact(72));
assert_eq!(precision2.add(&precision3), Precision::Inexact(53));
assert_eq!(precision1.add(&absent_precision), Precision::Absent);
assert_eq!(
precision_max_exact.add(&precision1),
Precision::Inexact(usize::MAX)
);
assert_eq!(
precision_max_inexact.add(&precision1),
Precision::Inexact(usize::MAX)
);
}
#[test]
fn test_add_scalar() {
let precision = Precision::Exact(ScalarValue::Int32(Some(42)));
assert_eq!(
precision.add(&Precision::Exact(ScalarValue::Int32(Some(23)))),
Precision::Exact(ScalarValue::Int32(Some(65))),
);
assert_eq!(
precision.add(&Precision::Inexact(ScalarValue::Int32(Some(23)))),
Precision::Inexact(ScalarValue::Int32(Some(65))),
);
assert_eq!(
precision.add(&Precision::Exact(ScalarValue::Int32(None))),
// As per behavior of ScalarValue::add
Precision::Exact(ScalarValue::Int32(None)),
);
assert_eq!(precision.add(&Precision::Absent), Precision::Absent);
}
#[test]
fn test_sub() {
let precision1 = Precision::Exact(42);
let precision2 = Precision::Inexact(23);
let precision3 = Precision::Exact(30);
let absent_precision = Precision::Absent;
assert_eq!(precision1.sub(&precision2), Precision::Inexact(19));
assert_eq!(precision1.sub(&precision3), Precision::Exact(12));
assert_eq!(precision2.sub(&precision1), Precision::Inexact(0));
assert_eq!(precision3.sub(&precision1), Precision::Inexact(0));
assert_eq!(precision1.sub(&absent_precision), Precision::Absent);
}
#[test]
fn test_sub_scalar() {
let precision = Precision::Exact(ScalarValue::Int32(Some(42)));
assert_eq!(
precision.sub(&Precision::Exact(ScalarValue::Int32(Some(23)))),
Precision::Exact(ScalarValue::Int32(Some(19))),
);
assert_eq!(
precision.sub(&Precision::Inexact(ScalarValue::Int32(Some(23)))),
Precision::Inexact(ScalarValue::Int32(Some(19))),
);
assert_eq!(
precision.sub(&Precision::Exact(ScalarValue::Int32(None))),
// As per behavior of ScalarValue::sub
Precision::Exact(ScalarValue::Int32(None)),
);
assert_eq!(precision.sub(&Precision::Absent), Precision::Absent);
}
#[test]
fn test_multiply() {
let precision1 = Precision::Exact(6);
let precision2 = Precision::Inexact(3);
let precision3 = Precision::Exact(5);
let precision_max_exact = Precision::Exact(usize::MAX);
let precision_max_inexact = Precision::Exact(usize::MAX);
let absent_precision = Precision::Absent;
assert_eq!(precision1.multiply(&precision2), Precision::Inexact(18));
assert_eq!(precision1.multiply(&precision3), Precision::Exact(30));
assert_eq!(precision2.multiply(&precision3), Precision::Inexact(15));
assert_eq!(precision1.multiply(&absent_precision), Precision::Absent);
assert_eq!(
precision_max_exact.multiply(&precision1),
Precision::Inexact(usize::MAX)
);
assert_eq!(
precision_max_inexact.multiply(&precision1),
Precision::Inexact(usize::MAX)
);
}
#[test]
fn test_multiply_scalar() {
let precision = Precision::Exact(ScalarValue::Int32(Some(6)));
assert_eq!(
precision.multiply(&Precision::Exact(ScalarValue::Int32(Some(5)))),
Precision::Exact(ScalarValue::Int32(Some(30))),
);
assert_eq!(
precision.multiply(&Precision::Inexact(ScalarValue::Int32(Some(5)))),
Precision::Inexact(ScalarValue::Int32(Some(30))),
);
assert_eq!(
precision.multiply(&Precision::Exact(ScalarValue::Int32(None))),
// As per behavior of ScalarValue::mul_checked
Precision::Exact(ScalarValue::Int32(None)),
);
assert_eq!(precision.multiply(&Precision::Absent), Precision::Absent);
}
#[test]
fn test_cast_to() {
// Valid
assert_eq!(
Precision::Exact(ScalarValue::Int32(Some(42)))
.cast_to(&DataType::Int64)
.unwrap(),
Precision::Exact(ScalarValue::Int64(Some(42))),
);
assert_eq!(
Precision::Inexact(ScalarValue::Int32(Some(42)))
.cast_to(&DataType::Int64)
.unwrap(),
Precision::Inexact(ScalarValue::Int64(Some(42))),
);
// Null
assert_eq!(
Precision::Exact(ScalarValue::Int32(None))
.cast_to(&DataType::Int64)
.unwrap(),
Precision::Exact(ScalarValue::Int64(None)),
);
// Overflow returns error
assert!(Precision::Exact(ScalarValue::Int32(Some(256)))
.cast_to(&DataType::Int8)
.is_err());
}
#[test]
fn test_precision_cloning() {
// Precision<usize> is copy
let precision: Precision<usize> = Precision::Exact(42);
let p2 = precision;
assert_eq!(precision, p2);
// Precision<ScalarValue> is not copy (requires .clone())
let precision: Precision<ScalarValue> =
Precision::Exact(ScalarValue::Int64(Some(42)));
// Clippy would complain about this if it were Copy
#[allow(clippy::redundant_clone)]
let p2 = precision.clone();
assert_eq!(precision, p2);
}
#[test]
fn test_project_none() {
let projection = None;
let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
assert_eq!(stats, make_stats(vec![10, 20, 30]));
}
#[test]
fn test_project_empty() {
let projection = Some(vec![]);
let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
assert_eq!(stats, make_stats(vec![]));
}
#[test]
fn test_project_swap() {
let projection = Some(vec![2, 1]);
let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
assert_eq!(stats, make_stats(vec![30, 20]));
}
#[test]
fn test_project_repeated() {
let projection = Some(vec![1, 2, 1, 1, 0, 2]);
let stats = make_stats(vec![10, 20, 30]).project(projection.as_ref());
assert_eq!(stats, make_stats(vec![20, 30, 20, 20, 10, 30]));
}
// Make a Statistics structure with the specified null counts for each column
fn make_stats(counts: impl IntoIterator<Item = usize>) -> Statistics {
Statistics {
num_rows: Precision::Exact(42),
total_byte_size: Precision::Exact(500),
column_statistics: counts.into_iter().map(col_stats_i64).collect(),
}
}
fn col_stats_i64(null_count: usize) -> ColumnStatistics {
ColumnStatistics {
null_count: Precision::Exact(null_count),
max_value: Precision::Exact(ScalarValue::Int64(Some(42))),
min_value: Precision::Exact(ScalarValue::Int64(Some(64))),
sum_value: Precision::Exact(ScalarValue::Int64(Some(4600))),
distinct_count: Precision::Exact(100),
}
}
#[test]
fn test_try_merge_basic() {
// Create a schema with two columns
let schema = Arc::new(Schema::new(vec![
Field::new("col1", DataType::Int32, false),
Field::new("col2", DataType::Int32, false),
]));
// Create items with statistics
let stats1 = Statistics {
num_rows: Precision::Exact(10),
total_byte_size: Precision::Exact(100),
column_statistics: vec![
ColumnStatistics {
null_count: Precision::Exact(1),
max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
distinct_count: Precision::Absent,
},
ColumnStatistics {
null_count: Precision::Exact(2),
max_value: Precision::Exact(ScalarValue::Int32(Some(200))),
min_value: Precision::Exact(ScalarValue::Int32(Some(10))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(1000))),
distinct_count: Precision::Absent,
},
],
};
let stats2 = Statistics {
num_rows: Precision::Exact(15),
total_byte_size: Precision::Exact(150),
column_statistics: vec![
ColumnStatistics {
null_count: Precision::Exact(2),
max_value: Precision::Exact(ScalarValue::Int32(Some(120))),
min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(600))),
distinct_count: Precision::Absent,
},
ColumnStatistics {
null_count: Precision::Exact(3),
max_value: Precision::Exact(ScalarValue::Int32(Some(180))),
min_value: Precision::Exact(ScalarValue::Int32(Some(5))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(1200))),
distinct_count: Precision::Absent,
},
],
};
let items = vec![stats1, stats2];
let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
// Verify the results
assert_eq!(summary_stats.num_rows, Precision::Exact(25)); // 10 + 15
assert_eq!(summary_stats.total_byte_size, Precision::Exact(250)); // 100 + 150
// Verify column statistics
let col1_stats = &summary_stats.column_statistics[0];
assert_eq!(col1_stats.null_count, Precision::Exact(3)); // 1 + 2
assert_eq!(
col1_stats.max_value,
Precision::Exact(ScalarValue::Int32(Some(120)))
);
assert_eq!(
col1_stats.min_value,
Precision::Exact(ScalarValue::Int32(Some(-10)))
);
assert_eq!(
col1_stats.sum_value,
Precision::Exact(ScalarValue::Int32(Some(1100)))
); // 500 + 600
let col2_stats = &summary_stats.column_statistics[1];
assert_eq!(col2_stats.null_count, Precision::Exact(5)); // 2 + 3
assert_eq!(
col2_stats.max_value,
Precision::Exact(ScalarValue::Int32(Some(200)))
);
assert_eq!(
col2_stats.min_value,
Precision::Exact(ScalarValue::Int32(Some(5)))
);
assert_eq!(
col2_stats.sum_value,
Precision::Exact(ScalarValue::Int32(Some(2200)))
); // 1000 + 1200
}
#[test]
fn test_try_merge_mixed_precision() {
// Create a schema with one column
let schema = Arc::new(Schema::new(vec![Field::new(
"col1",
DataType::Int32,
false,
)]));
// Create items with different precision levels
let stats1 = Statistics {
num_rows: Precision::Exact(10),
total_byte_size: Precision::Inexact(100),
column_statistics: vec![ColumnStatistics {
null_count: Precision::Exact(1),
max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
distinct_count: Precision::Absent,
}],
};
let stats2 = Statistics {
num_rows: Precision::Inexact(15),
total_byte_size: Precision::Exact(150),
column_statistics: vec![ColumnStatistics {
null_count: Precision::Inexact(2),
max_value: Precision::Inexact(ScalarValue::Int32(Some(120))),
min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
sum_value: Precision::Absent,
distinct_count: Precision::Absent,
}],
};
let items = vec![stats1, stats2];
let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
assert_eq!(summary_stats.num_rows, Precision::Inexact(25));
assert_eq!(summary_stats.total_byte_size, Precision::Inexact(250));
let col_stats = &summary_stats.column_statistics[0];
assert_eq!(col_stats.null_count, Precision::Inexact(3));
assert_eq!(
col_stats.max_value,
Precision::Inexact(ScalarValue::Int32(Some(120)))
);
assert_eq!(
col_stats.min_value,
Precision::Inexact(ScalarValue::Int32(Some(-10)))
);
assert!(matches!(col_stats.sum_value, Precision::Absent));
}
#[test]
fn test_try_merge_empty() {
let schema = Arc::new(Schema::new(vec![Field::new(
"col1",
DataType::Int32,
false,
)]));
// Empty collection
let items: Vec<Statistics> = vec![];
let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
// Verify default values for empty collection
assert_eq!(summary_stats.num_rows, Precision::Absent);
assert_eq!(summary_stats.total_byte_size, Precision::Absent);
assert_eq!(summary_stats.column_statistics.len(), 1);
assert_eq!(
summary_stats.column_statistics[0].null_count,
Precision::Absent
);
}
#[test]
fn test_try_merge_mismatched_size() {
// Create a schema with one column
let schema = Arc::new(Schema::new(vec![Field::new(
"col1",
DataType::Int32,
false,
)]));
// No column statistics
let stats1 = Statistics::default();
let stats2 =
Statistics::default().add_column_statistics(ColumnStatistics::new_unknown());
let items = vec![stats1, stats2];
let e = Statistics::try_merge_iter(&items, &schema).unwrap_err();
assert_contains!(e.to_string(), "Error during planning: Cannot merge statistics with different number of columns: 0 vs 1");
}
#[test]
fn test_try_merge_distinct_count_absent() {
// Create statistics with known distinct counts
let stats1 = Statistics::default()
.with_num_rows(Precision::Exact(10))
.with_total_byte_size(Precision::Exact(100))
.add_column_statistics(
ColumnStatistics::new_unknown()
.with_null_count(Precision::Exact(0))
.with_min_value(Precision::Exact(ScalarValue::Int32(Some(1))))
.with_max_value(Precision::Exact(ScalarValue::Int32(Some(10))))
.with_distinct_count(Precision::Exact(5)),
);
let stats2 = Statistics::default()
.with_num_rows(Precision::Exact(15))
.with_total_byte_size(Precision::Exact(150))
.add_column_statistics(
ColumnStatistics::new_unknown()
.with_null_count(Precision::Exact(0))
.with_min_value(Precision::Exact(ScalarValue::Int32(Some(5))))
.with_max_value(Precision::Exact(ScalarValue::Int32(Some(20))))
.with_distinct_count(Precision::Exact(7)),
);
// Merge statistics
let merged_stats = stats1.try_merge(&stats2).unwrap();
// Verify the results
assert_eq!(merged_stats.num_rows, Precision::Exact(25));
assert_eq!(merged_stats.total_byte_size, Precision::Exact(250));
let col_stats = &merged_stats.column_statistics[0];
assert_eq!(col_stats.null_count, Precision::Exact(0));
assert_eq!(
col_stats.min_value,
Precision::Exact(ScalarValue::Int32(Some(1)))
);
assert_eq!(
col_stats.max_value,
Precision::Exact(ScalarValue::Int32(Some(20)))
);
// Distinct count should be Absent after merge
assert_eq!(col_stats.distinct_count, Precision::Absent);
}
#[test]
fn test_with_fetch_basic_preservation() {
// Test that column statistics and byte size are preserved (as inexact) when applying fetch
let original_stats = Statistics {
num_rows: Precision::Exact(1000),
total_byte_size: Precision::Exact(8000),
column_statistics: vec![
ColumnStatistics {
null_count: Precision::Exact(10),
max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
min_value: Precision::Exact(ScalarValue::Int32(Some(0))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(5050))),
distinct_count: Precision::Exact(50),
},
ColumnStatistics {
null_count: Precision::Exact(20),
max_value: Precision::Exact(ScalarValue::Int64(Some(200))),
min_value: Precision::Exact(ScalarValue::Int64(Some(10))),
sum_value: Precision::Exact(ScalarValue::Int64(Some(10100))),
distinct_count: Precision::Exact(75),
},
],
};
// Apply fetch of 100 rows (10% of original)
let result = original_stats.clone().with_fetch(Some(100), 0, 1).unwrap();
// Check num_rows
assert_eq!(result.num_rows, Precision::Exact(100));
// Check total_byte_size is scaled proportionally and marked as inexact
// 100/1000 = 0.1, so 8000 * 0.1 = 800
assert_eq!(result.total_byte_size, Precision::Inexact(800));
// Check column statistics are preserved but marked as inexact
assert_eq!(result.column_statistics.len(), 2);
// First column
assert_eq!(
result.column_statistics[0].null_count,
Precision::Inexact(10)
);
assert_eq!(
result.column_statistics[0].max_value,
Precision::Inexact(ScalarValue::Int32(Some(100)))
);
assert_eq!(
result.column_statistics[0].min_value,
Precision::Inexact(ScalarValue::Int32(Some(0)))
);
assert_eq!(
result.column_statistics[0].sum_value,
Precision::Inexact(ScalarValue::Int32(Some(5050)))
);
assert_eq!(
result.column_statistics[0].distinct_count,
Precision::Inexact(50)
);
// Second column
assert_eq!(
result.column_statistics[1].null_count,
Precision::Inexact(20)
);
assert_eq!(
result.column_statistics[1].max_value,
Precision::Inexact(ScalarValue::Int64(Some(200)))
);
assert_eq!(
result.column_statistics[1].min_value,
Precision::Inexact(ScalarValue::Int64(Some(10)))
);
assert_eq!(
result.column_statistics[1].sum_value,
Precision::Inexact(ScalarValue::Int64(Some(10100)))
);
assert_eq!(
result.column_statistics[1].distinct_count,
Precision::Inexact(75)
);
}
#[test]
fn test_with_fetch_inexact_input() {
// Test that inexact input statistics remain inexact
let original_stats = Statistics {
num_rows: Precision::Inexact(1000),
total_byte_size: Precision::Inexact(8000),
column_statistics: vec![ColumnStatistics {
null_count: Precision::Inexact(10),
max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
min_value: Precision::Inexact(ScalarValue::Int32(Some(0))),
sum_value: Precision::Inexact(ScalarValue::Int32(Some(5050))),
distinct_count: Precision::Inexact(50),
}],
};
let result = original_stats.clone().with_fetch(Some(500), 0, 1).unwrap();
// Check num_rows is inexact
assert_eq!(result.num_rows, Precision::Inexact(500));
// Check total_byte_size is scaled and inexact
// 500/1000 = 0.5, so 8000 * 0.5 = 4000
assert_eq!(result.total_byte_size, Precision::Inexact(4000));
// Column stats remain inexact
assert_eq!(
result.column_statistics[0].null_count,
Precision::Inexact(10)
);
}
#[test]
fn test_with_fetch_skip_all_rows() {
// Test when skip >= num_rows (all rows are skipped)
let original_stats = Statistics {
num_rows: Precision::Exact(100),
total_byte_size: Precision::Exact(800),
column_statistics: vec![col_stats_i64(10)],
};
let result = original_stats.clone().with_fetch(Some(50), 100, 1).unwrap();
assert_eq!(result.num_rows, Precision::Exact(0));
// When ratio is 0/100 = 0, byte size should be 0
assert_eq!(result.total_byte_size, Precision::Inexact(0));
}
#[test]
fn test_with_fetch_no_limit() {
// Test when fetch is None and skip is 0 (no limit applied)
let original_stats = Statistics {
num_rows: Precision::Exact(100),
total_byte_size: Precision::Exact(800),
column_statistics: vec![col_stats_i64(10)],
};
let result = original_stats.clone().with_fetch(None, 0, 1).unwrap();
// Stats should be unchanged when no fetch and no skip
assert_eq!(result.num_rows, Precision::Exact(100));
assert_eq!(result.total_byte_size, Precision::Exact(800));
}
#[test]
fn test_with_fetch_with_skip() {
// Test with both skip and fetch
let original_stats = Statistics {
num_rows: Precision::Exact(1000),
total_byte_size: Precision::Exact(8000),
column_statistics: vec![col_stats_i64(10)],
};
// Skip 200, fetch 300, so we get rows 200-500
let result = original_stats
.clone()
.with_fetch(Some(300), 200, 1)
.unwrap();
assert_eq!(result.num_rows, Precision::Exact(300));
// 300/1000 = 0.3, so 8000 * 0.3 = 2400
assert_eq!(result.total_byte_size, Precision::Inexact(2400));
}
#[test]
fn test_with_fetch_multi_partition() {
// Test with multiple partitions
let original_stats = Statistics {
num_rows: Precision::Exact(1000), // per partition
total_byte_size: Precision::Exact(8000),
column_statistics: vec![col_stats_i64(10)],
};
// Fetch 100 per partition, 4 partitions = 400 total
let result = original_stats.clone().with_fetch(Some(100), 0, 4).unwrap();
assert_eq!(result.num_rows, Precision::Exact(400));
// 400/1000 = 0.4, so 8000 * 0.4 = 3200
assert_eq!(result.total_byte_size, Precision::Inexact(3200));
}
#[test]
fn test_with_fetch_absent_stats() {
// Test with absent statistics
let original_stats = Statistics {
num_rows: Precision::Absent,
total_byte_size: Precision::Absent,
column_statistics: vec![ColumnStatistics {
null_count: Precision::Absent,
max_value: Precision::Absent,
min_value: Precision::Absent,
sum_value: Precision::Absent,
distinct_count: Precision::Absent,
}],
};
let result = original_stats.clone().with_fetch(Some(100), 0, 1).unwrap();
// With absent input stats, output should be inexact estimate
assert_eq!(result.num_rows, Precision::Inexact(100));
assert_eq!(result.total_byte_size, Precision::Absent);
// Column stats should remain absent
assert_eq!(result.column_statistics[0].null_count, Precision::Absent);
}
#[test]
fn test_with_fetch_fetch_exceeds_rows() {
// Test when fetch is larger than available rows after skip
let original_stats = Statistics {
num_rows: Precision::Exact(100),
total_byte_size: Precision::Exact(800),
column_statistics: vec![col_stats_i64(10)],
};
// Skip 50, fetch 100, but only 50 rows remain
let result = original_stats.clone().with_fetch(Some(100), 50, 1).unwrap();
assert_eq!(result.num_rows, Precision::Exact(50));
// 50/100 = 0.5, so 800 * 0.5 = 400
assert_eq!(result.total_byte_size, Precision::Inexact(400));
}
#[test]
fn test_with_fetch_preserves_all_column_stats() {
// Comprehensive test that all column statistic fields are preserved
let original_col_stats = ColumnStatistics {
null_count: Precision::Exact(42),
max_value: Precision::Exact(ScalarValue::Int32(Some(999))),
min_value: Precision::Exact(ScalarValue::Int32(Some(-100))),
sum_value: Precision::Exact(ScalarValue::Int32(Some(123456))),
distinct_count: Precision::Exact(789),
};
let original_stats = Statistics {
num_rows: Precision::Exact(1000),
total_byte_size: Precision::Exact(8000),
column_statistics: vec![original_col_stats.clone()],
};
let result = original_stats.with_fetch(Some(250), 0, 1).unwrap();
let result_col_stats = &result.column_statistics[0];
// All values should be preserved but marked as inexact
assert_eq!(result_col_stats.null_count, Precision::Inexact(42));
assert_eq!(
result_col_stats.max_value,
Precision::Inexact(ScalarValue::Int32(Some(999)))
);
assert_eq!(
result_col_stats.min_value,
Precision::Inexact(ScalarValue::Int32(Some(-100)))
);
assert_eq!(
result_col_stats.sum_value,
Precision::Inexact(ScalarValue::Int32(Some(123456)))
);
assert_eq!(result_col_stats.distinct_count, Precision::Inexact(789));
}
}