This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 6f330c98b3 Fix data page statistics when all rows are null in a data 
page (#11295)
6f330c98b3 is described below

commit 6f330c98b390b23ae6ba04dce1b264fcfd28e684
Author: Eric Fredine <[email protected]>
AuthorDate: Sun Jul 7 14:42:16 2024 -0700

    Fix data page statistics when all rows are null in a data page (#11295)
    
    * Adds tests for data page statistics when all values on the page are null. 
Fixes most of the failing tests for iterators not handling this situation 
correctly.
    
    * Fix handling of data page statistics for FixedBinaryArray using a builder.
    
    * Fix data page all nulls stats test for Dictionary DataType.
    
    * Fixes handling of None statistics for Decimal128 and Decimal256.
    
    * Consolidate make_data_page_stats_iterator uses.
    
    * Fix linting error.
    
    * Remove unnecessary collect.
    
    ---------
    
    Co-authored-by: Eric Fredine <[email protected]>
---
 .../datasource/physical_plan/parquet/statistics.rs | 128 +++++++++--------
 datafusion/core/tests/parquet/arrow_statistics.rs  | 158 +++++++++++++++------
 2 files changed, 184 insertions(+), 102 deletions(-)

diff --git a/datafusion/core/src/datasource/physical_plan/parquet/statistics.rs 
b/datafusion/core/src/datasource/physical_plan/parquet/statistics.rs
index bd05fe64e6..b9aca2ac2c 100644
--- a/datafusion/core/src/datasource/physical_plan/parquet/statistics.rs
+++ b/datafusion/core/src/datasource/physical_plan/parquet/statistics.rs
@@ -19,6 +19,7 @@
 
 // TODO: potentially move this to arrow-rs: 
https://github.com/apache/arrow-rs/issues/4328
 
+use arrow::array::builder::FixedSizeBinaryBuilder;
 use arrow::datatypes::i256;
 use arrow::{array::ArrayRef, datatypes::DataType};
 use arrow_array::{
@@ -600,6 +601,31 @@ make_data_page_stats_iterator!(
     Index::DOUBLE,
     f64
 );
+make_data_page_stats_iterator!(
+    MinByteArrayDataPageStatsIterator,
+    |x: &PageIndex<ByteArray>| { x.min.clone() },
+    Index::BYTE_ARRAY,
+    ByteArray
+);
+make_data_page_stats_iterator!(
+    MaxByteArrayDataPageStatsIterator,
+    |x: &PageIndex<ByteArray>| { x.max.clone() },
+    Index::BYTE_ARRAY,
+    ByteArray
+);
+make_data_page_stats_iterator!(
+    MaxFixedLenByteArrayDataPageStatsIterator,
+    |x: &PageIndex<FixedLenByteArray>| { x.max.clone() },
+    Index::FIXED_LEN_BYTE_ARRAY,
+    FixedLenByteArray
+);
+
+make_data_page_stats_iterator!(
+    MinFixedLenByteArrayDataPageStatsIterator,
+    |x: &PageIndex<FixedLenByteArray>| { x.min.clone() },
+    Index::FIXED_LEN_BYTE_ARRAY,
+    FixedLenByteArray
+);
 
 macro_rules! get_decimal_page_stats_iterator {
     ($iterator_type: ident, $func: ident, $stat_value_type: ident, 
$convert_func: ident) => {
@@ -634,9 +660,7 @@ macro_rules! get_decimal_page_stats_iterator {
                                 .indexes
                                 .iter()
                                 .map(|x| {
-                                    Some($stat_value_type::from(
-                                        x.$func.unwrap_or_default(),
-                                    ))
+                                    x.$func.and_then(|x| 
Some($stat_value_type::from(x)))
                                 })
                                 .collect::<Vec<_>>(),
                         ),
@@ -645,9 +669,7 @@ macro_rules! get_decimal_page_stats_iterator {
                                 .indexes
                                 .iter()
                                 .map(|x| {
-                                    Some($stat_value_type::from(
-                                        x.$func.unwrap_or_default(),
-                                    ))
+                                    x.$func.and_then(|x| 
Some($stat_value_type::from(x)))
                                 })
                                 .collect::<Vec<_>>(),
                         ),
@@ -656,9 +678,9 @@ macro_rules! get_decimal_page_stats_iterator {
                                 .indexes
                                 .iter()
                                 .map(|x| {
-                                    Some($convert_func(
-                                        
x.clone().$func.unwrap_or_default().data(),
-                                    ))
+                                    x.clone()
+                                        .$func
+                                        .and_then(|x| 
Some($convert_func(x.data())))
                                 })
                                 .collect::<Vec<_>>(),
                         ),
@@ -667,9 +689,9 @@ macro_rules! get_decimal_page_stats_iterator {
                                 .indexes
                                 .iter()
                                 .map(|x| {
-                                    Some($convert_func(
-                                        
x.clone().$func.unwrap_or_default().data(),
-                                    ))
+                                    x.clone()
+                                        .$func
+                                        .and_then(|x| 
Some($convert_func(x.data())))
                                 })
                                 .collect::<Vec<_>>(),
                         ),
@@ -713,32 +735,6 @@ get_decimal_page_stats_iterator!(
     i256,
     from_bytes_to_i256
 );
-make_data_page_stats_iterator!(
-    MinByteArrayDataPageStatsIterator,
-    |x: &PageIndex<ByteArray>| { x.min.clone() },
-    Index::BYTE_ARRAY,
-    ByteArray
-);
-make_data_page_stats_iterator!(
-    MaxByteArrayDataPageStatsIterator,
-    |x: &PageIndex<ByteArray>| { x.max.clone() },
-    Index::BYTE_ARRAY,
-    ByteArray
-);
-
-make_data_page_stats_iterator!(
-    MaxFixedLenByteArrayDataPageStatsIterator,
-    |x: &PageIndex<FixedLenByteArray>| { x.max.clone() },
-    Index::FIXED_LEN_BYTE_ARRAY,
-    FixedLenByteArray
-);
-
-make_data_page_stats_iterator!(
-    MinFixedLenByteArrayDataPageStatsIterator,
-    |x: &PageIndex<FixedLenByteArray>| { x.min.clone() },
-    Index::FIXED_LEN_BYTE_ARRAY,
-    FixedLenByteArray
-);
 
 macro_rules! get_data_page_statistics {
     ($stat_type_prefix: ident, $data_type: ident, $iterator: ident) => {
@@ -757,7 +753,7 @@ macro_rules! get_data_page_statistics {
                     UInt8Array::from_iter(
                         [<$stat_type_prefix 
Int32DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
+                                x.into_iter().map(|x| {
                                     x.and_then(|x| u8::try_from(x).ok())
                                 })
                             })
@@ -768,7 +764,7 @@ macro_rules! get_data_page_statistics {
                     UInt16Array::from_iter(
                         [<$stat_type_prefix 
Int32DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
+                                x.into_iter().map(|x| {
                                     x.and_then(|x| u16::try_from(x).ok())
                                 })
                             })
@@ -779,7 +775,7 @@ macro_rules! get_data_page_statistics {
                     UInt32Array::from_iter(
                         [<$stat_type_prefix 
Int32DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
+                                x.into_iter().map(|x| {
                                     x.and_then(|x| Some(x as u32))
                                 })
                             })
@@ -789,7 +785,7 @@ macro_rules! get_data_page_statistics {
                     UInt64Array::from_iter(
                         [<$stat_type_prefix 
Int64DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
+                                x.into_iter().map(|x| {
                                     x.and_then(|x| Some(x as u64))
                                 })
                             })
@@ -799,7 +795,7 @@ macro_rules! get_data_page_statistics {
                     Int8Array::from_iter(
                         [<$stat_type_prefix 
Int32DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
+                                x.into_iter().map(|x| {
                                     x.and_then(|x| i8::try_from(x).ok())
                                 })
                             })
@@ -810,7 +806,7 @@ macro_rules! get_data_page_statistics {
                     Int16Array::from_iter(
                         [<$stat_type_prefix 
Int32DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
+                                x.into_iter().map(|x| {
                                     x.and_then(|x| i16::try_from(x).ok())
                                 })
                             })
@@ -823,8 +819,8 @@ macro_rules! get_data_page_statistics {
                     Float16Array::from_iter(
                         [<$stat_type_prefix 
Float16DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
-                                x.into_iter().filter_map(|x| {
-                                    x.and_then(|x| 
Some(from_bytes_to_f16(x.data())))
+                                x.into_iter().map(|x| {
+                                    x.and_then(|x| from_bytes_to_f16(x.data()))
                                 })
                             })
                             .flatten()
@@ -836,7 +832,7 @@ macro_rules! get_data_page_statistics {
                 Some(DataType::LargeBinary) => 
Ok(Arc::new(LargeBinaryArray::from_iter([<$stat_type_prefix 
ByteArrayDataPageStatsIterator>]::new($iterator).flatten()))),
                 Some(DataType::Utf8) => Ok(Arc::new(StringArray::from(
                     [<$stat_type_prefix 
ByteArrayDataPageStatsIterator>]::new($iterator).map(|x| {
-                        x.into_iter().filter_map(|x| {
+                        x.into_iter().map(|x| {
                         x.and_then(|x| {
                             let res = std::str::from_utf8(x.data()).map(|s| 
s.to_string()).ok();
                             if res.is_none() {
@@ -849,7 +845,7 @@ macro_rules! get_data_page_statistics {
                 ))),
                 Some(DataType::LargeUtf8) => 
Ok(Arc::new(LargeStringArray::from(
                     [<$stat_type_prefix 
ByteArrayDataPageStatsIterator>]::new($iterator).map(|x| {
-                        x.into_iter().filter_map(|x| {
+                        x.into_iter().map(|x| {
                             x.and_then(|x| {
                                 let res = 
std::str::from_utf8(x.data()).map(|s| s.to_string()).ok();
                                 if res.is_none() {
@@ -878,10 +874,10 @@ macro_rules! get_data_page_statistics {
                         Date64Array::from([<$stat_type_prefix 
Int32DataPageStatsIterator>]::new($iterator)
                             .map(|x| {
                                 x.into_iter()
-                                .filter_map(|x| {
+                                .map(|x| {
                                     x.and_then(|x| i64::try_from(x).ok())
+                                    .map(|x| x * 24 * 60 * 60 * 1000)
                                 })
-                                .map(|x| x * 24 * 60 * 60 * 1000)
                             }).flatten().collect::<Vec<_>>()
                         )
                     )
@@ -919,16 +915,28 @@ macro_rules! get_data_page_statistics {
                     })
                 },
                Some(DataType::FixedSizeBinary(size)) => {
-                    Ok(Arc::new(
-                        FixedSizeBinaryArray::try_from_iter(
-                            [<$stat_type_prefix 
FixedLenByteArrayDataPageStatsIterator>]::new($iterator)
-                            .flat_map(|x| x.into_iter())
-                            .filter_map(|x| x)
-                        ).unwrap_or_else(|e| {
-                            log::debug!("FixedSizeBinary statistics is 
invalid: {}", e);
-                            FixedSizeBinaryArray::new(*size, vec![].into(), 
None)
-                        })
-                    ))
+                    let mut builder = FixedSizeBinaryBuilder::new(*size);
+                    let iterator = [<$stat_type_prefix 
FixedLenByteArrayDataPageStatsIterator>]::new($iterator);
+                    for x in iterator {
+                        for x in x.into_iter() {
+                            let Some(x) = x else {
+                                builder.append_null(); // no statistics value
+                                continue;
+                            };
+
+                            if x.len() == *size as usize {
+                                let _ = builder.append_value(x.data());
+                            } else {
+                                log::debug!(
+                                    "FixedSizeBinary({}) statistics is a 
binary of size {}, ignoring it.",
+                                    size,
+                                    x.len(),
+                                );
+                                builder.append_null();
+                            }
+                        }
+                    }
+                    Ok(Arc::new(builder.finish()))
                 },
                 _ => unimplemented!()
             }
diff --git a/datafusion/core/tests/parquet/arrow_statistics.rs 
b/datafusion/core/tests/parquet/arrow_statistics.rs
index 6bfb9b02d3..2b4ba0b171 100644
--- a/datafusion/core/tests/parquet/arrow_statistics.rs
+++ b/datafusion/core/tests/parquet/arrow_statistics.rs
@@ -29,15 +29,15 @@ use arrow::datatypes::{
     TimestampNanosecondType, TimestampSecondType,
 };
 use arrow_array::{
-    make_array, Array, ArrayRef, BinaryArray, BooleanArray, Date32Array, 
Date64Array,
-    Decimal128Array, Decimal256Array, FixedSizeBinaryArray, Float16Array, 
Float32Array,
-    Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, 
LargeBinaryArray,
-    LargeStringArray, RecordBatch, StringArray, Time32MillisecondArray,
+    make_array, new_null_array, Array, ArrayRef, BinaryArray, BooleanArray, 
Date32Array,
+    Date64Array, Decimal128Array, Decimal256Array, FixedSizeBinaryArray, 
Float16Array,
+    Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array,
+    LargeBinaryArray, LargeStringArray, RecordBatch, StringArray, 
Time32MillisecondArray,
     Time32SecondArray, Time64MicrosecondArray, Time64NanosecondArray,
     TimestampMicrosecondArray, TimestampMillisecondArray, 
TimestampNanosecondArray,
     TimestampSecondArray, UInt16Array, UInt32Array, UInt64Array, UInt8Array,
 };
-use arrow_schema::{DataType, Field, Schema};
+use arrow_schema::{DataType, Field, Schema, TimeUnit};
 use datafusion::datasource::physical_plan::parquet::StatisticsConverter;
 use half::f16;
 use parquet::arrow::arrow_reader::{
@@ -91,51 +91,60 @@ impl Int64Case {
 
     // Create a parquet file with the specified settings
     pub fn build(&self) -> ParquetRecordBatchReaderBuilder<File> {
-        let mut output_file = tempfile::Builder::new()
-            .prefix("parquert_statistics_test")
-            .suffix(".parquet")
-            .tempfile()
-            .expect("tempfile creation");
-
-        let mut builder =
-            
WriterProperties::builder().set_max_row_group_size(self.row_per_group);
-        if let Some(enable_stats) = self.enable_stats {
-            builder = builder.set_statistics_enabled(enable_stats);
-        }
-        if let Some(data_page_row_count_limit) = 
self.data_page_row_count_limit {
-            builder = 
builder.set_data_page_row_count_limit(data_page_row_count_limit);
-        }
-        let props = builder.build();
-
         let batches = vec![self.make_int64_batches_with_null()];
+        build_parquet_file(
+            self.row_per_group,
+            self.enable_stats,
+            self.data_page_row_count_limit,
+            batches,
+        )
+    }
+}
 
-        let schema = batches[0].schema();
+fn build_parquet_file(
+    row_per_group: usize,
+    enable_stats: Option<EnabledStatistics>,
+    data_page_row_count_limit: Option<usize>,
+    batches: Vec<RecordBatch>,
+) -> ParquetRecordBatchReaderBuilder<File> {
+    let mut output_file = tempfile::Builder::new()
+        .prefix("parquert_statistics_test")
+        .suffix(".parquet")
+        .tempfile()
+        .expect("tempfile creation");
+
+    let mut builder = 
WriterProperties::builder().set_max_row_group_size(row_per_group);
+    if let Some(enable_stats) = enable_stats {
+        builder = builder.set_statistics_enabled(enable_stats);
+    }
+    if let Some(data_page_row_count_limit) = data_page_row_count_limit {
+        builder = 
builder.set_data_page_row_count_limit(data_page_row_count_limit);
+    }
+    let props = builder.build();
 
-        let mut writer =
-            ArrowWriter::try_new(&mut output_file, schema, 
Some(props)).unwrap();
+    let schema = batches[0].schema();
 
-        // if we have a datapage limit send the batches in one at a time to 
give
-        // the writer a chance to be split into multiple pages
-        if self.data_page_row_count_limit.is_some() {
-            for batch in batches {
-                for i in 0..batch.num_rows() {
-                    writer.write(&batch.slice(i, 1)).expect("writing batch");
-                }
-            }
-        } else {
-            for batch in batches {
-                writer.write(&batch).expect("writing batch");
+    let mut writer = ArrowWriter::try_new(&mut output_file, schema, 
Some(props)).unwrap();
+
+    // if we have a datapage limit send the batches in one at a time to give
+    // the writer a chance to be split into multiple pages
+    if data_page_row_count_limit.is_some() {
+        for batch in &batches {
+            for i in 0..batch.num_rows() {
+                writer.write(&batch.slice(i, 1)).expect("writing batch");
             }
         }
+    } else {
+        for batch in &batches {
+            writer.write(batch).expect("writing batch");
+        }
+    }
 
-        // close file
-        let _file_meta = writer.close().unwrap();
+    let _file_meta = writer.close().unwrap();
 
-        // open the file & get the reader
-        let file = output_file.reopen().unwrap();
-        let options = ArrowReaderOptions::new().with_page_index(true);
-        ArrowReaderBuilder::try_new_with_options(file, options).unwrap()
-    }
+    let file = output_file.reopen().unwrap();
+    let options = ArrowReaderOptions::new().with_page_index(true);
+    ArrowReaderBuilder::try_new_with_options(file, options).unwrap()
 }
 
 /// Defines what data to create in a parquet file
@@ -503,6 +512,71 @@ async fn test_multiple_data_pages_nulls_and_negatives() {
     .run()
 }
 
+#[tokio::test]
+async fn test_data_page_stats_with_all_null_page() {
+    for data_type in &[
+        DataType::Boolean,
+        DataType::UInt64,
+        DataType::UInt32,
+        DataType::UInt16,
+        DataType::UInt8,
+        DataType::Int64,
+        DataType::Int32,
+        DataType::Int16,
+        DataType::Int8,
+        DataType::Float16,
+        DataType::Float32,
+        DataType::Float64,
+        DataType::Date32,
+        DataType::Date64,
+        DataType::Time32(TimeUnit::Millisecond),
+        DataType::Time32(TimeUnit::Second),
+        DataType::Time64(TimeUnit::Microsecond),
+        DataType::Time64(TimeUnit::Nanosecond),
+        DataType::Timestamp(TimeUnit::Second, None),
+        DataType::Timestamp(TimeUnit::Millisecond, None),
+        DataType::Timestamp(TimeUnit::Microsecond, None),
+        DataType::Timestamp(TimeUnit::Nanosecond, None),
+        DataType::Binary,
+        DataType::LargeBinary,
+        DataType::FixedSizeBinary(3),
+        DataType::Utf8,
+        DataType::LargeUtf8,
+        DataType::Dictionary(Box::new(DataType::Int32), 
Box::new(DataType::Utf8)),
+        DataType::Decimal128(8, 2),  // as INT32
+        DataType::Decimal128(10, 2), // as INT64
+        DataType::Decimal128(20, 2), // as FIXED_LEN_BYTE_ARRAY
+        DataType::Decimal256(8, 2),  // as INT32
+        DataType::Decimal256(10, 2), // as INT64
+        DataType::Decimal256(20, 2), // as FIXED_LEN_BYTE_ARRAY
+    ] {
+        let batch =
+            RecordBatch::try_from_iter(vec![("col", new_null_array(data_type, 
4))])
+                .expect("record batch creation");
+
+        let reader =
+            build_parquet_file(4, Some(EnabledStatistics::Page), Some(4), 
vec![batch]);
+
+        let expected_data_type = match data_type {
+            DataType::Dictionary(_, value_type) => value_type.as_ref(),
+            _ => data_type,
+        };
+
+        // There is one data page with 4 nulls
+        // The statistics should be present but null
+        Test {
+            reader: &reader,
+            expected_min: new_null_array(expected_data_type, 1),
+            expected_max: new_null_array(expected_data_type, 1),
+            expected_null_counts: UInt64Array::from(vec![4]),
+            expected_row_counts: Some(UInt64Array::from(vec![4])),
+            column_name: "col",
+            check: Check::DataPage,
+        }
+        .run()
+    }
+}
+
 /////////////// MORE GENERAL TESTS //////////////////////
 // . Many columns in a file
 // . Differnet data types


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to