diff --git a/datafusion/core/tests/parquet/page_pruning.rs b/datafusion/core/tests/parquet/page_pruning.rs index 372a7a601d492..66636e354e2f2 100644 --- a/datafusion/core/tests/parquet/page_pruning.rs +++ b/datafusion/core/tests/parquet/page_pruning.rs @@ -742,11 +742,12 @@ uint_tests!( // page-2 0 0.0 4.0 // page-3 0 5.0 9.0 async fn prune_f64_lt() { + // Parquet floating bounds omit possible NaNs, so they cannot prune pages. test_prune( Scenario::Float64, "SELECT * FROM t where f < 1", Some(0), - Some(5), + Some(0), 11, 5, ) @@ -755,7 +756,7 @@ async fn prune_f64_lt() { Scenario::Float64, "SELECT * FROM t where -f > -1", Some(0), - Some(5), + Some(0), 11, 5, ) @@ -764,13 +765,13 @@ async fn prune_f64_lt() { #[tokio::test] async fn prune_f64_scalar_fun_and_gt() { - // result of sql "SELECT * FROM t where abs(f - 1) <= 0.000001 and f >= 0.1" - // only use "f >= 0" to prune + // The scalar function is unsupported for pruning, and the floating bounds + // for f >= 0.1 omit possible NaNs. Neither condition can prune pages. test_prune( Scenario::Float64, "SELECT * FROM t where abs(f - 1) <= 0.000001 and f >= 0.1", Some(0), - Some(10), + Some(0), 1, 5, ) diff --git a/datafusion/core/tests/parquet/row_group_pruning.rs b/datafusion/core/tests/parquet/row_group_pruning.rs index 0721715921909..c08abcef7eadc 100644 --- a/datafusion/core/tests/parquet/row_group_pruning.rs +++ b/datafusion/core/tests/parquet/row_group_pruning.rs @@ -807,14 +807,16 @@ async fn prune_uint32_eq_large_in_list() { #[tokio::test] async fn prune_f64_lt() { + // Parquet floating bounds omit possible NaNs, so all row groups reach the + // Bloom filter stage, which cannot prune these range predicates. RowGroupPruningTest::new() .with_scenario(Scenario::Float64) .with_query("SELECT * FROM t where f < 1") .with_expected_errors(Some(0)) - .with_matched_by_stats(Some(3)) - .with_pruned_by_stats(Some(1)) + .with_matched_by_stats(Some(4)) + .with_pruned_by_stats(Some(0)) .with_pruned_files(Some(0)) - .with_matched_by_bloom_filter(Some(3)) + .with_matched_by_bloom_filter(Some(4)) .with_pruned_by_bloom_filter(Some(0)) .with_expected_rows(11) .test_row_group_prune() @@ -823,10 +825,10 @@ async fn prune_f64_lt() { .with_scenario(Scenario::Float64) .with_query("SELECT * FROM t where -f > -1") .with_expected_errors(Some(0)) - .with_matched_by_stats(Some(3)) - .with_pruned_by_stats(Some(1)) + .with_matched_by_stats(Some(4)) + .with_pruned_by_stats(Some(0)) .with_pruned_files(Some(0)) - .with_matched_by_bloom_filter(Some(3)) + .with_matched_by_bloom_filter(Some(4)) .with_pruned_by_bloom_filter(Some(0)) .with_expected_rows(11) .test_row_group_prune() @@ -835,16 +837,16 @@ async fn prune_f64_lt() { #[tokio::test] async fn prune_f64_scalar_fun_and_gt() { - // result of sql "SELECT * FROM t where abs(f - 1) <= 0.000001 and f >= 0.1" - // only use "f >= 0" to prune + // The scalar function is unsupported for pruning, and the floating bounds + // for f >= 0.1 omit possible NaNs. Neither condition can prune row groups. RowGroupPruningTest::new() .with_scenario(Scenario::Float64) .with_query("SELECT * FROM t where abs(f - 1) <= 0.000001 and f >= 0.1") .with_expected_errors(Some(0)) - .with_matched_by_stats(Some(2)) - .with_pruned_by_stats(Some(2)) + .with_matched_by_stats(Some(4)) + .with_pruned_by_stats(Some(0)) .with_pruned_files(Some(0)) - .with_matched_by_bloom_filter(Some(2)) + .with_matched_by_bloom_filter(Some(4)) .with_pruned_by_bloom_filter(Some(0)) .with_expected_rows(1) .test_row_group_prune() diff --git a/datafusion/datasource-parquet/src/metadata.rs b/datafusion/datasource-parquet/src/metadata.rs index 213cac24a85be..9d24e9f78eef8 100644 --- a/datafusion/datasource-parquet/src/metadata.rs +++ b/datafusion/datasource-parquet/src/metadata.rs @@ -43,7 +43,7 @@ use object_store::{ObjectMeta, ObjectStore}; use parquet::DecodeResult; use parquet::arrow::arrow_reader::statistics::StatisticsConverter; use parquet::arrow::{parquet_column, parquet_to_arrow_schema}; -use parquet::basic::{ColumnOrder, SortOrder, Type as PhysicalType}; +use parquet::basic::{ColumnOrder, LogicalType, SortOrder, Type as PhysicalType}; use parquet::file::metadata::{ PageIndexPolicy, ParquetMetaData, ParquetMetaDataPushDecoder, ParquetMetaDataReader, RowGroupMetaData, SortingColumn, @@ -70,17 +70,26 @@ fn requires_unsigned_byte_array_order(column: &ColumnDescriptor) -> bool { /// The deprecated Parquet `min`/`max` fields use signed comparison, unlike /// Arrow's string and binary comparisons. Even the modern bounds cannot be /// interpreted without the corresponding footer `column_orders` entry. -/// Signed logical types, such as decimals, retain their existing behavior. +/// Non-floating signed logical types, such as decimals, retain their behavior. /// Columns with undefined sort orders, such as `INT96`, never have usable /// min/max bounds regardless of their physical type. The `INT96` check is /// defensive because parquet-rs does not currently expose those bounds. +/// Floating-point bounds omit NaNs, which Arrow orders below or above all +/// finite values depending on their sign. Without NaN-absence statistics, +/// neither endpoint bounds every non-null value in the column. pub(crate) fn has_untrusted_min_max_order( parquet_schema: &SchemaDescriptor, column_orders: Option<&[ColumnOrder]>, parquet_column_index: usize, ) -> bool { let column = parquet_schema.column(parquet_column_index); - if column.sort_order() == SortOrder::UNDEFINED { + if column.sort_order() == SortOrder::UNDEFINED + || matches!( + column.physical_type(), + PhysicalType::FLOAT | PhysicalType::DOUBLE + ) + || matches!(column.logical_type_ref(), Some(LogicalType::Float16)) + { return true; } requires_unsigned_byte_array_order(&column) diff --git a/datafusion/datasource-parquet/src/opener/mod.rs b/datafusion/datasource-parquet/src/opener/mod.rs index 25c3bc9a77851..cfb4690786270 100644 --- a/datafusion/datasource-parquet/src/opener/mod.rs +++ b/datafusion/datasource-parquet/src/opener/mod.rs @@ -2823,9 +2823,10 @@ mod test { async fn test_prune_on_statistics() { let store = Arc::new(InMemory::new()) as Arc; + // Integer bounds remain usable; Parquet floating bounds exclude possible NaNs. let batch = record_batch!( ("a", Int32, vec![Some(1), Some(2), Some(2)]), - ("b", Float32, vec![Some(1.0), Some(2.0), None]) + ("b", Int32, vec![Some(1), Some(2), None]) ) .unwrap(); @@ -2842,8 +2843,8 @@ mod test { .add_column_statistics(ColumnStatistics::new_unknown()) .add_column_statistics( ColumnStatistics::new_unknown() - .with_min_value(Precision::Exact(ScalarValue::Float32(Some(1.0)))) - .with_max_value(Precision::Exact(ScalarValue::Float32(Some(2.0)))) + .with_min_value(Precision::Exact(ScalarValue::Int32(Some(1)))) + .with_max_value(Precision::Exact(ScalarValue::Int32(Some(2)))) .with_null_count(Precision::Exact(1)), ), )); @@ -2867,8 +2868,8 @@ mod test { assert_eq!(num_batches, 1); assert_eq!(num_rows, 3); - // A filter on `b = 5.0` should exclude all rows - let expr = col("b").eq(lit(ScalarValue::Float32(Some(5.0)))); + // A filter on `b = 5` should exclude all rows + let expr = col("b").eq(lit(ScalarValue::Int32(Some(5)))); let predicate = logical2physical(&expr, &schema); let opener = make_opener(predicate); let stream = open_file(&opener, file).await.unwrap(); @@ -2943,7 +2944,7 @@ mod test { let batch = record_batch!( ("a", Int32, vec![Some(1), Some(2), Some(3)]), - ("b", Float64, vec![Some(1.0), Some(2.0), None]) + ("b", Int64, vec![Some(1), Some(2), None]) ) .unwrap(); let data_size = @@ -2959,15 +2960,15 @@ mod test { .add_column_statistics(ColumnStatistics::new_unknown()) .add_column_statistics( ColumnStatistics::new_unknown() - .with_min_value(Precision::Exact(ScalarValue::Float64(Some(1.0)))) - .with_max_value(Precision::Exact(ScalarValue::Float64(Some(2.0)))) + .with_min_value(Precision::Exact(ScalarValue::Int64(Some(1)))) + .with_max_value(Precision::Exact(ScalarValue::Int64(Some(2)))) .with_null_count(Precision::Exact(1)), ), )); let table_schema = Arc::new(Schema::new(vec![ Field::new("part", DataType::Int32, false), Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Float32, true), + Field::new("b", DataType::Int32, true), ])); let table_schema_for_opener = TableSchemaBuilder::from(&file_schema) .with_table_partition_cols(vec![Arc::new(Field::new( @@ -2987,7 +2988,7 @@ mod test { }; // Filter should match the partition value and file statistics - let expr = col("part").eq(lit(1)).and(col("b").eq(lit(1.0))); + let expr = col("part").eq(lit(1)).and(col("b").eq(lit(1i64))); let predicate = logical2physical(&expr, &table_schema); let opener = make_opener(predicate); let stream = open_file(&opener, file.clone()).await.unwrap(); @@ -2996,7 +2997,7 @@ mod test { assert_eq!(num_rows, 3); // Should prune based on partition value but not file statistics - let expr = col("part").eq(lit(2)).and(col("b").eq(lit(1.0))); + let expr = col("part").eq(lit(2)).and(col("b").eq(lit(1i64))); let predicate = logical2physical(&expr, &table_schema); let opener = make_opener(predicate); let stream = open_file(&opener, file.clone()).await.unwrap(); @@ -3005,7 +3006,7 @@ mod test { assert_eq!(num_rows, 0); // Should prune based on file statistics but not partition value - let expr = col("part").eq(lit(1)).and(col("b").eq(lit(7.0))); + let expr = col("part").eq(lit(1)).and(col("b").eq(lit(7i64))); let predicate = logical2physical(&expr, &table_schema); let opener = make_opener(predicate); let stream = open_file(&opener, file.clone()).await.unwrap(); @@ -3014,7 +3015,7 @@ mod test { assert_eq!(num_rows, 0); // Should prune based on both partition value and file statistics - let expr = col("part").eq(lit(2)).and(col("b").eq(lit(7.0))); + let expr = col("part").eq(lit(2)).and(col("b").eq(lit(7i64))); let predicate = logical2physical(&expr, &table_schema); let opener = make_opener(predicate); let stream = open_file(&opener, file).await.unwrap(); diff --git a/datafusion/datasource-parquet/src/statistics_order_tests.rs b/datafusion/datasource-parquet/src/statistics_order_tests.rs index 5081abf2cb47b..bba26f87c5857 100644 --- a/datafusion/datasource-parquet/src/statistics_order_tests.rs +++ b/datafusion/datasource-parquet/src/statistics_order_tests.rs @@ -15,12 +15,13 @@ // specific language governing permissions and limitations // under the License. -//! Regression tests for interpreting Parquet byte-array statistics orders. +//! Regression tests for Parquet bounds that do not follow Arrow's comparison order. use std::io::Write; +use std::ops::Not; use std::sync::Arc; -use arrow::array::{BooleanArray, record_batch}; +use arrow::array::{BooleanArray, Int32Array, RecordBatch, StringArray, record_batch}; use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; use bytes::Bytes; use datafusion_common::pruning::{PrunableStatistics, PruningStatistics}; @@ -66,6 +67,102 @@ struct TestFile { } impl TestFile { + fn floating(data_type: &DataType) -> Self { + // The payloads remain distinct even after conversion to Float16. + let nan = f64::from_bits(0x7ff8_2000_0000_0000); + let other_nan = f64::from_bits(0x7ff8_4000_0000_0000); + let values = [ + Some(1.0), + Some(nan), + Some(1.0), + Some(other_nan), + Some(1.0), + Some(-nan), + Some(1.0), + Some(-other_nan), + Some(1.0), + Some(nan), + None, + Some(-nan), + None, + None, + None, + None, + ] + .into_iter() + .map(|value| ScalarValue::Float64(value).cast_to(data_type).unwrap()) + .collect::>(); + let schema = Arc::new(Schema::new(vec![ + Field::new("f", data_type.clone(), true), + Field::new("n", DataType::Int32, false), + Field::new("s", DataType::Utf8, false), + ])); + let batch = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + ScalarValue::iter_to_array(values.clone()).unwrap(), + Arc::new(Int32Array::from_iter_values(0..16)), + Arc::new(StringArray::from_iter_values( + ["a", "b", "c", "d"].into_iter().flat_map(|s| [s; 4]), + )), + ], + ) + .unwrap(); + let properties = WriterProperties::builder() + .set_max_row_group_row_count(Some(4)) + .set_data_page_row_count_limit(2) + .set_write_batch_size(2) + .set_dictionary_enabled(false) + .set_statistics_enabled(EnabledStatistics::Page) + .build(); + let mut bytes = Vec::new(); + let mut writer = + ArrowWriter::try_new(&mut bytes, Arc::clone(&schema), Some(properties)) + .unwrap(); + writer.write(&batch).unwrap(); + writer.close().unwrap(); + let bytes = Bytes::from(bytes); + let metadata = Arc::new(read_metadata(&bytes)); + assert_eq!(metadata.num_row_groups(), 4); + let finite_bound = match data_type { + DataType::Float16 => vec![0x00, 0x3c], + DataType::Float32 => 1.0_f32.to_le_bytes().to_vec(), + DataType::Float64 => 1.0_f64.to_le_bytes().to_vec(), + _ => unreachable!(), + }; + for group in metadata.row_groups().iter().take(3) { + let stats = group.column(0).statistics().unwrap(); + assert_eq!(stats.min_bytes_opt().unwrap(), finite_bound); + assert_eq!(stats.max_bytes_opt().unwrap(), finite_bound); + } + for group in metadata.offset_index().unwrap() { + assert_eq!(group[0].page_locations.len(), 2); + } + + // Verify the actual data pages preserve the signed NaN payloads and + // nulls. ScalarValue equality compares floating-point bit patterns. + let decoded = ParquetRecordBatchReaderBuilder::try_new(bytes.clone()) + .unwrap() + .build() + .unwrap() + .flat_map(|batch| { + let batch = batch.unwrap(); + (0..batch.num_rows()) + .map(|row| { + ScalarValue::try_from_array(batch.column(0).as_ref(), row) + .unwrap() + }) + .collect::>() + }) + .collect::>(); + assert_eq!(decoded, values); + Self { + bytes, + schema, + metadata, + } + } + fn new(order: StatisticsOrder) -> Self { let batch = record_batch!( ( @@ -311,6 +408,197 @@ fn metrics() -> ParquetFileMetrics { ) } +fn assert_float_pruning( + file: &TestFile, + expr: &Expr, + max_in_list_size: usize, + expected: usize, +) { + let physical = logical2physical(expr, &file.schema); + let predicate = PruningPredicateBuilder::new() + .with_file_schema(Arc::clone(&file.schema)) + .with_max_in_list_size(max_in_list_size) + .try_build(Arc::clone(&physical)) + .unwrap(); + let context = format!( + "type={}, cap={max_in_list_size}, expr={expr}", + file.schema.field(0).data_type() + ); + let all = ParquetAccessPlan::new_all(file.metadata.num_row_groups()); + // Always evaluate the physical residual against the original, unpruned + // bytes. Pruning layers must not establish each other's expected result. + assert_eq!( + file.matching_rows(&physical, all.clone()), + expected, + "{context}" + ); + if expected > 0 { + assert!(file.file_matches(&predicate), "file: {context}"); + } + let row_groups = file.row_group_plan(&predicate); + assert_eq!( + file.matching_rows(&physical, row_groups.clone()), + expected, + "row groups: {context}" + ); + for index in row_groups.row_group_indexes() { + if row_groups.is_fully_matched(index) { + let mut group = ParquetAccessPlan::new_none(file.metadata.num_row_groups()); + group.scan(index); + assert_eq!( + file.matching_rows(&physical, group), + file.metadata.row_group(index).num_rows() as usize, + "fully matched row group {index}: {context}", + ); + } + } + // Use the opener's configured cap and start the page-only path with all + // row groups, so file/row-group guards cannot mask unsafe page bounds. + let pages = crate::opener::build_page_pruning_predicate( + &physical, + &file.schema, + max_in_list_size, + ); + for (label, plan) in [("pages", all), ("row groups and pages", row_groups)] { + let plan = pages.prune_plan_with_page_index( + plan, + &file.schema, + file.metadata.file_metadata().schema_descr(), + &file.metadata, + &metrics(), + ); + assert_eq!( + file.matching_rows(&physical, plan), + expected, + "{label}: {context}" + ); + } +} + +#[test] +fn floating_nan_in_lists_preserve_rows_at_every_pruning_level() { + for data_type in [DataType::Float16, DataType::Float32, DataType::Float64] { + let file = TestFile::floating(&data_type); + let value = |value| lit(ScalarValue::Float64(value).cast_to(&data_type).unwrap()); + let nan = f64::from_bits(0x7ff8_2000_0000_0000); + for cap in [0, MAX_IN_LIST_SIZE, MAX_IN_LIST_SIZE + 1, 1024] { + for size in [2, MAX_IN_LIST_SIZE + 1] { + for nan in [nan, -nan] { + let mut values = (2..=size) + .map(|v| value(Some(v as f64))) + .collect::>(); + values.push(value(Some(nan))); + assert_float_pruning( + &file, + &col("f").in_list(values.clone(), false), + cap, + 2, + ); + values[0] = value(None); + assert_float_pruning(&file, &col("f").in_list(values, false), cap, 2); + } + // The footer/page bounds are [1, 1], but NaNs still satisfy + // NOT IN (1, ...). Adding NULL makes all nonmatches unknown. + let mut values = (1..=size) + .map(|v| value(Some(v as f64))) + .collect::>(); + assert_float_pruning( + &file, + &col("f").in_list(values.clone(), true), + cap, + 6, + ); + values[1] = value(None); + assert_float_pruning(&file, &col("f").in_list(values, true), cap, 0); + } + } + } +} + +#[test] +fn floating_nan_comparisons_do_not_prune_or_fully_match_finite_bounds() { + for data_type in [DataType::Float16, DataType::Float32, DataType::Float64] { + let file = TestFile::floating(&data_type); + let value = |v| lit(ScalarValue::Float64(Some(v)).cast_to(&data_type).unwrap()); + let nan = f64::from_bits(0x7ff8_2000_0000_0000); + for (expr, expected) in [ + (col("f").eq(value(1.0)), 5), + (col("f").not_eq(value(1.0)), 6), + (col("f").lt(value(1.0)), 3), + (col("f").gt(value(1.0)), 3), + (col("f").lt_eq(value(1.0)), 8), + (col("f").gt_eq(value(1.0)), 8), + (col("f").eq(value(nan)), 2), + (col("f").eq(value(-nan)), 2), + (col("f").eq(value(1.0)).not(), 6), + (col("f").lt_eq(value(1.0)).not(), 3), + (col("f").eq(value(nan)).or(col("f").eq(value(-nan))), 4), + ] { + assert_float_pruning(&file, &expr, MAX_IN_LIST_SIZE, expected); + } + } +} + +#[test] +fn floating_bounds_keep_null_counts_and_other_column_statistics() { + for data_type in [DataType::Float16, DataType::Float32, DataType::Float64] { + let file = TestFile::floating(&data_type); + let statistics = file.statistics(); + let float = &statistics.column_statistics[0]; + assert_eq!(float.min_value, Precision::Absent); + assert_eq!(float.max_value, Precision::Absent); + assert_eq!(float.null_count, Precision::Exact(5)); + assert_eq!( + statistics.column_statistics[1].min_value, + Precision::Exact(ScalarValue::Int32(Some(0))) + ); + assert_eq!( + statistics.column_statistics[1].max_value, + Precision::Exact(ScalarValue::Int32(Some(15))) + ); + assert_eq!( + statistics.column_statistics[2].min_value, + Precision::Exact(ScalarValue::Utf8(Some("a".into()))) + ); + assert_eq!( + statistics.column_statistics[2].max_value, + Precision::Exact(ScalarValue::Utf8(Some("d".into()))) + ); + + for (expr, groups, expected) in [ + (col("f").is_null(), vec![2, 3], 5), + (col("f").is_not_null(), vec![0, 1, 2], 11), + ( + col("f").eq(lit(ScalarValue::Float64(Some(1.0)) + .cast_to(&data_type) + .unwrap())), + vec![0, 1, 2], + 5, + ), + ] { + let (physical, predicate) = file.predicate(&expr); + assert_eq!(file.row_group_plan(&predicate).row_group_indexes(), groups); + let pages = file.page_plan(&physical, ParquetAccessPlan::new_all(4)); + assert_eq!(pages.row_group_indexes(), groups); + assert_eq!(file.matching_rows(&physical, pages), expected); + } + for expr in [col("n").eq(lit(99)), col("s").eq(lit("z"))] { + let (physical, predicate) = file.predicate(&expr); + assert!(!file.file_matches(&predicate)); + assert!( + file.row_group_plan(&predicate) + .row_group_indexes() + .is_empty() + ); + assert!( + file.page_plan(&physical, ParquetAccessPlan::new_all(4)) + .row_group_indexes() + .is_empty() + ); + } + } +} + #[test] fn byte_array_order_preserves_matching_rows_at_every_pruning_level() { for order in [ diff --git a/datafusion/sqllogictest/test_files/parquet.slt b/datafusion/sqllogictest/test_files/parquet.slt index 7b7a4fb196503..1ee5c0de16a67 100644 --- a/datafusion/sqllogictest/test_files/parquet.slt +++ b/datafusion/sqllogictest/test_files/parquet.slt @@ -380,6 +380,21 @@ NULL statement ok DROP TABLE single_nan; +# Floating-point bounds omit NaNs that still match ordered comparisons. +statement ok +CREATE EXTERNAL TABLE float16_with_nan +STORED AS PARQUET +LOCATION '../../parquet-testing/data/float16_nonzeros_and_nans.parquet'; + +query R +SELECT x FROM float16_with_nan +WHERE x > arrow_cast(2.0, 'Float16'); +---- +NaN + +statement ok +DROP TABLE float16_with_nan; + statement ok CREATE EXTERNAL TABLE list_columns STORED AS PARQUET diff --git a/datafusion/sqllogictest/test_files/parquet_statistics.slt b/datafusion/sqllogictest/test_files/parquet_statistics.slt index 9cf6b1e0381d1..994dbe3bde1fd 100644 --- a/datafusion/sqllogictest/test_files/parquet_statistics.slt +++ b/datafusion/sqllogictest/test_files/parquet_statistics.slt @@ -170,18 +170,18 @@ query TT EXPLAIN SELECT f32 FROM typed_table WHERE f32 = 2.5; ---- physical_plan -01)FilterExec: CAST(f32@0 AS Float64) = 2.5, statistics=[Rows=Inexact(1), Bytes=Inexact(1), [(Col[0]: Min=Exact(Float32(2.5)) Max=Exact(Float32(2.5)) Null=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(1))]] -02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, statistics=[Rows=Inexact(5), Bytes=Inexact(20), [(Col[0]: Min=Inexact(Float32(1.5)) Max=Inexact(Float32(5.5)) Null=Inexact(0) ScanBytes=Inexact(20))]] -03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/typed_table.parquet]]}, projection=[f32], file_type=parquet, predicate=CAST(f32@2 AS Float64) = 2.5, pruning_predicate=f32_null_count@2 != row_count@3 AND CAST(f32_min@0 AS Float64) <= 2.5 AND 2.5 <= CAST(f32_max@1 AS Float64), required_guarantees=[], statistics=[Rows=Inexact(5), Bytes=Inexact(20), [(Col[0]: Min=Inexact(Float32(1.5)) Max=Inexact(Float32(5.5)) Null=Inexact(0) ScanBytes=Inexact(20))]] +01)FilterExec: CAST(f32@0 AS Float64) = 2.5, statistics=[Rows=Inexact(5), Bytes=Inexact(20), [(Col[0]: Min=Exact(Float32(2.5)) Max=Exact(Float32(2.5)) Null=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(20))]] +02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, statistics=[Rows=Inexact(5), Bytes=Inexact(20), [(Col[0]: Null=Inexact(0) ScanBytes=Inexact(20))]] +03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/typed_table.parquet]]}, projection=[f32], file_type=parquet, predicate=CAST(f32@2 AS Float64) = 2.5, pruning_predicate=f32_null_count@2 != row_count@3 AND CAST(f32_min@0 AS Float64) <= 2.5 AND 2.5 <= CAST(f32_max@1 AS Float64), required_guarantees=[], statistics=[Rows=Inexact(5), Bytes=Inexact(20), [(Col[0]: Null=Inexact(0) ScanBytes=Inexact(20))]] # Reversed operand order: literal = column (Float64) query TT EXPLAIN SELECT f64 FROM typed_table WHERE 2.5 = f64; ---- physical_plan -01)FilterExec: f64@0 = 2.5, statistics=[Rows=Inexact(1), Bytes=Inexact(1), [(Col[0]: Min=Exact(Float64(2.5)) Max=Exact(Float64(2.5)) Null=Exact(0) Distinct=Exact(1) ScanBytes=Inexact(1))]] -02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Inexact(Float64(1.5)) Max=Inexact(Float64(5.5)) Null=Inexact(0) ScanBytes=Inexact(40))]] -03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/typed_table.parquet]]}, projection=[f64], file_type=parquet, predicate=f64@3 = 2.5, pruning_predicate=f64_null_count@2 != row_count@3 AND f64_min@0 <= 2.5 AND 2.5 <= f64_max@1, required_guarantees=[f64 in (2.5)], statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Inexact(Float64(1.5)) Max=Inexact(Float64(5.5)) Null=Inexact(0) ScanBytes=Inexact(40))]] +01)FilterExec: f64@0 = 2.5, statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Exact(Float64(2.5)) Max=Exact(Float64(2.5)) Null=Exact(0) Distinct=Exact(1) ScanBytes=Inexact(40))]] +02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Null=Inexact(0) ScanBytes=Inexact(40))]] +03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/typed_table.parquet]]}, projection=[f64], file_type=parquet, predicate=f64@3 = 2.5, pruning_predicate=f64_null_count@2 != row_count@3 AND f64_min@0 <= 2.5 AND 2.5 <= f64_max@1, required_guarantees=[f64 in (2.5)], statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Null=Inexact(0) ScanBytes=Inexact(40))]] statement ok DROP TABLE typed_table;