Skip to main content

deltalake_core/writer/
stats.rs

1use std::ops::Not;
2use std::sync::Arc;
3use std::time::{SystemTime, UNIX_EPOCH};
4use std::{
5    collections::{HashMap, HashSet},
6    ops::AddAssign,
7};
8
9use delta_kernel::expressions::Scalar;
10use delta_kernel::table_properties::DataSkippingNumIndexedCols;
11use indexmap::IndexMap;
12use itertools::Itertools;
13use parquet::basic::LogicalType;
14use parquet::basic::Type;
15use parquet::file::metadata::ParquetMetaData;
16use parquet::schema::types::{ColumnDescriptor, SchemaDescriptor};
17use parquet::{
18    basic::TimeUnit,
19    file::{metadata::RowGroupMetaData, statistics::Statistics},
20};
21use tracing::warn;
22
23use super::*;
24use crate::kernel::{Add, scalars::ScalarExt};
25use crate::protocol::{ColumnValueStat, Stats};
26
27/// Creates an [`Add`] log action struct.
28pub(crate) fn create_add(
29    partition_values: &IndexMap<String, Scalar>,
30    path: String,
31    size: i64,
32    file_metadata: &ParquetMetaData,
33    num_indexed_cols: DataSkippingNumIndexedCols,
34    stats_columns: &Option<Vec<impl AsRef<str>>>,
35) -> Result<Add, DeltaTableError> {
36    let stats = stats_from_file_metadata(
37        partition_values,
38        file_metadata,
39        num_indexed_cols,
40        stats_columns,
41    )?;
42    let stats_string = serde_json::to_string(&stats)?;
43
44    // Determine the modification timestamp to include in the add action - milliseconds since epoch
45    // Err should be impossible in this case since `SystemTime::now()` is always greater than `UNIX_EPOCH`
46    let modification_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
47    let modification_time = modification_time.as_millis() as i64;
48
49    Ok(Add {
50        path,
51        size,
52        partition_values: partition_values
53            .iter()
54            .map(|(k, v)| {
55                (
56                    k.clone(),
57                    if v.is_null() {
58                        None
59                    } else {
60                        Some(v.serialize())
61                    },
62                )
63            })
64            .collect(),
65        modification_time,
66        data_change: true,
67        stats: Some(stats_string),
68        tags: None,
69        deletion_vector: None,
70        base_row_id: None,
71        default_row_commit_version: None,
72        clustering_provider: None,
73    })
74}
75
76// As opposed to `stats_from_file_metadata` which operates on `parquet::format::FileMetaData`,
77// this function produces the stats by reading the metadata from already written out files.
78//
79// Note that the file metadata used here is actually `parquet::file::metadata::FileMetaData`
80// which is a thrift decoding of the `parquet::format::FileMetaData` which is typically obtained
81// when flushing the write.
82pub(crate) fn stats_from_parquet_metadata(
83    partition_values: &IndexMap<String, Scalar>,
84    parquet_metadata: &ParquetMetaData,
85    num_indexed_cols: DataSkippingNumIndexedCols,
86    stats_columns: &Option<Vec<String>>,
87) -> Result<Stats, DeltaWriterError> {
88    let num_rows = parquet_metadata.file_metadata().num_rows();
89    let schema_descriptor = parquet_metadata.file_metadata().schema_descr_ptr();
90    let row_group_metadata = parquet_metadata.row_groups().to_vec();
91
92    stats_from_metadata(
93        partition_values,
94        schema_descriptor,
95        row_group_metadata,
96        num_rows,
97        num_indexed_cols,
98        stats_columns,
99    )
100}
101
102fn stats_from_file_metadata(
103    partition_values: &IndexMap<String, Scalar>,
104    file_metadata: &ParquetMetaData,
105    num_indexed_cols: DataSkippingNumIndexedCols,
106    stats_columns: &Option<Vec<impl AsRef<str>>>,
107) -> Result<Stats, DeltaWriterError> {
108    let schema_descriptor = file_metadata.file_metadata().schema_descr();
109
110    let row_group_metadata: Vec<RowGroupMetaData> = file_metadata.row_groups().to_vec();
111
112    stats_from_metadata(
113        partition_values,
114        Arc::new(schema_descriptor.clone()),
115        row_group_metadata,
116        file_metadata.file_metadata().num_rows(),
117        num_indexed_cols,
118        stats_columns,
119    )
120}
121
122fn stats_from_metadata(
123    partition_values: &IndexMap<String, Scalar>,
124    schema_descriptor: Arc<SchemaDescriptor>,
125    row_group_metadata: Vec<RowGroupMetaData>,
126    num_rows: i64,
127    num_indexed_cols: DataSkippingNumIndexedCols,
128    stats_columns: &Option<Vec<impl AsRef<str>>>,
129) -> Result<Stats, DeltaWriterError> {
130    let mut min_values: HashMap<String, ColumnValueStat> = HashMap::new();
131    let mut max_values: HashMap<String, ColumnValueStat> = HashMap::new();
132    let mut null_count: HashMap<String, ColumnCountStat> = HashMap::new();
133    let dialect = sqlparser::dialect::GenericDialect {};
134
135    let idx_to_iterate = if let Some(stats_cols) = stats_columns {
136        let stats_cols = stats_cols
137            .iter()
138            .map(|v| {
139                match sqlparser::parser::Parser::new(&dialect)
140                    .try_with_sql(v.as_ref())
141                    .map_err(|e| DeltaTableError::generic(e.to_string()))?
142                    .parse_multipart_identifier()
143                {
144                    Ok(parts) => Ok(parts.into_iter().map(|v| v.value).join(".")),
145                    Err(e) => Err(DeltaWriterError::DeltaTable(
146                        DeltaTableError::GenericError {
147                            source: Box::new(e),
148                        },
149                    )),
150                }
151            })
152            .collect::<Result<Vec<String>, DeltaWriterError>>()?;
153
154        schema_descriptor
155            .columns()
156            .iter()
157            .enumerate()
158            .filter_map(|(index, col)| {
159                if stats_cols.contains(&col.name().to_string()) {
160                    Some(index)
161                } else {
162                    None
163                }
164            })
165            .collect()
166    } else if num_indexed_cols == DataSkippingNumIndexedCols::AllColumns {
167        (0..schema_descriptor.num_columns()).collect::<Vec<_>>()
168    } else if let DataSkippingNumIndexedCols::NumColumns(n_cols) = num_indexed_cols {
169        // The `delta.dataSkippingNumIndexedCols` budget is consumed by distinct
170        // top-level fields, not by parquet leaf columns. A single top-level
171        // column with many nested fields therefore takes one slot, not N.
172        // Partition columns do not consume a slot.
173        let limit = n_cols as usize;
174        let mut admitted: HashSet<String> = HashSet::new();
175        let mut admitted_count: usize = 0;
176        let mut idxs: Vec<usize> = Vec::new();
177        for (idx, col) in schema_descriptor.columns().iter().enumerate() {
178            let top = match col.path().parts().first() {
179                Some(t) => t.clone(),
180                None => continue,
181            };
182            if partition_values.contains_key(&top) {
183                continue;
184            }
185            if !admitted.contains(&top) {
186                if admitted_count >= limit {
187                    break;
188                }
189                admitted.insert(top);
190                admitted_count += 1;
191            }
192            idxs.push(idx);
193        }
194        idxs
195    } else {
196        return Err(DeltaWriterError::DeltaTable(DeltaTableError::Generic(
197            "delta.dataSkippingNumIndexedCols valid values are >=-1".to_string(),
198        )));
199    };
200
201    for idx in idx_to_iterate {
202        let column_descr = schema_descriptor.column(idx);
203
204        let column_path = column_descr.path();
205        let column_path_parts = column_path.parts();
206
207        // Do not include partition columns in statistics (still relevant for
208        // the `AllColumns` and explicit `stats_columns` branches).
209        if partition_values.contains_key(&column_path_parts[0]) {
210            continue;
211        }
212
213        let maybe_stats: Option<AggregatedStats> = row_group_metadata
214            .iter()
215            .flat_map(|g| {
216                g.column(idx).statistics().into_iter().filter_map(|s| {
217                    let is_binary = matches!(&column_descr.physical_type(), Type::BYTE_ARRAY)
218                        && matches!(column_descr.logical_type_ref(), Some(LogicalType::String))
219                            .not();
220                    if is_binary {
221                        warn!(
222                            "Skipping column {} because it's a binary field.",
223                            &column_descr.name().to_string()
224                        );
225                        None
226                    } else {
227                        Some(AggregatedStats::from((s, column_descr.logical_type_ref())))
228                    }
229                })
230            })
231            .reduce(|mut left, right| {
232                left += right;
233                left
234            });
235
236        if let Some(stats) = maybe_stats {
237            apply_min_max_for_column(
238                stats,
239                column_descr.clone(),
240                column_descr.path().parts(),
241                &mut min_values,
242                &mut max_values,
243                &mut null_count,
244            )?;
245        }
246    }
247
248    Ok(Stats {
249        min_values,
250        max_values,
251        num_records: num_rows,
252        null_count,
253    })
254}
255
256/// Logical scalars extracted from statistics. These are used to aggregate
257/// minimums and maximums. We can't use the physical scalars because they
258/// are not ordered correctly for some types. For example, decimals are stored
259/// as fixed length binary, and can't be sorted leixcographically.
260#[derive(Debug, Clone, PartialEq, PartialOrd)]
261enum StatsScalar {
262    Boolean(bool),
263    Int32(i32),
264    Int64(i64),
265    Float32(f32),
266    Float64(f64),
267    Date(chrono::NaiveDate),
268    Timestamp(chrono::NaiveDateTime),
269    // We are serializing to f64 later and the ordering should be the same
270    // Scale is stored to handle scale=0 serialization correctly
271    Decimal { value: f64, scale: i32 },
272    String(String),
273    Bytes(Vec<u8>),
274    Uuid(uuid::Uuid),
275}
276
277impl StatsScalar {
278    fn try_from_stats(
279        stats: &Statistics,
280        logical_type: Option<&LogicalType>,
281        use_min: bool,
282    ) -> Result<Self, DeltaWriterError> {
283        macro_rules! get_stat {
284            ($val: expr) => {
285                if use_min {
286                    *$val.min_opt().unwrap()
287                } else {
288                    *$val.max_opt().unwrap()
289                }
290            };
291        }
292
293        match (stats, logical_type) {
294            (Statistics::Boolean(v), _) => Ok(Self::Boolean(get_stat!(v))),
295            // Int32 can be date, decimal, or just int32
296            (Statistics::Int32(v), Some(LogicalType::Date)) => {
297                let epoch_start = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); // creating from epoch should be infallible
298                let date = epoch_start + chrono::Duration::days(get_stat!(v) as i64);
299                Ok(Self::Date(date))
300            }
301            (Statistics::Int32(v), Some(LogicalType::Decimal { scale, .. })) => {
302                let val = get_stat!(v) as f64 / 10.0_f64.powi(*scale);
303                // Spark serializes these as numbers
304                Ok(Self::Decimal {
305                    value: val,
306                    scale: *scale,
307                })
308            }
309            (Statistics::Int32(v), _) => Ok(Self::Int32(get_stat!(v))),
310            // Int64 can be timestamp, decimal, or integer
311            (Statistics::Int64(v), Some(LogicalType::Timestamp { unit, .. })) => {
312                // For now, we assume timestamps are adjusted to UTC. Non-UTC timestamps
313                // are behind a feature gate in Delta:
314                // https://github.com/delta-io/delta/blob/master/PROTOCOL.md#timestamp-without-timezone-timestampntz
315                let v = get_stat!(v);
316                let timestamp = match unit {
317                    TimeUnit::MILLIS => chrono::DateTime::from_timestamp_millis(v),
318                    TimeUnit::MICROS => chrono::DateTime::from_timestamp_micros(v),
319                    TimeUnit::NANOS => {
320                        let secs = v / 1_000_000_000;
321                        let nanosecs = (v % 1_000_000_000) as u32;
322                        chrono::DateTime::from_timestamp(secs, nanosecs)
323                    }
324                };
325                let timestamp = timestamp.ok_or(DeltaWriterError::StatsParsingFailed {
326                    debug_value: v.to_string(),
327                    logical_type: logical_type.cloned(),
328                })?;
329                Ok(Self::Timestamp(timestamp.naive_utc()))
330            }
331            (Statistics::Int64(v), Some(LogicalType::Decimal { scale, .. })) => {
332                let val = get_stat!(v) as f64 / 10.0_f64.powi(*scale);
333                // Spark serializes these as numbers
334                Ok(Self::Decimal {
335                    value: val,
336                    scale: *scale,
337                })
338            }
339            (Statistics::Int64(v), _) => Ok(Self::Int64(get_stat!(v))),
340            (Statistics::Float(v), _) => Ok(Self::Float32(get_stat!(v))),
341            (Statistics::Double(v), _) => Ok(Self::Float64(get_stat!(v))),
342            (Statistics::ByteArray(v), logical_type) => {
343                let bytes = if use_min {
344                    v.min_bytes_opt()
345                } else {
346                    v.max_bytes_opt()
347                }
348                .unwrap_or_default();
349                match logical_type {
350                    None => Ok(Self::Bytes(bytes.to_vec())),
351                    Some(LogicalType::String) => {
352                        Ok(Self::String(String::from_utf8(bytes.to_vec()).map_err(
353                            |_| DeltaWriterError::StatsParsingFailed {
354                                debug_value: format!("{bytes:?}"),
355                                logical_type: Some(LogicalType::String),
356                            },
357                        )?))
358                    }
359                    _ => Err(DeltaWriterError::StatsParsingFailed {
360                        debug_value: format!("{bytes:?}"),
361                        logical_type: logical_type.cloned(),
362                    }),
363                }
364            }
365            (Statistics::FixedLenByteArray(v), Some(LogicalType::Decimal { scale, precision })) => {
366                let val = if use_min {
367                    v.min_bytes_opt()
368                } else {
369                    v.max_bytes_opt()
370                }
371                .unwrap_or_default();
372
373                let val = if val.len() <= 16 {
374                    i128::from_be_bytes(sign_extend_be(val)) as f64
375                } else {
376                    return Err(DeltaWriterError::StatsParsingFailed {
377                        debug_value: format!("{val:?}"),
378                        logical_type: Some(LogicalType::Decimal {
379                            scale: *scale,
380                            precision: *precision,
381                        }),
382                    });
383                };
384
385                let mut val = val / 10.0_f64.powi(*scale);
386
387                if val.is_normal()
388                    && (val.trunc() as i128).to_string().len() > (precision - scale) as usize
389                {
390                    // For normal values with integer parts that get rounded to a number beyond
391                    // the precision - scale range take the next smaller (by magnitude) value
392                    val = f64::from_bits(val.to_bits() - 1);
393                }
394
395                Ok(Self::Decimal {
396                    value: val,
397                    scale: *scale,
398                })
399            }
400            (Statistics::FixedLenByteArray(v), Some(LogicalType::Uuid)) => {
401                let val = if use_min {
402                    v.min_bytes_opt()
403                } else {
404                    v.max_bytes_opt()
405                }
406                .unwrap_or_default();
407
408                if val.len() != 16 {
409                    return Err(DeltaWriterError::StatsParsingFailed {
410                        debug_value: format!("{val:?}"),
411                        logical_type: Some(LogicalType::Uuid),
412                    });
413                }
414
415                let mut bytes = [0; 16];
416                bytes.copy_from_slice(val);
417
418                let val = uuid::Uuid::from_bytes(bytes);
419                Ok(Self::Uuid(val))
420            }
421            (stats, _) => Err(DeltaWriterError::StatsParsingFailed {
422                debug_value: format!("{stats:?}"),
423                logical_type: logical_type.cloned(),
424            }),
425        }
426    }
427}
428
429/// Performs big endian sign extension
430/// Copied from arrow-rs repo/parquet crate:
431/// https://github.com/apache/arrow-rs/blob/b25c441745602c9967b1e3cc4a28bc469cfb1311/parquet/src/arrow/buffer/bit_util.rs#L54
432pub fn sign_extend_be<const N: usize>(b: &[u8]) -> [u8; N] {
433    assert!(b.len() <= N, "Array too large, expected less than {N}");
434    let is_negative = (b[0] & 128u8) == 128u8;
435    let mut result = if is_negative { [255u8; N] } else { [0u8; N] };
436    for (d, s) in result.iter_mut().skip(N - b.len()).zip(b) {
437        *d = *s;
438    }
439    result
440}
441
442impl From<StatsScalar> for serde_json::Value {
443    fn from(scalar: StatsScalar) -> Self {
444        match scalar {
445            StatsScalar::Boolean(v) => serde_json::Value::Bool(v),
446            StatsScalar::Int32(v) => serde_json::Value::from(v),
447            StatsScalar::Int64(v) => serde_json::Value::from(v),
448            StatsScalar::Float32(v) => serde_json::Value::from(v),
449            StatsScalar::Float64(v) => serde_json::Value::from(v),
450            StatsScalar::Date(v) => serde_json::Value::from(v.format("%Y-%m-%d").to_string()),
451            StatsScalar::Timestamp(v) => {
452                serde_json::Value::from(v.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string())
453            }
454            StatsScalar::Decimal { value, scale } => {
455                // For scale=0, serialize as integer since serde_json would otherwise
456                // serialize f64 as "1234.0" instead of "1234"
457                if scale == 0 {
458                    serde_json::Value::from(value.round() as i64)
459                } else {
460                    serde_json::Value::from(value)
461                }
462            }
463            StatsScalar::String(v) => serde_json::Value::from(v),
464            StatsScalar::Bytes(v) => {
465                let escaped_bytes = v
466                    .into_iter()
467                    .flat_map(std::ascii::escape_default)
468                    .collect::<Vec<u8>>();
469                let escaped_string = String::from_utf8(escaped_bytes).unwrap();
470                serde_json::Value::from(escaped_string)
471            }
472            StatsScalar::Uuid(v) => serde_json::Value::from(v.hyphenated().to_string()),
473        }
474    }
475}
476
477/// Aggregated stats
478struct AggregatedStats {
479    pub min: Option<StatsScalar>,
480    pub max: Option<StatsScalar>,
481    pub null_count: u64,
482}
483
484impl From<(&Statistics, Option<&LogicalType>)> for AggregatedStats {
485    fn from(value: (&Statistics, Option<&LogicalType>)) -> Self {
486        let (stats, logical_type) = value;
487        let null_count = stats.null_count_opt().unwrap_or_default();
488        if stats.min_bytes_opt().is_some() && stats.max_bytes_opt().is_some() {
489            let min = StatsScalar::try_from_stats(stats, logical_type, true).ok();
490            let max = StatsScalar::try_from_stats(stats, logical_type, false).ok();
491            Self {
492                min,
493                max,
494                null_count,
495            }
496        } else {
497            Self {
498                min: None,
499                max: None,
500                null_count,
501            }
502        }
503    }
504}
505
506impl AddAssign for AggregatedStats {
507    fn add_assign(&mut self, rhs: Self) {
508        self.min = match (self.min.take(), rhs.min) {
509            (Some(lhs), Some(rhs)) => {
510                if lhs < rhs {
511                    Some(lhs)
512                } else {
513                    Some(rhs)
514                }
515            }
516            (lhs, rhs) => lhs.or(rhs),
517        };
518        self.max = match (self.max.take(), rhs.max) {
519            (Some(lhs), Some(rhs)) => {
520                if lhs > rhs {
521                    Some(lhs)
522                } else {
523                    Some(rhs)
524                }
525            }
526            (lhs, rhs) => lhs.or(rhs),
527        };
528
529        self.null_count += rhs.null_count;
530    }
531}
532
533/// For a list field, we don't want the inner field names. We need to chuck out
534/// the list and items fields from the path, but also need to handle the
535/// peculiar case where the user named the list field "list" or "item".
536///
537/// NOTE: As of delta_kernel 0.3.1 the name switched from `item` to `element` to line up with the
538/// parquet spec, see
539/// [here](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists)
540///
541/// For example:
542///
543/// * ["some_nested_list", "list", "item", "list", "item"] -> "some_nested_list"
544/// * ["some_list", "list", "item"] -> "some_list"
545/// * ["list", "list", "item"] -> "list"
546/// * ["item", "list", "item"] -> "item"
547fn get_list_field_name(column_descr: &Arc<ColumnDescriptor>) -> Option<String> {
548    let max_rep_levels = column_descr.max_rep_level();
549    let column_path_parts = column_descr.path().parts();
550
551    // If there are more nested names, we can't handle them yet.
552    if column_path_parts.len() > (2 * max_rep_levels + 1) as usize {
553        return None;
554    }
555
556    let mut column_path_parts = column_path_parts.to_vec();
557    let mut items_seen = 0;
558    let mut lists_seen = 0;
559    while let Some(part) = column_path_parts.pop() {
560        match (part.as_str(), lists_seen, items_seen) {
561            ("list", seen, _) if seen == max_rep_levels => return Some("list".to_string()),
562            ("element", _, seen) if seen == max_rep_levels => return Some("element".to_string()),
563            ("list", _, _) => lists_seen += 1,
564            ("element", _, _) => items_seen += 1,
565            (other, _, _) => return Some(other.to_string()),
566        }
567    }
568    None
569}
570
571fn apply_min_max_for_column(
572    statistics: AggregatedStats,
573    column_descr: Arc<ColumnDescriptor>,
574    column_path_parts: &[String],
575    min_values: &mut HashMap<String, ColumnValueStat>,
576    max_values: &mut HashMap<String, ColumnValueStat>,
577    null_counts: &mut HashMap<String, ColumnCountStat>,
578) -> Result<(), DeltaWriterError> {
579    // Special handling for list column
580    if column_descr.max_rep_level() > 0 {
581        let key = get_list_field_name(&column_descr);
582
583        if let Some(key) = key {
584            null_counts.insert(key, ColumnCountStat::Value(statistics.null_count as i64));
585        }
586
587        return Ok(());
588    }
589
590    match (column_path_parts.len(), column_path_parts.first()) {
591        // Base case - we are at the leaf struct level in the path
592        (1, _) => {
593            let key = column_descr.name().to_string();
594
595            if let Some(min) = statistics.min {
596                let min = ColumnValueStat::Value(min.into());
597                min_values.insert(key.clone(), min);
598            }
599
600            if let Some(max) = statistics.max {
601                let max = ColumnValueStat::Value(max.into());
602                max_values.insert(key.clone(), max);
603            }
604
605            null_counts.insert(key, ColumnCountStat::Value(statistics.null_count as i64));
606
607            Ok(())
608        }
609        // Recurse to load value at the appropriate level of HashMap
610        (_, Some(key)) => {
611            let child_min_values = min_values
612                .entry(key.to_owned())
613                .or_insert_with(|| ColumnValueStat::Column(HashMap::new()));
614            let child_max_values = max_values
615                .entry(key.to_owned())
616                .or_insert_with(|| ColumnValueStat::Column(HashMap::new()));
617            let child_null_counts = null_counts
618                .entry(key.to_owned())
619                .or_insert_with(|| ColumnCountStat::Column(HashMap::new()));
620
621            match (child_min_values, child_max_values, child_null_counts) {
622                (
623                    ColumnValueStat::Column(mins),
624                    ColumnValueStat::Column(maxes),
625                    ColumnCountStat::Column(null_counts),
626                ) => {
627                    let remaining_parts: Vec<String> = column_path_parts
628                        .iter()
629                        .skip(1)
630                        .map(|s| s.to_string())
631                        .collect();
632
633                    apply_min_max_for_column(
634                        statistics,
635                        column_descr,
636                        remaining_parts.as_slice(),
637                        mins,
638                        maxes,
639                        null_counts,
640                    )?;
641
642                    Ok(())
643                }
644                _ => {
645                    unreachable!();
646                }
647            }
648        }
649        // column path parts will always have at least one element.
650        (_, None) => {
651            unreachable!();
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::utils::record_batch_from_message;
659    use super::*;
660    use crate::{
661        DeltaTable,
662        errors::DeltaTableError,
663        protocol::{ColumnCountStat, ColumnValueStat},
664        table::builder::DeltaTableBuilder,
665    };
666    use parquet::data_type::{ByteArray, FixedLenByteArray};
667    use parquet::file::statistics::ValueStatistics;
668    use parquet::{basic::Compression, file::properties::WriterProperties};
669    use serde_json::{Value, json};
670    use std::collections::HashMap;
671    use std::path::Path;
672    use std::sync::LazyLock;
673    use url::Url;
674
675    macro_rules! simple_parquet_stat {
676        ($variant:expr, $value:expr) => {
677            $variant(ValueStatistics::new(
678                Some($value),
679                Some($value),
680                None,
681                Some(0),
682                false,
683            ))
684        };
685    }
686
687    #[test]
688    fn test_stats_scalar_serialization() {
689        let cases = &[
690            (
691                simple_parquet_stat!(Statistics::Boolean, true),
692                Some(LogicalType::Integer {
693                    bit_width: 1,
694                    is_signed: true,
695                }),
696                Value::Bool(true),
697            ),
698            (
699                simple_parquet_stat!(Statistics::Int32, 1),
700                Some(LogicalType::Integer {
701                    bit_width: 32,
702                    is_signed: true,
703                }),
704                Value::from(1),
705            ),
706            (
707                simple_parquet_stat!(Statistics::Int32, 1234),
708                Some(LogicalType::Decimal {
709                    scale: 3,
710                    precision: 4,
711                }),
712                Value::from(1.234),
713            ),
714            (
715                simple_parquet_stat!(Statistics::Int32, 1234),
716                Some(LogicalType::Decimal {
717                    scale: -1,
718                    precision: 4,
719                }),
720                Value::from(12340.0),
721            ),
722            (
723                simple_parquet_stat!(Statistics::Int32, 1234),
724                Some(LogicalType::Decimal {
725                    scale: 0,
726                    precision: 4,
727                }),
728                Value::from(1234),
729            ),
730            (
731                simple_parquet_stat!(Statistics::Int32, 10561),
732                Some(LogicalType::Date),
733                Value::from("1998-12-01"),
734            ),
735            (
736                simple_parquet_stat!(Statistics::Int64, 1641040496789123456),
737                Some(LogicalType::Timestamp {
738                    is_adjusted_to_u_t_c: true,
739                    unit: parquet::basic::TimeUnit::NANOS,
740                }),
741                Value::from("2022-01-01T12:34:56.789123456Z"),
742            ),
743            (
744                simple_parquet_stat!(Statistics::Int64, 1641040496789123),
745                Some(LogicalType::Timestamp {
746                    is_adjusted_to_u_t_c: true,
747                    unit: parquet::basic::TimeUnit::MICROS,
748                }),
749                Value::from("2022-01-01T12:34:56.789123Z"),
750            ),
751            (
752                simple_parquet_stat!(Statistics::Int64, 1641040496789),
753                Some(LogicalType::Timestamp {
754                    is_adjusted_to_u_t_c: true,
755                    unit: parquet::basic::TimeUnit::MILLIS,
756                }),
757                Value::from("2022-01-01T12:34:56.789Z"),
758            ),
759            (
760                simple_parquet_stat!(Statistics::Int64, 1234),
761                Some(LogicalType::Decimal {
762                    scale: 3,
763                    precision: 4,
764                }),
765                Value::from(1.234),
766            ),
767            (
768                simple_parquet_stat!(Statistics::Int64, 1234),
769                Some(LogicalType::Decimal {
770                    scale: -1,
771                    precision: 4,
772                }),
773                Value::from(12340.0),
774            ),
775            (
776                simple_parquet_stat!(Statistics::Int64, 1234),
777                Some(LogicalType::Decimal {
778                    scale: 0,
779                    precision: 4,
780                }),
781                Value::from(1234),
782            ),
783            (
784                simple_parquet_stat!(Statistics::Int64, 1234),
785                None,
786                Value::from(1234),
787            ),
788            (
789                simple_parquet_stat!(Statistics::ByteArray, ByteArray::from(b"hello".to_vec())),
790                Some(LogicalType::String),
791                Value::from("hello"),
792            ),
793            (
794                simple_parquet_stat!(Statistics::ByteArray, ByteArray::from(b"\x00\\".to_vec())),
795                None,
796                Value::from("\\x00\\\\"),
797            ),
798            (
799                simple_parquet_stat!(
800                    Statistics::FixedLenByteArray,
801                    FixedLenByteArray::from(1243124142314423i128.to_be_bytes().to_vec())
802                ),
803                Some(LogicalType::Decimal {
804                    scale: 3,
805                    precision: 16,
806                }),
807                Value::from(1243124142314.423),
808            ),
809            (
810                simple_parquet_stat!(
811                    Statistics::FixedLenByteArray,
812                    FixedLenByteArray::from(vec![0, 39, 16])
813                ),
814                Some(LogicalType::Decimal {
815                    scale: 3,
816                    precision: 5,
817                }),
818                Value::from(10.0),
819            ),
820            (
821                simple_parquet_stat!(
822                    Statistics::FixedLenByteArray,
823                    FixedLenByteArray::from(1234i128.to_be_bytes().to_vec())
824                ),
825                Some(LogicalType::Decimal {
826                    scale: 0,
827                    precision: 4,
828                }),
829                Value::from(1234),
830            ),
831            (
832                simple_parquet_stat!(
833                    Statistics::FixedLenByteArray,
834                    FixedLenByteArray::from(vec![
835                        75, 59, 76, 168, 90, 134, 196, 122, 9, 138, 34, 63, 255, 255, 255, 255
836                    ])
837                ),
838                Some(LogicalType::Decimal {
839                    scale: 6,
840                    precision: 38,
841                }),
842                Value::from(9.999999999999999e31),
843            ),
844            (
845                simple_parquet_stat!(
846                    Statistics::FixedLenByteArray,
847                    FixedLenByteArray::from(vec![
848                        180, 196, 179, 87, 165, 121, 59, 133, 246, 117, 221, 192, 0, 0, 0, 1
849                    ])
850                ),
851                Some(LogicalType::Decimal {
852                    scale: 6,
853                    precision: 38,
854                }),
855                Value::from(-9.999999999999999e31),
856            ),
857            (
858                simple_parquet_stat!(
859                    Statistics::FixedLenByteArray,
860                    FixedLenByteArray::from(
861                        [
862                            0xc2, 0xe8, 0xc7, 0xf7, 0xd1, 0xf9, 0x4b, 0x49, 0xa5, 0xd9, 0x4b, 0xfe,
863                            0x75, 0xc3, 0x17, 0xe2
864                        ]
865                        .to_vec()
866                    )
867                ),
868                Some(LogicalType::Uuid),
869                Value::from("c2e8c7f7-d1f9-4b49-a5d9-4bfe75c317e2"),
870            ),
871        ];
872
873        for (stats, logical_type, expected) in cases {
874            let scalar = StatsScalar::try_from_stats(stats, logical_type.as_ref(), true).unwrap();
875            let actual = serde_json::Value::from(scalar);
876            assert_eq!(&actual, expected);
877        }
878    }
879
880    #[tokio::test]
881    async fn test_delta_stats() {
882        let temp_dir = tempfile::tempdir().unwrap();
883        let table_path = temp_dir.path();
884        create_temp_table(table_path);
885
886        let table_uri = Url::from_directory_path(table_path).unwrap();
887        let table = load_table(&table_uri, HashMap::new()).await.unwrap();
888
889        let mut writer = RecordBatchWriter::for_table(&table).unwrap();
890        writer = writer.with_writer_properties(
891            WriterProperties::builder()
892                .set_compression(Compression::SNAPPY)
893                .set_max_row_group_row_count(Some(128))
894                .build(),
895        );
896
897        let arrow_schema = writer.arrow_schema();
898        let batch = record_batch_from_message(arrow_schema, JSON_ROWS.clone().as_ref()).unwrap();
899
900        writer.write(batch).await.unwrap();
901        let add = writer.flush().await.unwrap();
902        assert_eq!(add.len(), 1);
903        let stats = add[0].get_stats().unwrap().unwrap();
904
905        let min_max_keys = vec!["meta", "some_int", "some_string", "some_bool", "uuid"];
906        let mut null_count_keys = vec!["some_list", "some_nested_list"];
907        null_count_keys.extend_from_slice(min_max_keys.as_slice());
908
909        assert_eq!(
910            min_max_keys.len(),
911            stats.min_values.len(),
912            "min values don't match"
913        );
914        assert_eq!(
915            min_max_keys.len(),
916            stats.max_values.len(),
917            "max values don't match"
918        );
919        assert_eq!(
920            null_count_keys.len(),
921            stats.null_count.len(),
922            "null counts don't match"
923        );
924
925        // assert on min values
926        for (k, v) in stats.min_values.iter() {
927            match (k.as_str(), v) {
928                ("meta", ColumnValueStat::Column(map)) => {
929                    assert_eq!(2, map.len());
930
931                    let kafka = map.get("kafka").unwrap().as_column().unwrap();
932                    assert_eq!(3, kafka.len());
933                    let partition = kafka.get("partition").unwrap().as_value().unwrap();
934                    assert_eq!(0, partition.as_i64().unwrap());
935
936                    let producer = map.get("producer").unwrap().as_column().unwrap();
937                    assert_eq!(1, producer.len());
938                    let timestamp = producer.get("timestamp").unwrap().as_value().unwrap();
939                    assert_eq!("2021-06-22", timestamp.as_str().unwrap());
940                }
941                ("some_int", ColumnValueStat::Value(v)) => assert_eq!(302, v.as_i64().unwrap()),
942                ("some_bool", ColumnValueStat::Value(v)) => assert!(!v.as_bool().unwrap()),
943                ("some_string", ColumnValueStat::Value(v)) => {
944                    assert_eq!("GET", v.as_str().unwrap())
945                }
946                ("date", ColumnValueStat::Value(v)) => {
947                    assert_eq!("2021-06-22", v.as_str().unwrap())
948                }
949                ("uuid", ColumnValueStat::Value(v)) => {
950                    assert_eq!("176c770d-92af-4a21-bf76-5d8c5261d659", v.as_str().unwrap())
951                }
952                k => panic!("Key {k:?} should not be present in min_values"),
953            }
954        }
955
956        // assert on max values
957        for (k, v) in stats.max_values.iter() {
958            match (k.as_str(), v) {
959                ("meta", ColumnValueStat::Column(map)) => {
960                    assert_eq!(2, map.len());
961
962                    let kafka = map.get("kafka").unwrap().as_column().unwrap();
963                    assert_eq!(3, kafka.len());
964                    let partition = kafka.get("partition").unwrap().as_value().unwrap();
965                    assert_eq!(1, partition.as_i64().unwrap());
966
967                    let producer = map.get("producer").unwrap().as_column().unwrap();
968                    assert_eq!(1, producer.len());
969                    let timestamp = producer.get("timestamp").unwrap().as_value().unwrap();
970                    assert_eq!("2021-06-22", timestamp.as_str().unwrap());
971                }
972                ("some_int", ColumnValueStat::Value(v)) => assert_eq!(400, v.as_i64().unwrap()),
973                ("some_bool", ColumnValueStat::Value(v)) => assert!(v.as_bool().unwrap()),
974                ("some_string", ColumnValueStat::Value(v)) => {
975                    assert_eq!("PUT", v.as_str().unwrap())
976                }
977                ("date", ColumnValueStat::Value(v)) => {
978                    assert_eq!("2021-06-22", v.as_str().unwrap())
979                }
980                ("uuid", ColumnValueStat::Value(v)) => {
981                    assert_eq!("a98bea04-d119-4f21-8edc-eb218b5849af", v.as_str().unwrap())
982                }
983                k => panic!("Key {k:?} should not be present in max_values"),
984            }
985        }
986
987        // assert on null count
988        for (k, v) in stats.null_count.iter() {
989            match (k.as_str(), v) {
990                ("meta", ColumnCountStat::Column(map)) => {
991                    assert_eq!(2, map.len());
992
993                    let kafka = map.get("kafka").unwrap().as_column().unwrap();
994                    assert_eq!(3, kafka.len());
995                    let partition = kafka.get("partition").unwrap().as_value().unwrap();
996                    assert_eq!(0, partition);
997
998                    let producer = map.get("producer").unwrap().as_column().unwrap();
999                    assert_eq!(1, producer.len());
1000                    let timestamp = producer.get("timestamp").unwrap().as_value().unwrap();
1001                    assert_eq!(0, timestamp);
1002                }
1003                ("some_int", ColumnCountStat::Value(v)) => assert_eq!(100, *v),
1004                ("some_bool", ColumnCountStat::Value(v)) => assert_eq!(100, *v),
1005                ("some_string", ColumnCountStat::Value(v)) => assert_eq!(100, *v),
1006                ("some_list", ColumnCountStat::Value(v)) => assert_eq!(100, *v),
1007                ("some_nested_list", ColumnCountStat::Value(v)) => assert_eq!(100, *v),
1008                ("date", ColumnCountStat::Value(v)) => assert_eq!(0, *v),
1009                ("uuid", ColumnCountStat::Value(v)) => assert_eq!(0, *v),
1010                k => panic!("Key {k:?} should not be present in null_count"),
1011            }
1012        }
1013    }
1014
1015    // Regression test for delta-io/delta-rs#3172: leaves under a nested
1016    // top-level field used to consume the `delta.dataSkippingNumIndexedCols`
1017    // budget one-by-one, starving later top-level columns of stats. After the
1018    // fix the budget is counted per distinct top-level field, so every
1019    // top-level column up to the limit gets stats.
1020    #[tokio::test]
1021    async fn test_nested_fields_do_not_consume_stats_budget() {
1022        use crate::kernel::{DataType as DeltaDataType, PrimitiveType, StructField, StructType};
1023
1024        // 5 top-level columns, 8 parquet leaves total ("1", nested.{2,3,4,5},
1025        // year, month, day). With `dataSkippingNumIndexedCols=5` the
1026        // leaf-counted implementation would burn the budget on "1" plus the
1027        // four `nested.*` leaves, dropping year/month/day. With the
1028        // top-level-counted budget all five top-level columns are admitted.
1029        let nested = StructType::try_new([
1030            StructField::nullable("2", DeltaDataType::Primitive(PrimitiveType::Long)),
1031            StructField::nullable("3", DeltaDataType::Primitive(PrimitiveType::Long)),
1032            StructField::nullable("4", DeltaDataType::Primitive(PrimitiveType::Long)),
1033            StructField::nullable("5", DeltaDataType::Primitive(PrimitiveType::Long)),
1034        ])
1035        .unwrap();
1036        let configuration: HashMap<String, Option<String>> = [(
1037            "delta.dataSkippingNumIndexedCols".to_string(),
1038            Some("5".to_string()),
1039        )]
1040        .into_iter()
1041        .collect();
1042
1043        let table = DeltaTable::new_in_memory()
1044            .create()
1045            .with_columns([
1046                StructField::nullable("1", DeltaDataType::Primitive(PrimitiveType::String)),
1047                StructField::nullable("nested", DeltaDataType::Struct(Box::new(nested))),
1048                StructField::nullable("year", DeltaDataType::Primitive(PrimitiveType::Long)),
1049                StructField::nullable("month", DeltaDataType::Primitive(PrimitiveType::Long)),
1050                StructField::nullable("day", DeltaDataType::Primitive(PrimitiveType::Long)),
1051            ])
1052            .with_configuration(configuration)
1053            .await
1054            .unwrap();
1055
1056        let mut writer = RecordBatchWriter::for_table(&table).unwrap();
1057        let arrow_schema = writer.arrow_schema();
1058        let rows = vec![json!({
1059            "1": "foo",
1060            "nested": {"2": 100, "3": 200, "4": 300, "5": 400},
1061            "year": 2024,
1062            "month": 12,
1063            "day": 1
1064        })];
1065        let batch = record_batch_from_message(arrow_schema, rows.as_slice()).unwrap();
1066
1067        writer.write(batch).await.unwrap();
1068        let add = writer.flush().await.unwrap();
1069        assert_eq!(add.len(), 1);
1070        let stats = add[0].get_stats().unwrap().unwrap();
1071
1072        // Every top-level non-partition column should have min/max/nullCount.
1073        for key in ["1", "year", "month", "day"] {
1074            assert!(
1075                stats.min_values.contains_key(key),
1076                "min_values missing top-level column {key:?}: {:?}",
1077                stats.min_values.keys().collect::<Vec<_>>()
1078            );
1079            assert!(
1080                stats.max_values.contains_key(key),
1081                "max_values missing top-level column {key:?}: {:?}",
1082                stats.max_values.keys().collect::<Vec<_>>()
1083            );
1084            assert!(
1085                stats.null_count.contains_key(key),
1086                "null_count missing top-level column {key:?}: {:?}",
1087                stats.null_count.keys().collect::<Vec<_>>()
1088            );
1089        }
1090
1091        // The nested struct's leaves should still produce per-field stats
1092        // under the "nested" key (one top-level slot, all leaves admitted).
1093        let nested_min = stats
1094            .min_values
1095            .get("nested")
1096            .and_then(ColumnValueStat::as_column)
1097            .expect("nested entry should be a column map");
1098        for key in ["2", "3", "4", "5"] {
1099            assert!(
1100                nested_min.contains_key(key),
1101                "nested.{key} missing from min_values"
1102            );
1103        }
1104    }
1105
1106    async fn load_table(
1107        table_url: &Url,
1108        options: HashMap<String, String>,
1109    ) -> Result<DeltaTable, DeltaTableError> {
1110        DeltaTableBuilder::from_url(table_url.clone())?
1111            .with_storage_options(options)
1112            .load()
1113            .await
1114    }
1115
1116    fn create_temp_table(table_path: &Path) {
1117        let log_path = table_path.join("_delta_log");
1118
1119        std::fs::create_dir(log_path.as_path()).unwrap();
1120        std::fs::write(
1121            log_path.join("00000000000000000000.json"),
1122            V0_COMMIT.as_str(),
1123        )
1124        .unwrap();
1125    }
1126
1127    static SCHEMA: LazyLock<Value> = LazyLock::new(|| {
1128        json!({
1129            "type": "struct",
1130            "fields": [
1131                {
1132                    "name": "meta",
1133                    "type": {
1134                        "type": "struct",
1135                        "fields": [
1136                            {
1137                                "name": "kafka",
1138                                "type": {
1139                                    "type": "struct",
1140                                    "fields": [
1141                                        {
1142                                            "name": "topic",
1143                                            "type": "string",
1144                                            "nullable": true, "metadata": {}
1145                                        },
1146                                        {
1147                                            "name": "partition",
1148                                            "type": "integer",
1149                                            "nullable": true, "metadata": {}
1150                                        },
1151                                        {
1152                                            "name": "offset",
1153                                            "type": "long",
1154                                            "nullable": true, "metadata": {}
1155                                        }
1156                                    ],
1157                                },
1158                                "nullable": true, "metadata": {}
1159                            },
1160                            {
1161                                "name": "producer",
1162                                "type": {
1163                                    "type": "struct",
1164                                    "fields": [
1165                                        {
1166                                            "name": "timestamp",
1167                                            "type": "string",
1168                                            "nullable": true, "metadata": {}
1169                                        }
1170                                    ],
1171                                },
1172                                "nullable": true, "metadata": {}
1173                            }
1174                        ]
1175                    },
1176                    "nullable": true, "metadata": {}
1177                },
1178                { "name": "some_string", "type": "string", "nullable": true, "metadata": {} },
1179                { "name": "some_int", "type": "integer", "nullable": true, "metadata": {} },
1180                { "name": "some_bool", "type": "boolean", "nullable": true, "metadata": {} },
1181                {
1182                    "name": "some_list",
1183                    "type": {
1184                        "type": "array",
1185                        "elementType": "string",
1186                        "containsNull": true
1187                    },
1188                    "nullable": true, "metadata": {}
1189                },
1190                {
1191                    "name": "some_nested_list",
1192                    "type": {
1193                        "type": "array",
1194                        "elementType": {
1195                            "type": "array",
1196                            "elementType": "integer",
1197                            "containsNull": true
1198                        },
1199                        "containsNull": true
1200                    },
1201                    "nullable": true, "metadata": {}
1202               },
1203               { "name": "date", "type": "string", "nullable": true, "metadata": {} },
1204               { "name": "uuid", "type": "string", "nullable": true, "metadata": {} },
1205            ]
1206        })
1207    });
1208    static V0_COMMIT: LazyLock<String> = LazyLock::new(|| {
1209        let schema_string = serde_json::to_string(&SCHEMA.clone()).unwrap();
1210        let jsons = [
1211            json!({
1212                "protocol":{"minReaderVersion":1,"minWriterVersion":2}
1213            }),
1214            json!({
1215                "metaData": {
1216                    "id": "22ef18ba-191c-4c36-a606-3dad5cdf3830",
1217                    "format": {
1218                        "provider": "parquet", "options": {}
1219                    },
1220                    "schemaString": schema_string,
1221                    "partitionColumns": ["date"], "configuration": {}, "createdTime": 1564524294376i64
1222                }
1223            }),
1224        ];
1225
1226        jsons
1227            .iter()
1228            .map(|j| serde_json::to_string(j).unwrap())
1229            .collect::<Vec<String>>()
1230            .join("\n")
1231    });
1232    static JSON_ROWS: LazyLock<Vec<Value>> = LazyLock::new(|| {
1233        std::iter::repeat_n(
1234            json!({
1235                "meta": {
1236                    "kafka": {
1237                        "offset": 0,
1238                        "partition": 0,
1239                        "topic": "some_topic"
1240                    },
1241                    "producer": {
1242                        "timestamp": "2021-06-22"
1243                    },
1244                },
1245                "some_string": "GET",
1246                "some_int": 302,
1247                "some_bool": true,
1248                "some_list": ["a", "b", "c"],
1249                "some_nested_list": [[42], [84]],
1250                "date": "2021-06-22",
1251                "uuid": "176c770d-92af-4a21-bf76-5d8c5261d659",
1252            }),
1253            100,
1254        )
1255        .chain(std::iter::repeat_n(
1256            json!({
1257                "meta": {
1258                    "kafka": {
1259                        "offset": 100,
1260                        "partition": 1,
1261                        "topic": "another_topic"
1262                    },
1263                    "producer": {
1264                        "timestamp": "2021-06-22"
1265                    },
1266                },
1267                "some_string": "PUT",
1268                "some_int": 400,
1269                "some_bool": false,
1270                "some_list": ["x", "y", "z"],
1271                "some_nested_list": [[42], [84]],
1272                "date": "2021-06-22",
1273                "uuid": "54f3e867-3f7b-4122-a452-9d74fb4fe1ba",
1274            }),
1275            100,
1276        ))
1277        .chain(std::iter::repeat_n(
1278            json!({
1279                "meta": {
1280                    "kafka": {
1281                        "offset": 0,
1282                        "partition": 0,
1283                        "topic": "some_topic"
1284                    },
1285                    "producer": {
1286                        "timestamp": "2021-06-22"
1287                    },
1288                },
1289                "some_nested_list": [[42], null],
1290                "date": "2021-06-22",
1291                "uuid": "a98bea04-d119-4f21-8edc-eb218b5849af",
1292            }),
1293            100,
1294        ))
1295        .collect()
1296    });
1297}