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
27pub(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 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
76pub(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 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 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#[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 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 (Statistics::Int32(v), Some(LogicalType::Date)) => {
297 let epoch_start = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); 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 Ok(Self::Decimal {
305 value: val,
306 scale: *scale,
307 })
308 }
309 (Statistics::Int32(v), _) => Ok(Self::Int32(get_stat!(v))),
310 (Statistics::Int64(v), Some(LogicalType::Timestamp { unit, .. })) => {
312 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 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 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
429pub 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 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
477struct 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
533fn 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 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 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 (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 (_, 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 (_, 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 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 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 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 #[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 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 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 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}