From e091ff9bbe8703f0c9653b7e9459d7ffffe5234f Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Wed, 28 Oct 2026 08:46:20 +0200 Subject: [PATCH 1/2] Do not trust Parquet float maximums that leave out NaN Parquet writers compute float min/max without NaN, while NaN sorts above every number. A row group, page or file holding 1.0 and NaN has max 1.0, so trusting that maximum pruned it for c > 2, answered MAX(c) with 1.0 from file statistics, and read the column as the constant 1.0. Treat a float column's Parquet maximum as unknown in row-group pruning, page pruning and the file statistics built from the footer. The minimum stays a lower bound. Float columns lose max-side pruning because Parquet statistics carry no NaN count. Co-Authored-By: Claude Opus 5.5 --- datafusion/core/tests/parquet/page_pruning.rs | 5 +- .../core/tests/parquet/row_group_pruning.rs | 9 +-- datafusion/datasource-parquet/src/metadata.rs | 50 +++++++++++++++ .../datasource-parquet/src/page_filter.rs | 4 ++ .../src/row_group_filter.rs | 55 ++++++++++++++++- .../parquet_float_nan_statistics.slt | 61 +++++++++++++++++++ .../test_files/parquet_statistics.slt | 12 ++-- 7 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt diff --git a/datafusion/core/tests/parquet/page_pruning.rs b/datafusion/core/tests/parquet/page_pruning.rs index 372a7a601d492..a8fdad0eb38d0 100644 --- a/datafusion/core/tests/parquet/page_pruning.rs +++ b/datafusion/core/tests/parquet/page_pruning.rs @@ -765,12 +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 + // only "f >= 0.1" could prune, and only by the float maximum, which Parquet statistics + // compute without NaN (NaN sorts above every number), so no page is pruned 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..d1ff34ddec16a 100644 --- a/datafusion/core/tests/parquet/row_group_pruning.rs +++ b/datafusion/core/tests/parquet/row_group_pruning.rs @@ -836,15 +836,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 + // only "f >= 0.1" could prune, and only by the float maximum, which Parquet statistics + // compute without NaN (NaN sorts above every number), so no row group is pruned 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 38db1e22ee6c0..41a611b10fdc1 100644 --- a/datafusion/datasource-parquet/src/metadata.rs +++ b/datafusion/datasource-parquet/src/metadata.rs @@ -584,6 +584,16 @@ fn min_max_aggregate_data_type(input_type: &DataType) -> &DataType { } } +/// Whether a column's Parquet maximum may sit below some of its values. +/// +/// Parquet writers leave NaN out of floating-point statistics (the Parquet spec asks them to), +/// while NaN sorts above every other value. A float column's maximum therefore only bounds the +/// non-NaN values: it must not prune `col > x` or stand for the column's `MAX`. The minimum stays +/// a valid lower bound, since NaN is never below it. +pub(crate) fn max_statistic_may_omit_nan(data_type: &DataType) -> bool { + min_max_aggregate_data_type(data_type).is_floating() +} + fn create_max_min_accs( schema: &Schema, ) -> (Vec>, Vec>) { @@ -591,6 +601,9 @@ fn create_max_min_accs( .fields() .iter() .map(|field| { + if max_statistic_may_omit_nan(field.data_type()) { + return None; + } MaxAccumulator::try_new(min_max_aggregate_data_type(field.data_type())).ok() }) .collect(); @@ -1138,6 +1151,43 @@ mod tests { Ok(()) } + #[test] + fn float_maximum_is_absent_from_file_statistics() -> Result<()> { + use arrow::array::{Float64Array, RecordBatch}; + use parquet::arrow::ArrowWriter; + let schema = Arc::new(Schema::new(vec![ + Field::new("f", DataType::Float64, true), + Field::new("i", DataType::Int32, true), + ])); + let batch = RecordBatch::try_new( + Arc::clone(&schema), + vec![ + Arc::new(Float64Array::from(vec![1.0, f64::NAN])), + Arc::new(Int32Array::from(vec![1, 2])), + ], + )?; + let mut buffer = Vec::new(); + let mut writer = ArrowWriter::try_new(&mut buffer, Arc::clone(&schema), None)?; + writer.write(&batch)?; + writer.close()?; + let metadata = + ParquetMetaDataReader::new().parse_and_finish(&bytes::Bytes::from(buffer))?; + let statistics = + DFParquetMetadata::statistics_from_parquet_metadata(&metadata, &schema)?; + // The writer left NaN out of the float statistics, so max 1.0 would hide the NaN row. + let float = &statistics.column_statistics[0]; + assert_eq!(float.max_value, Precision::Absent); + assert_eq!( + float.min_value.get_value(), + Some(&ScalarValue::Float64(Some(1.0))) + ); + assert_eq!( + statistics.column_statistics[1].max_value.get_value(), + Some(&ScalarValue::Int32(Some(2))) + ); + Ok(()) + } + #[test] fn test_has_any_exact_match() { // Case 1: Mixed exact and inexact matches diff --git a/datafusion/datasource-parquet/src/page_filter.rs b/datafusion/datasource-parquet/src/page_filter.rs index 6bc1aca667981..63696118aef5e 100644 --- a/datafusion/datasource-parquet/src/page_filter.rs +++ b/datafusion/datasource-parquet/src/page_filter.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use super::metrics::ParquetFileMetrics; use crate::ParquetAccessPlan; +use crate::metadata::max_statistic_may_omit_nan; use arrow::array::BooleanArray; use arrow::{ @@ -577,6 +578,9 @@ impl PruningStatistics for PagesPruningStatistics<'_> { } fn max_values(&self, _column: &datafusion_common::Column) -> Option { + if max_statistic_may_omit_nan(self.converter.arrow_field().data_type()) { + return None; + } match self.converter.data_page_maxes( self.column_index, self.offset_index, diff --git a/datafusion/datasource-parquet/src/row_group_filter.rs b/datafusion/datasource-parquet/src/row_group_filter.rs index ddf71bb7e6d95..debe7473be9c8 100644 --- a/datafusion/datasource-parquet/src/row_group_filter.rs +++ b/datafusion/datasource-parquet/src/row_group_filter.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use super::{ParquetAccessPlan, ParquetFileMetrics, RowGroupAccess}; use crate::bloom_filter::BloomFilterStatistics; +use crate::metadata::max_statistic_may_omit_nan; use arrow::array::{ArrayRef, BooleanArray, UInt64Array}; use arrow::datatypes::Schema; use datafusion_common::pruning::PruningStatistics; @@ -495,9 +496,11 @@ impl PruningStatistics for RowGroupPruningStatistics<'_> { } fn max_values(&self, column: &Column) -> Option { - self.statistics_converter(column) - .and_then(|c| Ok(c.row_group_maxes(self.metadata_iter())?)) - .ok() + let converter = self.statistics_converter(column).ok()?; + if max_statistic_may_omit_nan(converter.arrow_field().data_type()) { + return None; + } + converter.row_group_maxes(self.metadata_iter()).ok() } fn num_containers(&self) -> usize { @@ -660,6 +663,52 @@ mod tests { assert_pruned(row_groups, ExpectedPruning::Some(vec![1])) } + #[test] + fn row_group_float_maximum_does_not_prune() { + use datafusion_expr::{col, lit}; + // Parquet statistics leave NaN out, and NaN sorts above every number: a row group + // holding 1.0 and NaN has max 1.0 but a row matching `c1 > 15`. The minimum still prunes. + let schema = + Arc::new(Schema::new(vec![Field::new("c1", DataType::Float64, true)])); + let field = PrimitiveTypeField::new("c1", PhysicalType::DOUBLE); + let schema_descr = get_test_schema_descr(vec![field]); + let row_group = |min: f64, max: f64| { + get_row_group_meta_data( + &schema_descr, + vec![ParquetStatistics::double( + Some(min), + Some(max), + None, + Some(0), + false, + )], + ) + }; + let row_groups_for = |expr: datafusion_expr::Expr| { + let expr = logical2physical(&expr, &schema); + let pruning_predicate = + build_test_pruning_predicate(expr, Arc::clone(&schema)); + let mut row_groups = + RowGroupAccessPlanFilter::new(ParquetAccessPlan::new_all(2)); + row_groups.prune_by_statistics( + &schema, + &schema_descr, + &[row_group(1.0, 10.0), row_group(11.0, 20.0)], + &pruning_predicate, + &parquet_file_metrics(), + ); + row_groups + }; + assert_pruned( + row_groups_for(col("c1").gt(lit(15.0))), + ExpectedPruning::None, + ); + assert_pruned( + row_groups_for(col("c1").lt(lit(5.0))), + ExpectedPruning::Some(vec![0]), + ); + } + #[test] fn row_group_fully_matched_requires_known_non_null_predicate_columns() { use datafusion_expr::{col, lit}; diff --git a/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt b/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt new file mode 100644 index 0000000000000..3c3996526c751 --- /dev/null +++ b/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt @@ -0,0 +1,61 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# NaN rows of a float column must survive Parquet statistics. +# +# Parquet writers leave NaN out of float min/max statistics, while NaN sorts above every +# number. A file holding 1.0 and NaN therefore has min = max = 1.0; trusting that maximum +# prunes the file for `column1 > 2`, answers MAX with 1.0, and reads the column as the +# constant 1.0. + +query I +COPY (VALUES (1.0), ('NaN'::double), (1.0)) +TO 'test_files/scratch/parquet_float_nan_statistics/t/0.parquet' +STORED AS PARQUET; +---- +3 + +statement ok +CREATE EXTERNAL TABLE t +STORED AS PARQUET +LOCATION 'test_files/scratch/parquet_float_nan_statistics/t'; + +query I +SELECT count(*) FROM t WHERE column1 > 2; +---- +1 + +query R +SELECT max(column1) FROM t; +---- +NaN + +query R +SELECT column1 FROM t ORDER BY column1; +---- +1 +1 +NaN + +# The minimum is still a bound: NaN is never below it. +query I +SELECT count(*) FROM t WHERE column1 < 1; +---- +0 + +statement ok +DROP TABLE t; diff --git a/datafusion/sqllogictest/test_files/parquet_statistics.slt b/datafusion/sqllogictest/test_files/parquet_statistics.slt index 9cf6b1e0381d1..3382e1001df9d 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]: Min=Inexact(Float32(1.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)) 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]: Min=Inexact(Float64(1.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)) Null=Inexact(0) ScanBytes=Inexact(40))]] statement ok DROP TABLE typed_table; From a1e4d19c6cb0d349df072120ed9cc211cfe204a2 Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Wed, 28 Oct 2026 10:07:20 +0200 Subject: [PATCH 2/2] Mark zero float footer minimums inexact; test NaN TopK and zero MIN Parquet writers record a zero minimum as -0.0 whatever the sign of the stored zeros, so MIN over {0.0, 1.0} came back as -0.0 from footer statistics. Keep such minimums as bounds only. The opener tests now prune a float column by its minimum, since its maximum is no longer a bound. Co-Authored-By: Claude Opus 5.5 --- datafusion/datasource-parquet/src/metadata.rs | 25 ++++++++++++++--- .../datasource-parquet/src/opener/mod.rs | 9 +++--- .../parquet_float_nan_statistics.slt | 28 ++++++++++++++++++- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/datafusion/datasource-parquet/src/metadata.rs b/datafusion/datasource-parquet/src/metadata.rs index 41a611b10fdc1..7665025c9c218 100644 --- a/datafusion/datasource-parquet/src/metadata.rs +++ b/datafusion/datasource-parquet/src/metadata.rs @@ -587,13 +587,24 @@ fn min_max_aggregate_data_type(input_type: &DataType) -> &DataType { /// Whether a column's Parquet maximum may sit below some of its values. /// /// Parquet writers leave NaN out of floating-point statistics (the Parquet spec asks them to), -/// while NaN sorts above every other value. A float column's maximum therefore only bounds the -/// non-NaN values: it must not prune `col > x` or stand for the column's `MAX`. The minimum stays -/// a valid lower bound, since NaN is never below it. +/// while a NaN with its sign bit clear (the `'NaN'` literal) sorts above every other value. A +/// float column's maximum therefore only bounds the non-NaN values: it must not prune `col > x` +/// or stand for the column's `MAX`. The minimum stays a valid lower bound for such a NaN. pub(crate) fn max_statistic_may_omit_nan(data_type: &DataType) -> bool { min_max_aggregate_data_type(data_type).is_floating() } +/// Whether a float minimum is a zero, which Parquet writers record as `-0.0` whatever the sign +/// of the stored zeros: it bounds the column but is not its exact minimum. +fn min_statistic_is_signed_zero(min: &ScalarValue) -> bool { + match min { + ScalarValue::Float16(Some(value)) => value.to_f32() == 0.0, + ScalarValue::Float32(Some(value)) => *value == 0.0, + ScalarValue::Float64(Some(value)) => *value == 0.0, + _ => false, + } +} + fn create_max_min_accs( schema: &Schema, ) -> (Vec>, Vec>) { @@ -650,7 +661,13 @@ impl StatisticsAccumulators<'_> { self.is_min_value_exact.get(i).unwrap(), ) { (Some(min_value), Some(true)) => { - min_value.evaluate().ok().map(Precision::Exact) + min_value.evaluate().ok().map(|min| { + if min_statistic_is_signed_zero(&min) { + Precision::Inexact(min) + } else { + Precision::Exact(min) + } + }) } (Some(min_value), Some(false)) | (Some(min_value), None) => { min_value.evaluate().ok().map(Precision::Inexact) diff --git a/datafusion/datasource-parquet/src/opener/mod.rs b/datafusion/datasource-parquet/src/opener/mod.rs index a9f4662f23c94..81a9deace6ec8 100644 --- a/datafusion/datasource-parquet/src/opener/mod.rs +++ b/datafusion/datasource-parquet/src/opener/mod.rs @@ -2489,8 +2489,9 @@ 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 = 0.5` should exclude all rows (by the minimum: a float maximum is not a + // bound, since Parquet statistics leave NaN out) + let expr = col("b").eq(lit(ScalarValue::Float32(Some(0.5)))); let predicate = logical2physical(&expr, &schema); let opener = make_opener(predicate); let stream = open_file(&opener, file).await.unwrap(); @@ -2627,7 +2628,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(0.5))); let predicate = logical2physical(&expr, &table_schema); let opener = make_opener(predicate); let stream = open_file(&opener, file.clone()).await.unwrap(); @@ -2636,7 +2637,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(0.5))); let predicate = logical2physical(&expr, &table_schema); let opener = make_opener(predicate); let stream = open_file(&opener, file).await.unwrap(); diff --git a/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt b/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt index 3c3996526c751..f04deb7bacbd9 100644 --- a/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt +++ b/datafusion/sqllogictest/test_files/parquet_float_nan_statistics.slt @@ -51,7 +51,12 @@ SELECT column1 FROM t ORDER BY column1; 1 NaN -# The minimum is still a bound: NaN is never below it. +query R +SELECT column1 FROM t ORDER BY column1 DESC LIMIT 1; +---- +NaN + +# The minimum is still a bound: the 'NaN' literal is never below it. query I SELECT count(*) FROM t WHERE column1 < 1; ---- @@ -59,3 +64,24 @@ SELECT count(*) FROM t WHERE column1 < 1; statement ok DROP TABLE t; + +# Parquet writers record a zero minimum as -0.0, so MIN must not be answered from it. +query I +COPY (VALUES (0.0), (1.0)) +TO 'test_files/scratch/parquet_float_nan_statistics/zero/0.parquet' +STORED AS PARQUET; +---- +2 + +statement ok +CREATE EXTERNAL TABLE zero +STORED AS PARQUET +LOCATION 'test_files/scratch/parquet_float_nan_statistics/zero'; + +query T +SELECT CAST(min(column1) AS VARCHAR) FROM zero; +---- +0.0 + +statement ok +DROP TABLE zero;