datafusion_iceberg 0.9.0

Datafusion integration for Iceberg table format
Documentation
use datafusion::{
    common::stats::Precision,
    physical_plan::{ColumnStatistics, Statistics},
    scalar::ScalarValue,
};
use iceberg_rust::error::Error;
use iceberg_rust::file_format::parquet::estimate_distinct_count;
use iceberg_rust::spec::{
    manifest::{ManifestEntry, Status},
    schema::Schema,
    values::Value,
};

pub(crate) fn statistics_from_datafiles(
    schema: &Schema,
    datafiles: &[(String, ManifestEntry)],
) -> Statistics {
    datafiles
        .iter()
        .filter(|(_, manifest)| !matches!(manifest.status(), Status::Deleted))
        .map(|(_, manifest)| {
            let column_stats = column_statistics(schema, manifest);
            Statistics {
                num_rows: Precision::Exact(*manifest.data_file().record_count() as usize),
                total_byte_size: Precision::Exact(
                    *manifest.data_file().file_size_in_bytes() as usize
                ),
                column_statistics: column_stats
                    .into_iter()
                    .map(|x| ColumnStatistics {
                        null_count: x.null_count,
                        max_value: x.max_value,
                        min_value: x.min_value,
                        distinct_count: x.distinct_count,
                        sum_value: x.sum_value,
                    })
                    .collect(),
            }
        })
        .reduce(|acc, x| Statistics {
            num_rows: acc.num_rows.add(&x.num_rows),
            total_byte_size: acc.total_byte_size.add(&x.total_byte_size),
            column_statistics: acc
                .column_statistics
                .into_iter()
                .zip(x.column_statistics)
                .map(|(acc, x)| {
                    let new_distinct_count = new_distinct_count(&acc, &x);

                    ColumnStatistics {
                        null_count: acc.null_count.add(&x.null_count),
                        max_value: acc.max_value.max(&x.max_value),
                        min_value: acc.min_value.min(&x.min_value),
                        distinct_count: new_distinct_count,
                        sum_value: acc.sum_value.add(&x.sum_value),
                    }
                })
                .collect(),
        })
        .unwrap_or_default()
}

fn column_statistics<'a>(
    schema: &'a Schema,
    manifest: &'a ManifestEntry,
) -> impl Iterator<Item = ColumnStatistics> + 'a {
    schema.fields().iter().map(|x| x.id).map(|id| {
        let data_file = &manifest.data_file();
        ColumnStatistics {
            null_count: data_file
                .null_value_counts()
                .as_ref()
                .and_then(|x| x.get(&id))
                .map(|x| Precision::Exact(*x as usize))
                .unwrap_or(Precision::Absent),
            max_value: data_file
                .upper_bounds()
                .as_ref()
                .and_then(|x| x.get(&id))
                .and_then(|x| {
                    Some(Precision::Exact(
                        convert_value_to_scalar_value(x.clone()).ok()?,
                    ))
                })
                .unwrap_or(Precision::Absent),
            min_value: data_file
                .lower_bounds()
                .as_ref()
                .and_then(|x| x.get(&id))
                .and_then(|x| {
                    Some(Precision::Exact(
                        convert_value_to_scalar_value(x.clone()).ok()?,
                    ))
                })
                .unwrap_or(Precision::Absent),
            distinct_count: Precision::Absent,
            sum_value: Precision::Absent,
        }
    })
}

pub(crate) fn manifest_statistics(schema: &Schema, manifest: &ManifestEntry) -> Statistics {
    Statistics {
        num_rows: Precision::Exact(*manifest.data_file().record_count() as usize),
        total_byte_size: Precision::Exact(*manifest.data_file().file_size_in_bytes() as usize),
        column_statistics: column_statistics(schema, manifest).collect(),
    }
}

fn convert_value_to_scalar_value(value: Value) -> Result<ScalarValue, Error> {
    match value {
        Value::Boolean(b) => Ok(ScalarValue::Boolean(Some(b))),
        Value::Int(i) => Ok(ScalarValue::Int32(Some(i))),
        Value::LongInt(l) => Ok(ScalarValue::Int64(Some(l))),
        Value::Float(f) => Ok(ScalarValue::Float32(Some(f.0))),
        Value::Double(d) => Ok(ScalarValue::Float64(Some(d.0))),
        Value::Date(d) => Ok(ScalarValue::Date32(Some(d))),
        Value::Time(t) => Ok(ScalarValue::Time64Microsecond(Some(t))),
        Value::Timestamp(ts) => Ok(ScalarValue::TimestampMicrosecond(Some(ts), None)),
        Value::TimestampTZ(ts) => Ok(ScalarValue::TimestampMicrosecond(Some(ts), None)),
        Value::String(s) => Ok(ScalarValue::Utf8(Some(s))),
        Value::UUID(u) => Ok(ScalarValue::FixedSizeBinary(
            16,
            Some(u.into_bytes().into()),
        )),
        Value::Fixed(size, data) => Ok(ScalarValue::FixedSizeBinary(size as i32, Some(data))),
        Value::Binary(data) => Ok(ScalarValue::Binary(Some(data))),
        Value::Decimal(decimal) => Ok(ScalarValue::Decimal128(
            Some(decimal.try_into().unwrap()),
            0,
            0,
        )),
        x => Err(Error::Conversion(
            "Iceberg value".to_string(),
            format!("{x:?}"),
        )),
    }
}

fn new_distinct_count(acc: &ColumnStatistics, x: &ColumnStatistics) -> Precision<usize> {
    match (
        &acc.distinct_count,
        &x.distinct_count,
        &acc.min_value,
        &acc.max_value,
        &x.min_value,
        &x.max_value,
    ) {
        (
            Precision::Exact(old_count),
            Precision::Exact(new_count),
            Precision::Exact(ScalarValue::Int32(Some(old_min))),
            Precision::Exact(ScalarValue::Int32(Some(old_max))),
            Precision::Exact(ScalarValue::Int32(Some(new_min))),
            Precision::Exact(ScalarValue::Int32(Some(new_max))),
        ) => {
            let estimated = estimate_distinct_count(
                &[old_min, old_max],
                &[new_min, new_max],
                *old_count as i64,
                *new_count as i64,
            );
            Precision::Inexact(*old_count + estimated as usize)
        }
        (
            Precision::Exact(old_count),
            Precision::Exact(new_count),
            Precision::Exact(ScalarValue::Int64(Some(old_min))),
            Precision::Exact(ScalarValue::Int64(Some(old_max))),
            Precision::Exact(ScalarValue::Int64(Some(new_min))),
            Precision::Exact(ScalarValue::Int64(Some(new_max))),
        ) => {
            let estimated = estimate_distinct_count(
                &[old_min, old_max],
                &[new_min, new_max],
                *old_count as i64,
                *new_count as i64,
            );
            Precision::Inexact(*old_count + estimated as usize)
        }
        (Precision::Absent, Precision::Exact(_), _, _, _, _) => x.distinct_count,
        _ => acc.distinct_count.add(&x.distinct_count),
    }
}