1use std::sync::Arc;
24
25use crate::PartitionedFile;
26use crate::file_groups::FileGroup;
27
28use arrow::array::RecordBatch;
29use arrow::compute::SortColumn;
30use arrow::datatypes::SchemaRef;
31use arrow::row::{Row, Rows};
32use datafusion_common::stats::{NdvFallback, Precision};
33use datafusion_common::{
34 DataFusionError, Result, ScalarValue, plan_datafusion_err, plan_err,
35};
36use datafusion_physical_expr::expressions::Column;
37use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
38use datafusion_physical_plan::{ColumnStatistics, Statistics};
39
40use futures::{Stream, StreamExt};
41
42pub(crate) struct MinMaxStatistics {
46 min_by_sort_order: Rows,
47 max_by_sort_order: Rows,
48 sort_order: LexOrdering,
49}
50
51impl MinMaxStatistics {
52 #[expect(unused)]
54 pub fn sort_order(&self) -> &LexOrdering {
55 &self.sort_order
56 }
57
58 #[expect(unused)]
60 pub fn min(&'_ self, idx: usize) -> Row<'_> {
61 self.min_by_sort_order.row(idx)
62 }
63
64 pub fn max(&'_ self, idx: usize) -> Row<'_> {
66 self.max_by_sort_order.row(idx)
67 }
68
69 pub fn new_from_files<'a>(
70 projected_sort_order: &LexOrdering, projected_schema: &SchemaRef, projection: Option<&[usize]>, files: impl IntoIterator<Item = &'a PartitionedFile>,
74 ) -> Result<Self> {
75 let Some(statistics_and_partition_values) = files
76 .into_iter()
77 .map(|file| {
78 file.statistics
79 .as_ref()
80 .zip(Some(file.partition_values.as_slice()))
81 })
82 .collect::<Option<Vec<_>>>()
83 else {
84 return plan_err!("Parquet file missing statistics");
85 };
86
87 let get_min_max = |i: usize| -> Result<(Vec<ScalarValue>, Vec<ScalarValue>)> {
89 Ok(statistics_and_partition_values
90 .iter()
91 .map(|(s, pv)| {
92 if i < s.column_statistics.len() {
93 s.column_statistics[i]
94 .min_value
95 .get_value()
96 .cloned()
97 .zip(s.column_statistics[i].max_value.get_value().cloned())
98 .ok_or_else(|| plan_datafusion_err!("statistics not found"))
99 } else {
100 if let Some(partition_value) =
101 pv.get(i - s.column_statistics.len())
102 {
103 Ok((partition_value.clone(), partition_value.clone()))
104 } else {
105 Err(plan_datafusion_err!(
106 "statistics not found for partition, expected at most {}",
107 s.column_statistics.len()
108 ))
109 }
110 }
111 })
112 .collect::<Result<Vec<_>>>()?
113 .into_iter()
114 .unzip())
115 };
116
117 let Some(sort_columns) =
118 sort_columns_from_physical_sort_exprs(projected_sort_order)
119 else {
120 return plan_err!("sort expression must be on column");
121 };
122
123 let min_max_schema = Arc::new(
125 projected_schema
126 .project(&(sort_columns.iter().map(|c| c.index()).collect::<Vec<_>>()))?,
127 );
128
129 let min_max_sort_order = projected_sort_order
130 .iter()
131 .zip(sort_columns.iter())
132 .enumerate()
133 .map(|(idx, (sort_expr, col))| {
134 let expr = Arc::new(Column::new(col.name(), idx));
135 PhysicalSortExpr::new(expr, sort_expr.options)
136 });
137 let min_max_sort_order = LexOrdering::new(min_max_sort_order).unwrap();
139
140 let (min_values, max_values): (Vec<_>, Vec<_>) = sort_columns
141 .iter()
142 .map(|c| {
143 let i = projection
147 .map(|p| p[c.index()])
148 .unwrap_or_else(|| c.index());
149
150 let (min, max) = get_min_max(i).map_err(|e| {
151 e.context(format!("get min/max for column: '{}'", c.name()))
152 })?;
153 Ok((
154 ScalarValue::iter_to_array(min)?,
155 ScalarValue::iter_to_array(max)?,
156 ))
157 })
158 .collect::<Result<Vec<_>>>()
159 .map_err(|e| e.context("collect min/max values"))?
160 .into_iter()
161 .unzip();
162
163 let min_batch = RecordBatch::try_new(Arc::clone(&min_max_schema), min_values)
164 .map_err(|e| {
165 DataFusionError::ArrowError(
166 Box::new(e),
167 Some("\ncreate min batch".to_string()),
168 )
169 })?;
170 let max_batch = RecordBatch::try_new(Arc::clone(&min_max_schema), max_values)
171 .map_err(|e| {
172 DataFusionError::ArrowError(
173 Box::new(e),
174 Some("\ncreate max batch".to_string()),
175 )
176 })?;
177
178 Self::new(&min_max_sort_order, &min_max_schema, min_batch, max_batch)
179 }
180
181 #[expect(clippy::needless_pass_by_value)]
182 pub fn new(
183 sort_order: &LexOrdering,
184 schema: &SchemaRef,
185 min_values: RecordBatch,
186 max_values: RecordBatch,
187 ) -> Result<Self> {
188 use arrow::row::*;
189
190 let sort_fields = sort_order
191 .iter()
192 .map(|expr| {
193 expr.expr
194 .data_type(schema)
195 .map(|data_type| SortField::new_with_options(data_type, expr.options))
196 })
197 .collect::<Result<Vec<_>>>()
198 .map_err(|e| e.context("create sort fields"))?;
199 let converter = RowConverter::new(sort_fields)?;
200
201 let Some(sort_columns) = sort_columns_from_physical_sort_exprs(sort_order) else {
202 return plan_err!("sort expression must be on column");
203 };
204
205 let (new_min_cols, new_max_cols): (Vec<_>, Vec<_>) = sort_order
207 .iter()
208 .zip(sort_columns.iter().copied())
209 .map(|(sort_expr, column)| {
210 let maxes = max_values.column_by_name(column.name());
211 let mins = min_values.column_by_name(column.name());
212 let opt_value = if sort_expr.options.descending {
213 maxes.zip(mins)
214 } else {
215 mins.zip(maxes)
216 };
217 opt_value.ok_or_else(|| {
218 plan_datafusion_err!(
219 "missing column in MinMaxStatistics::new: '{}'",
220 column.name()
221 )
222 })
223 })
224 .collect::<Result<Vec<_>>>()?
225 .into_iter()
226 .unzip();
227
228 let [min, max] = [new_min_cols, new_max_cols].map(|cols| {
229 let values = RecordBatch::try_new(
230 min_values.schema(),
231 cols.into_iter().cloned().collect(),
232 )?;
233 let sorting_columns = sort_order
234 .iter()
235 .zip(sort_columns.iter().copied())
236 .map(|(sort_expr, column)| {
237 let schema = values.schema();
238 let idx = schema.index_of(column.name())?;
239
240 Ok(SortColumn {
241 values: Arc::clone(values.column(idx)),
242 options: Some(sort_expr.options),
243 })
244 })
245 .collect::<Result<Vec<_>>>()
246 .map_err(|e| e.context("create sorting columns"))?;
247 converter
248 .convert_columns(
249 &sorting_columns
250 .into_iter()
251 .map(|c| c.values)
252 .collect::<Vec<_>>(),
253 )
254 .map_err(|e| {
255 DataFusionError::ArrowError(
256 Box::new(e),
257 Some("convert columns".to_string()),
258 )
259 })
260 });
261
262 Ok(Self {
263 min_by_sort_order: min.map_err(|e| e.context("build min rows"))?,
264 max_by_sort_order: max.map_err(|e| e.context("build max rows"))?,
265 sort_order: sort_order.clone(),
266 })
267 }
268
269 pub fn min_values_sorted(&self) -> Vec<(usize, Row<'_>)> {
271 let mut sort: Vec<_> = self.min_by_sort_order.iter().enumerate().collect();
272 sort.sort_unstable_by_key(|(_, row)| *row);
273 sort
274 }
275
276 pub fn is_sorted(&self) -> bool {
279 self.max_by_sort_order
280 .iter()
281 .zip(self.min_by_sort_order.iter().skip(1))
282 .all(|(max, next_min)| max <= next_min)
283 }
284}
285
286fn sort_columns_from_physical_sort_exprs(
287 sort_order: &LexOrdering,
288) -> Option<Vec<&Column>> {
289 sort_order
290 .iter()
291 .map(|expr| expr.expr.downcast_ref::<Column>())
292 .collect()
293}
294
295fn seed_summary_statistics(summary_statistics: &mut Statistics, file_stats: &Statistics) {
296 summary_statistics.num_rows = file_stats.num_rows;
297 summary_statistics.total_byte_size = file_stats.total_byte_size;
298
299 for (summary_col_stats, file_col_stats) in summary_statistics
300 .column_statistics
301 .iter_mut()
302 .zip(file_stats.column_statistics.iter())
303 {
304 summary_col_stats.null_count = file_col_stats.null_count;
305 summary_col_stats.max_value = file_col_stats.max_value.clone();
306 summary_col_stats.min_value = file_col_stats.min_value.clone();
307 summary_col_stats.sum_value = file_col_stats.sum_value.cast_to_sum_type();
308 summary_col_stats.byte_size = file_col_stats.byte_size;
309 }
310}
311
312fn merge_summary_statistics(
313 summary_statistics: &mut Statistics,
314 file_stats: &Statistics,
315) {
316 summary_statistics.num_rows = summary_statistics.num_rows.add(&file_stats.num_rows);
317 summary_statistics.total_byte_size = summary_statistics
318 .total_byte_size
319 .add(&file_stats.total_byte_size);
320
321 for (summary_col_stats, file_col_stats) in summary_statistics
322 .column_statistics
323 .iter_mut()
324 .zip(file_stats.column_statistics.iter())
325 {
326 let ColumnStatistics {
327 null_count: file_nc,
328 max_value: file_max,
329 min_value: file_min,
330 sum_value: file_sum,
331 distinct_count: _,
332 byte_size: file_sbs,
333 } = file_col_stats;
334
335 summary_col_stats.null_count = summary_col_stats.null_count.add(file_nc);
336 summary_col_stats.max_value = summary_col_stats.max_value.max(file_max);
337 summary_col_stats.min_value = summary_col_stats.min_value.min(file_min);
338 summary_col_stats.sum_value = summary_col_stats.sum_value.add_for_sum(file_sum);
339 summary_col_stats.byte_size = summary_col_stats.byte_size.add(file_sbs);
340 }
341}
342
343fn seed_first_file_statistics(
344 limit_num_rows: &mut Precision<usize>,
345 summary_statistics: &mut Statistics,
346 file_stats: &Statistics,
347 collect_stats: bool,
348) {
349 *limit_num_rows = file_stats.num_rows;
350
351 if collect_stats {
352 seed_summary_statistics(summary_statistics, file_stats);
353 }
354}
355
356fn merge_file_statistics(
357 limit_num_rows: &mut Precision<usize>,
358 summary_statistics: &mut Statistics,
359 file_stats: &Statistics,
360 collect_stats: bool,
361) {
362 *limit_num_rows = limit_num_rows.add(&file_stats.num_rows);
363
364 if collect_stats {
365 merge_summary_statistics(summary_statistics, file_stats);
366 }
367}
368
369#[deprecated(
375 since = "47.0.0",
376 note = "Please use `get_files_with_limit` and `compute_all_files_statistics` instead"
377)]
378#[cfg_attr(not(test), expect(unused))]
379pub async fn get_statistics_with_limit(
380 all_files: impl Stream<Item = Result<(PartitionedFile, Arc<Statistics>)>>,
381 file_schema: SchemaRef,
382 limit: Option<usize>,
383 collect_stats: bool,
384) -> Result<(FileGroup, Statistics)> {
385 let mut result_files = FileGroup::default();
386 let size = file_schema.fields().len();
393 let mut summary_statistics = Statistics {
394 num_rows: Precision::Absent,
395 total_byte_size: Precision::Absent,
396 column_statistics: vec![ColumnStatistics::default(); size],
397 };
398 let mut limit_num_rows = Precision::<usize>::Absent;
401
402 let mut all_files = Box::pin(all_files.fuse());
404
405 if let Some(first_file) = all_files.next().await {
406 let (mut file, file_stats) = first_file?;
407 file.statistics = Some(Arc::clone(&file_stats));
408 result_files.push(file);
409
410 seed_first_file_statistics(
411 &mut limit_num_rows,
412 &mut summary_statistics,
413 &file_stats,
414 collect_stats,
415 );
416
417 let conservative_num_rows = match limit_num_rows {
422 Precision::Exact(nr) => nr,
423 _ => usize::MIN,
424 };
425 if conservative_num_rows <= limit.unwrap_or(usize::MAX) {
426 while let Some(current) = all_files.next().await {
427 let (mut file, file_stats) = current?;
428 file.statistics = Some(Arc::clone(&file_stats));
429 result_files.push(file);
430 merge_file_statistics(
431 &mut limit_num_rows,
432 &mut summary_statistics,
433 &file_stats,
434 collect_stats,
435 );
436
437 if limit_num_rows.get_value().unwrap_or(&usize::MIN)
442 > &limit.unwrap_or(usize::MAX)
443 {
444 break;
445 }
446 }
447 }
448 };
449
450 let mut statistics = summary_statistics;
451 if all_files.next().await.is_some() {
452 statistics = statistics.to_inexact()
456 }
457
458 Ok((result_files, statistics))
459}
460
461#[expect(clippy::needless_pass_by_value)]
477pub fn compute_file_group_statistics(
478 file_group: FileGroup,
479 file_schema: SchemaRef,
480 collect_stats: bool,
481) -> Result<FileGroup> {
482 if !collect_stats {
483 return Ok(file_group);
484 }
485
486 let file_group_stats = file_group.iter().filter_map(|file| {
487 let stats = file.statistics.as_ref()?;
488 Some(stats.as_ref())
489 });
490 let statistics = Statistics::try_merge_iter_with_ndv_fallback(
491 file_group_stats,
492 &file_schema,
493 NdvFallback::Max,
494 )?;
495
496 Ok(file_group.with_statistics(Arc::new(statistics)))
497}
498
499#[expect(clippy::needless_pass_by_value)]
517pub fn compute_all_files_statistics(
518 file_groups: Vec<FileGroup>,
519 table_schema: SchemaRef,
520 collect_stats: bool,
521 inexact_stats: bool,
522) -> Result<(Vec<FileGroup>, Statistics)> {
523 let file_groups_with_stats = file_groups
524 .into_iter()
525 .map(|file_group| {
526 compute_file_group_statistics(
527 file_group,
528 Arc::clone(&table_schema),
529 collect_stats,
530 )
531 })
532 .collect::<Result<Vec<_>>>()?;
533
534 let file_groups_statistics = file_groups_with_stats
536 .iter()
537 .filter_map(|file_group| file_group.file_statistics(None));
538
539 let mut statistics = Statistics::try_merge_iter_with_ndv_fallback(
540 file_groups_statistics,
541 &table_schema,
542 NdvFallback::Max,
543 )?;
544
545 if inexact_stats {
546 statistics = statistics.to_inexact()
547 }
548
549 Ok((file_groups_with_stats, statistics))
550}
551
552#[deprecated(since = "47.0.0", note = "Use Statistics::add")]
553pub fn add_row_stats(
554 file_num_rows: Precision<usize>,
555 num_rows: Precision<usize>,
556) -> Precision<usize> {
557 file_num_rows.add(&num_rows)
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use crate::PartitionedFile;
564 use crate::file_groups::FileGroup;
565 use arrow::datatypes::{DataType, Field, Schema};
566 use futures::stream;
567
568 fn file_stats(sum: u32) -> Statistics {
569 Statistics {
570 num_rows: Precision::Exact(1),
571 total_byte_size: Precision::Exact(4),
572 column_statistics: vec![ColumnStatistics {
573 null_count: Precision::Exact(0),
574 max_value: Precision::Exact(ScalarValue::UInt32(Some(sum))),
575 min_value: Precision::Exact(ScalarValue::UInt32(Some(sum))),
576 sum_value: Precision::Exact(ScalarValue::UInt32(Some(sum))),
577 distinct_count: Precision::Exact(1),
578 byte_size: Precision::Exact(4),
579 }],
580 }
581 }
582
583 fn test_schema() -> SchemaRef {
584 Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)]))
585 }
586
587 fn make_file_stats(
588 num_rows: usize,
589 total_byte_size: usize,
590 col_stats: ColumnStatistics,
591 ) -> Arc<Statistics> {
592 Arc::new(Statistics {
593 num_rows: Precision::Exact(num_rows),
594 total_byte_size: Precision::Exact(total_byte_size),
595 column_statistics: vec![col_stats],
596 })
597 }
598
599 fn rich_col_stats(
600 null_count: usize,
601 min: i64,
602 max: i64,
603 sum: i64,
604 byte_size: usize,
605 ) -> ColumnStatistics {
606 ColumnStatistics {
607 null_count: Precision::Exact(null_count),
608 max_value: Precision::Exact(ScalarValue::Int64(Some(max))),
609 min_value: Precision::Exact(ScalarValue::Int64(Some(min))),
610 distinct_count: Precision::Absent,
611 sum_value: Precision::Exact(ScalarValue::Int64(Some(sum))),
612 byte_size: Precision::Exact(byte_size),
613 }
614 }
615
616 fn utf8_file_stats(ndv: usize, min: &str, max: &str) -> Statistics {
617 Statistics {
618 num_rows: Precision::Exact(1),
619 total_byte_size: Precision::Exact(16),
620 column_statistics: vec![ColumnStatistics {
621 null_count: Precision::Exact(0),
622 max_value: Precision::Exact(ScalarValue::Utf8(Some(max.to_string()))),
623 min_value: Precision::Exact(ScalarValue::Utf8(Some(min.to_string()))),
624 sum_value: Precision::Absent,
625 distinct_count: Precision::Exact(ndv),
626 byte_size: Precision::Exact(16),
627 }],
628 }
629 }
630
631 fn file_with_stats(path: &str, stats: Statistics) -> PartitionedFile {
632 PartitionedFile::new(path, 1).with_statistics(Arc::new(stats))
633 }
634 #[tokio::test]
635 #[expect(deprecated)]
636 async fn test_get_statistics_with_limit_casts_first_file_sum_to_sum_type()
637 -> Result<()> {
638 let schema =
639 Arc::new(Schema::new(vec![Field::new("c1", DataType::UInt32, true)]));
640
641 let files = stream::iter(vec![Ok((
642 PartitionedFile::new("f1.parquet", 1),
643 Arc::new(file_stats(100)),
644 ))]);
645
646 let (_group, stats) =
647 get_statistics_with_limit(files, schema, None, true).await?;
648
649 assert_eq!(
650 stats.column_statistics[0].sum_value,
651 Precision::Exact(ScalarValue::UInt64(Some(100)))
652 );
653
654 Ok(())
655 }
656
657 #[tokio::test]
658 #[expect(deprecated)]
659 async fn test_get_statistics_with_limit_merges_sum_with_unsigned_widening()
660 -> Result<()> {
661 let schema =
662 Arc::new(Schema::new(vec![Field::new("c1", DataType::UInt32, true)]));
663
664 let files = stream::iter(vec![
665 Ok((
666 PartitionedFile::new("f1.parquet", 1),
667 Arc::new(file_stats(100)),
668 )),
669 Ok((
670 PartitionedFile::new("f2.parquet", 1),
671 Arc::new(file_stats(200)),
672 )),
673 ]);
674
675 let (_group, stats) =
676 get_statistics_with_limit(files, schema, None, true).await?;
677
678 assert_eq!(
679 stats.column_statistics[0].sum_value,
680 Precision::Exact(ScalarValue::UInt64(Some(300)))
681 );
682
683 Ok(())
684 }
685
686 #[tokio::test]
687 #[expect(deprecated)]
688 async fn get_statistics_with_limit_collect_stats_false_returns_bare_statistics() {
689 let all_files = stream::iter(vec![
690 Ok((
691 PartitionedFile::new("first.parquet", 10),
692 make_file_stats(0, 0, rich_col_stats(1, 1, 9, 15, 64)),
693 )),
694 Ok((
695 PartitionedFile::new("second.parquet", 20),
696 make_file_stats(10, 100, rich_col_stats(2, 10, 99, 300, 128)),
697 )),
698 ]);
699
700 let (_files, statistics) =
701 get_statistics_with_limit(all_files, test_schema(), None, false)
702 .await
703 .unwrap();
704
705 assert_eq!(statistics.num_rows, Precision::Absent);
706 assert_eq!(statistics.total_byte_size, Precision::Absent);
707 assert_eq!(statistics.column_statistics.len(), 1);
708 assert_eq!(
709 statistics.column_statistics[0].null_count,
710 Precision::Absent
711 );
712 assert_eq!(statistics.column_statistics[0].max_value, Precision::Absent);
713 assert_eq!(statistics.column_statistics[0].min_value, Precision::Absent);
714 assert_eq!(statistics.column_statistics[0].sum_value, Precision::Absent);
715 assert_eq!(statistics.column_statistics[0].byte_size, Precision::Absent);
716 }
717
718 #[tokio::test]
719 #[expect(deprecated)]
720 async fn get_statistics_with_limit_collect_stats_false_uses_row_counts_for_limit() {
721 let all_files = stream::iter(vec![
722 Ok((
723 PartitionedFile::new("first.parquet", 10),
724 make_file_stats(3, 30, rich_col_stats(1, 1, 9, 15, 64)),
725 )),
726 Ok((
727 PartitionedFile::new("second.parquet", 20),
728 make_file_stats(3, 30, rich_col_stats(2, 10, 99, 300, 128)),
729 )),
730 Ok((
731 PartitionedFile::new("third.parquet", 30),
732 make_file_stats(3, 30, rich_col_stats(0, 100, 199, 450, 256)),
733 )),
734 ]);
735
736 let (files, statistics) =
737 get_statistics_with_limit(all_files, test_schema(), Some(4), false)
738 .await
739 .unwrap();
740
741 assert_eq!(files.len(), 2);
742 assert_eq!(statistics.num_rows, Precision::Absent);
743 assert_eq!(statistics.total_byte_size, Precision::Absent);
744 }
745
746 #[tokio::test]
747 #[expect(deprecated)]
748 async fn get_statistics_with_limit_collect_stats_true_aggregates_statistics() {
749 let all_files = stream::iter(vec![
750 Ok((
751 PartitionedFile::new("first.parquet", 10),
752 make_file_stats(5, 50, rich_col_stats(1, 1, 9, 15, 64)),
753 )),
754 Ok((
755 PartitionedFile::new("second.parquet", 20),
756 make_file_stats(10, 100, rich_col_stats(2, 10, 99, 300, 128)),
757 )),
758 ]);
759
760 let (_files, statistics) =
761 get_statistics_with_limit(all_files, test_schema(), None, true)
762 .await
763 .unwrap();
764
765 assert_eq!(statistics.num_rows, Precision::Exact(15));
766 assert_eq!(statistics.total_byte_size, Precision::Exact(150));
767 assert_eq!(
768 statistics.column_statistics[0].null_count,
769 Precision::Exact(3)
770 );
771 assert_eq!(
772 statistics.column_statistics[0].min_value,
773 Precision::Exact(ScalarValue::Int64(Some(1)))
774 );
775 assert_eq!(
776 statistics.column_statistics[0].max_value,
777 Precision::Exact(ScalarValue::Int64(Some(99)))
778 );
779 assert_eq!(
780 statistics.column_statistics[0].sum_value,
781 Precision::Exact(ScalarValue::Int64(Some(315)))
782 );
783 assert_eq!(
784 statistics.column_statistics[0].byte_size,
785 Precision::Exact(192)
786 );
787 }
788
789 #[tokio::test]
790 #[expect(deprecated)]
791 async fn get_statistics_with_limit_collect_stats_true_limit_marks_inexact() {
792 let all_files = stream::iter(vec![
793 Ok((
794 PartitionedFile::new("first.parquet", 10),
795 make_file_stats(5, 50, rich_col_stats(0, 1, 5, 15, 64)),
796 )),
797 Ok((
798 PartitionedFile::new("second.parquet", 20),
799 make_file_stats(5, 50, rich_col_stats(1, 6, 10, 40, 64)),
800 )),
801 Ok((
802 PartitionedFile::new("third.parquet", 20),
803 make_file_stats(5, 50, rich_col_stats(2, 11, 15, 65, 64)),
804 )),
805 ]);
806
807 let (files, statistics) =
808 get_statistics_with_limit(all_files, test_schema(), Some(8), true)
809 .await
810 .unwrap();
811
812 assert_eq!(files.len(), 2);
813 assert_eq!(statistics.num_rows, Precision::Inexact(10));
814 assert_eq!(statistics.total_byte_size, Precision::Inexact(100));
815 assert_eq!(
816 statistics.column_statistics[0].min_value,
817 Precision::Inexact(ScalarValue::Int64(Some(1)))
818 );
819 assert_eq!(
820 statistics.column_statistics[0].max_value,
821 Precision::Inexact(ScalarValue::Int64(Some(10)))
822 );
823 assert_eq!(
824 statistics.column_statistics[0].sum_value,
825 Precision::Inexact(ScalarValue::Int64(Some(55)))
826 );
827 assert_eq!(
828 statistics.column_statistics[0].byte_size,
829 Precision::Inexact(128)
830 );
831 }
832
833 #[test]
834 fn test_compute_file_group_statistics_uses_max_ndv_fallback() -> Result<()> {
835 let schema = Arc::new(Schema::new(vec![Field::new("c1", DataType::Utf8, true)]));
836 let file_group = FileGroup::new(vec![
837 file_with_stats("f1.parquet", utf8_file_stats(5, "a", "x")),
838 file_with_stats("f2.parquet", utf8_file_stats(8, "b", "z")),
839 ]);
840
841 let file_group =
842 compute_file_group_statistics(file_group, Arc::clone(&schema), true)?;
843 let stats = file_group.file_statistics(None).unwrap();
844
845 assert_eq!(
846 stats.column_statistics[0].distinct_count,
847 Precision::Inexact(8)
848 );
849 assert_eq!(
850 stats.column_statistics[0].min_value,
851 Precision::Exact(ScalarValue::Utf8(Some("a".to_string())))
852 );
853 assert_eq!(
854 stats.column_statistics[0].max_value,
855 Precision::Exact(ScalarValue::Utf8(Some("z".to_string())))
856 );
857
858 Ok(())
859 }
860
861 #[test]
862 fn test_compute_all_files_statistics_uses_max_ndv_fallback() -> Result<()> {
863 let schema = Arc::new(Schema::new(vec![Field::new("c1", DataType::Utf8, true)]));
864 let file_groups = vec![
865 FileGroup::new(vec![
866 file_with_stats("f1.parquet", utf8_file_stats(5, "a", "x")),
867 file_with_stats("f2.parquet", utf8_file_stats(8, "b", "z")),
868 ]),
869 FileGroup::new(vec![
870 file_with_stats("f3.parquet", utf8_file_stats(3, "c", "w")),
871 file_with_stats("f4.parquet", utf8_file_stats(6, "d", "y")),
872 ]),
873 ];
874
875 let (file_groups, stats) =
876 compute_all_files_statistics(file_groups, schema, true, false)?;
877
878 assert_eq!(
879 file_groups[0]
880 .file_statistics(None)
881 .unwrap()
882 .column_statistics[0]
883 .distinct_count,
884 Precision::Inexact(8)
885 );
886 assert_eq!(
887 file_groups[1]
888 .file_statistics(None)
889 .unwrap()
890 .column_statistics[0]
891 .distinct_count,
892 Precision::Inexact(6)
893 );
894 assert_eq!(
895 stats.column_statistics[0].distinct_count,
896 Precision::Inexact(8)
897 );
898
899 Ok(())
900 }
901
902 #[test]
903 fn min_max_statistics_missing_column_stats_returns_error() {
904 let schema = test_schema();
905 let sort_order =
906 [PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)))].into();
907 let files = [
908 file_with_stats("f1.parquet", Statistics::default()),
909 file_with_stats("f2.parquet", Statistics::default()),
910 ];
911
912 let err = match MinMaxStatistics::new_from_files(
913 &sort_order,
914 &schema,
915 None,
916 files.iter(),
917 ) {
918 Ok(_) => panic!("expected missing statistics error"),
919 Err(err) => err,
920 };
921
922 assert!(
923 err.to_string()
924 .contains("statistics not found for partition"),
925 "unexpected error: {err:?}"
926 );
927 }
928}