You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/04/07 08:17:08 UTC
(datafusion) 01/01: feat: Set NDV to Exact(1) for numeric equality filter predicates (#21077)
This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch gh-readonly-queue/main/pr-21077-cdfade51ab35d48936a7ab80d7e6a571fd36f8cb
in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit a4b945e1459289996a0afb402b7b5267ebc78f10
Author: Alessandro Solimando <as...@apache.org>
AuthorDate: Tue Apr 7 10:16:41 2026 +0200
feat: Set NDV to Exact(1) for numeric equality filter predicates (#21077)
## Which issue does this PR close?
- Part of #20766
Related: #20789 (uses NDV for equality filter selectivity, complementary
- this PR improves the NDV output stats, that PR consumes them)
## Rationale for this change
When a filter predicate collapses a column interval to a single value
(e.g. `d_qoy = 1`), the output column can only have one distinct value.
Currently `distinct_count` is always demoted to `Inexact`, losing this
information.
This matters for downstream optimizers that rely on `distinct_count`,
such as join cardinality estimation in
`estimate_inner_join_cardinality`.
## What changes are included in this PR?
In `collect_new_statistics` (filter.rs), when the post-filter interval
has `lower == upper` (both non-null), set `distinct_count` to
`Precision::Exact(1)` instead of demoting the input NDV to `Inexact`.
## Are these changes tested?
Yes, 4 unit tests:
- Equality predicate (`a = 42`) -> NDV becomes `Exact(1)`
- OR predicate (`a = 42 OR a = 22`) -> interval does not collapse, NDV
stays `Inexact`
- AND with mixed predicates (`a = 42 AND b > 10 AND c = 7`) -> `a` and
`c` get `Exact(1)`, `b` stays `Inexact`
- Equality with absent bounds (`a = 42`, no min/max) -> interval
analysis still resolves to `Exact(1)`
## Are there any user-facing changes?
No breaking changes. Statistics consumers will now see `Exact(1)` for
`distinct_count` on columns constrained to a single value by filter
predicates.
Disclaimer: I used AI to assist in the code generation, I have manually
reviewed the output and it matches my intention and understanding.
---------
Co-authored-by: xudong.w <wx...@gmail.com>
---
datafusion/physical-plan/src/filter.rs | 339 ++++++++++++++++++++-
.../sqllogictest/test_files/parquet_statistics.slt | 75 ++++-
2 files changed, 407 insertions(+), 7 deletions(-)
diff --git a/datafusion/physical-plan/src/filter.rs b/datafusion/physical-plan/src/filter.rs
index 81088e5f89..afe2b0ae81 100644
--- a/datafusion/physical-plan/src/filter.rs
+++ b/datafusion/physical-plan/src/filter.rs
@@ -807,15 +807,23 @@ fn collect_new_statistics(
};
};
let (lower, upper) = interval.into_bounds();
- let is_exact = !lower.is_null() && !upper.is_null() && lower == upper;
- let min_value = interval_bound_to_precision(lower, is_exact);
- let max_value = interval_bound_to_precision(upper, is_exact);
+ let is_single_value =
+ !lower.is_null() && !upper.is_null() && lower == upper;
+ let min_value = interval_bound_to_precision(lower, is_single_value);
+ let max_value = interval_bound_to_precision(upper, is_single_value);
+ // When the interval collapses to a single value (equality
+ // predicate), the column has exactly 1 distinct value
+ let capped_distinct_count = if is_single_value {
+ Precision::Exact(1)
+ } else {
+ distinct_count.to_inexact()
+ };
ColumnStatistics {
null_count: input_column_stats[idx].null_count.to_inexact(),
max_value,
min_value,
sum_value: Precision::Absent,
- distinct_count: distinct_count.to_inexact(),
+ distinct_count: capped_distinct_count,
byte_size: input_column_stats[idx].byte_size,
}
},
@@ -2241,4 +2249,327 @@ mod tests {
Ok(())
}
+
+ #[tokio::test]
+ async fn test_filter_statistics_equality_sets_ndv_to_one() -> Result<()> {
+ // a: min=1, max=100, ndv=80
+ let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(400),
+ column_statistics: vec![ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
+ distinct_count: Precision::Inexact(80),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ // a = 42 collapses interval to a single value
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_or_equality_preserves_ndv() -> Result<()> {
+ // a: min=1, max=100, ndv=80
+ let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(400),
+ column_statistics: vec![ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
+ distinct_count: Precision::Inexact(80),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ // a = 42 OR a = 22: interval stays [1, 100], not a single value
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
+ )),
+ Operator::Or,
+ Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(22)))),
+ )),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Inexact(80)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_and_equality_ndv() -> Result<()> {
+ // a: min=1, max=100, ndv=80
+ // b: min=1, max=50, ndv=40
+ // c: min=1, max=200, ndv=150
+ let schema = Schema::new(vec![
+ Field::new("a", DataType::Int32, false),
+ Field::new("b", DataType::Int32, false),
+ Field::new("c", DataType::Int32, false),
+ ]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(1200),
+ column_statistics: vec![
+ ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
+ distinct_count: Precision::Inexact(80),
+ ..Default::default()
+ },
+ ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(50))),
+ distinct_count: Precision::Inexact(40),
+ ..Default::default()
+ },
+ ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(200))),
+ distinct_count: Precision::Inexact(150),
+ ..Default::default()
+ },
+ ],
+ },
+ schema.clone(),
+ ));
+
+ // a = 42 AND b > 10 AND c = 7
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(BinaryExpr::new(
+ Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
+ )),
+ Operator::And,
+ Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("b", 1)),
+ Operator::Gt,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
+ )),
+ )),
+ Operator::And,
+ Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("c", 2)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(7)))),
+ )),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ // a = 42 collapses to single value
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ // b > 10 narrows to [11, 50] but doesn't collapse
+ assert_eq!(
+ statistics.column_statistics[1].distinct_count,
+ Precision::Inexact(40)
+ );
+ // c = 7 collapses to single value
+ assert_eq!(
+ statistics.column_statistics[2].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_equality_absent_bounds_ndv() -> Result<()> {
+ // a: ndv=80, no min/max
+ let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(400),
+ column_statistics: vec![ColumnStatistics {
+ distinct_count: Precision::Inexact(80),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ // a = 42: even without known bounds, interval analysis resolves
+ // the equality to [42, 42], so NDV is correctly set to Exact(1)
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_equality_int8_ndv() -> Result<()> {
+ // a: min=-100, max=100, ndv=50
+ let schema = Schema::new(vec![Field::new("a", DataType::Int8, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(100),
+ column_statistics: vec![ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int8(Some(-100))),
+ max_value: Precision::Inexact(ScalarValue::Int8(Some(100))),
+ distinct_count: Precision::Inexact(50),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int8(Some(42)))),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_equality_int64_ndv() -> Result<()> {
+ // a: min=0, max=1_000_000, ndv=100_000
+ let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100_000),
+ total_byte_size: Precision::Inexact(800_000),
+ column_statistics: vec![ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int64(Some(0))),
+ max_value: Precision::Inexact(ScalarValue::Int64(Some(1_000_000))),
+ distinct_count: Precision::Inexact(100_000),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Int64(Some(42)))),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_equality_float32_ndv() -> Result<()> {
+ // a: min=0.0, max=100.0, ndv=50
+ let schema = Schema::new(vec![Field::new("a", DataType::Float32, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(400),
+ column_statistics: vec![ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Float32(Some(0.0))),
+ max_value: Precision::Inexact(ScalarValue::Float32(Some(100.0))),
+ distinct_count: Precision::Inexact(50),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(Column::new("a", 0)),
+ Operator::Eq,
+ Arc::new(Literal::new(ScalarValue::Float32(Some(42.5)))),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_filter_statistics_equality_reversed_ndv() -> Result<()> {
+ // a: min=1, max=100, ndv=80
+ let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
+ let input = Arc::new(StatisticsExec::new(
+ Statistics {
+ num_rows: Precision::Inexact(100),
+ total_byte_size: Precision::Inexact(400),
+ column_statistics: vec![ColumnStatistics {
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(100))),
+ distinct_count: Precision::Inexact(80),
+ ..Default::default()
+ }],
+ },
+ schema.clone(),
+ ));
+
+ // 42 = a (literal on the left)
+ let predicate = Arc::new(BinaryExpr::new(
+ Arc::new(Literal::new(ScalarValue::Int32(Some(42)))),
+ Operator::Eq,
+ Arc::new(Column::new("a", 0)),
+ ));
+ let filter: Arc<dyn ExecutionPlan> =
+ Arc::new(FilterExec::try_new(predicate, input)?);
+ let statistics = filter.partition_statistics(None)?;
+ assert_eq!(
+ statistics.column_statistics[0].distinct_count,
+ Precision::Exact(1)
+ );
+ Ok(())
+ }
}
diff --git a/datafusion/sqllogictest/test_files/parquet_statistics.slt b/datafusion/sqllogictest/test_files/parquet_statistics.slt
index 1a0e004a63..1073f60a0f 100644
--- a/datafusion/sqllogictest/test_files/parquet_statistics.slt
+++ b/datafusion/sqllogictest/test_files/parquet_statistics.slt
@@ -59,7 +59,7 @@ query TT
EXPLAIN SELECT * FROM test_table WHERE column1 = 1;
----
physical_plan
-01)FilterExec: column1@0 = 1, statistics=[Rows=Inexact(2), Bytes=Inexact(10), [(Col[0]: Min=Exact(Int64(1)) Max=Exact(Int64(1)) Null=Inexact(0) ScanBytes=Inexact(40))]]
+01)FilterExec: column1@0 = 1, statistics=[Rows=Inexact(2), Bytes=Inexact(10), [(Col[0]: Min=Exact(Int64(1)) Max=Exact(Int64(1)) Null=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(40))]]
02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=2, statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Inexact(Int64(1)) Max=Inexact(Int64(4)) Null=Inexact(0) ScanBytes=Inexact(40))]]
03)----DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/test_table/0.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/test_table/1.parquet]]}, projection=[column1], file_type=parquet, predicate=column1@0 = 1, pruning_predicate=column1_null_count@2 != row_count@3 AND column1_min@0 <= 1 AND 1 <= column1_max@1, required_guarantees=[column1 in (1)], statistics=[Rows=Inexact(5), Bytes=I [...]
@@ -84,7 +84,7 @@ query TT
EXPLAIN SELECT * FROM test_table WHERE column1 = 1;
----
physical_plan
-01)FilterExec: column1@0 = 1, statistics=[Rows=Inexact(2), Bytes=Inexact(10), [(Col[0]: Min=Exact(Int64(1)) Max=Exact(Int64(1)) Null=Inexact(0) ScanBytes=Inexact(40))]]
+01)FilterExec: column1@0 = 1, statistics=[Rows=Inexact(2), Bytes=Inexact(10), [(Col[0]: Min=Exact(Int64(1)) Max=Exact(Int64(1)) Null=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(40))]]
02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=2, statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Inexact(Int64(1)) Max=Inexact(Int64(4)) Null=Inexact(0) ScanBytes=Inexact(40))]]
03)----DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/test_table/0.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/test_table/1.parquet]]}, projection=[column1], file_type=parquet, predicate=column1@0 = 1, pruning_predicate=column1_null_count@2 != row_count@3 AND column1_min@0 <= 1 AND 1 <= column1_max@1, required_guarantees=[column1 in (1)], statistics=[Rows=Inexact(5), Bytes=I [...]
@@ -109,7 +109,7 @@ query TT
EXPLAIN SELECT * FROM test_table WHERE column1 = 1;
----
physical_plan
-01)FilterExec: column1@0 = 1, statistics=[Rows=Absent, Bytes=Absent, [(Col[0]: Min=Exact(Int64(1)) Max=Exact(Int64(1)))]]
+01)FilterExec: column1@0 = 1, statistics=[Rows=Absent, Bytes=Absent, [(Col[0]: Min=Exact(Int64(1)) Max=Exact(Int64(1)) Distinct=Exact(1))]]
02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=2, statistics=[Rows=Absent, Bytes=Absent, [(Col[0]:)]]
03)----DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/test_table/0.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/test_table/1.parquet]]}, projection=[column1], file_type=parquet, predicate=column1@0 = 1, pruning_predicate=column1_null_count@2 != row_count@3 AND column1_min@0 <= 1 AND 1 <= column1_max@1, required_guarantees=[column1 in (1)], statistics=[Rows=Absent, Bytes=Absen [...]
@@ -117,6 +117,75 @@ physical_plan
statement ok
DROP TABLE test_table;
+######
+# Equality filter NDV=Exact(1) for different numeric types
+######
+
+statement ok
+set datafusion.execution.collect_statistics = true;
+
+query I
+COPY (
+ SELECT
+ arrow_cast(v, 'Int8') AS i8,
+ arrow_cast(v, 'Int64') AS i64,
+ arrow_cast(v + 0.5, 'Float32') AS f32,
+ arrow_cast(v + 0.5, 'Float64') AS f64
+ FROM (VALUES (1), (2), (3), (4), (5)) AS t(v)
+)
+TO 'test_files/scratch/parquet_statistics/typed_table.parquet'
+STORED AS PARQUET;
+----
+5
+
+statement ok
+CREATE EXTERNAL TABLE typed_table (
+ i8 TINYINT,
+ i64 BIGINT,
+ f32 FLOAT,
+ f64 DOUBLE
+) STORED AS PARQUET
+LOCATION 'test_files/scratch/parquet_statistics/typed_table.parquet';
+
+# Int8 equality
+query TT
+EXPLAIN SELECT i8 FROM typed_table WHERE i8 = 2;
+----
+physical_plan
+01)FilterExec: i8@0 = 2, statistics=[Rows=Inexact(1), Bytes=Inexact(1), [(Col[0]: Min=Exact(Int8(2)) Max=Exact(Int8(2)) Null=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(5))]]
+02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, statistics=[Rows=Inexact(5), Bytes=Inexact(5), [(Col[0]: Min=Inexact(Int8(1)) Max=Inexact(Int8(5)) Null=Inexact(0) ScanBytes=Inexact(5))]]
+03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_statistics/typed_table.parquet]]}, projection=[i8], file_type=parquet, predicate=i8@0 = 2, pruning_predicate=i8_null_count@2 != row_count@3 AND i8_min@0 <= 2 AND 2 <= i8_max@1, required_guarantees=[i8 in (2)], statistics=[Rows=Inexact(5), Bytes=Inexact(5), [(Col[0]: Min=Inexact(Int8(1)) Max=Inexact(Int8(5)) Null=Inexact(0) ScanBytes=Inexact(5))]]
+
+# Int64 equality
+query TT
+EXPLAIN SELECT i64 FROM typed_table WHERE i64 = 2;
+----
+physical_plan
+01)FilterExec: i64@0 = 2, statistics=[Rows=Inexact(1), Bytes=Inexact(8), [(Col[0]: Min=Exact(Int64(2)) Max=Exact(Int64(2)) Null=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(40))]]
+02)--RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Inexact(Int64(1)) Max=Inexact(Int64(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=[i64], file_type=parquet, predicate=i64@1 = 2, pruning_predicate=i64_null_count@2 != row_count@3 AND i64_min@0 <= 2 AND 2 <= i64_max@1, required_guarantees=[i64 in (2)], statistics=[Rows=Inexact(5), Bytes=Inexact(40), [(Col[0]: Min=Inexact(Int64(1)) Max=Inexact(Int64(5)) Null=Inexact(0) ScanBytes=Inexact(40))]]
+
+# Float32 equality
+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(20))]]
+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=I [...]
+
+# 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=Inexact(0) Distinct=Exact(1) ScanBytes=Inexact(40))]]
+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))]]
+
+statement ok
+DROP TABLE typed_table;
+
# Config reset
statement ok
RESET datafusion.execution.collect_statistics;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org