blob: b518e826f04b83c1a1e377f6b216864dcb24e0d4 [file]
// 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.
use super::normalize_float;
use arrow::array::{
Array, ArrayRef, AsArray, FixedSizeListArray, Float32Array, Float64Array, LargeListArray,
ListArray, StructArray,
};
use arrow::datatypes::{DataType, Float32Type, Float64Type, Schema};
use arrow::record_batch::RecordBatch;
use datafusion::common::{Result, ScalarValue};
use datafusion::logical_expr::ColumnarValue;
use datafusion::physical_expr::PhysicalExpr;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
/// Spark's `NormalizeNaNAndZero` expression, which applies [`normalize_float`] to a Float32 or
/// Float64 child.
#[derive(Debug, Eq)]
pub struct NormalizeNaNAndZero {
pub data_type: DataType,
pub child: Arc<dyn PhysicalExpr>,
}
impl PartialEq for NormalizeNaNAndZero {
fn eq(&self, other: &Self) -> bool {
self.child.eq(&other.child) && self.data_type.eq(&other.data_type)
}
}
impl Hash for NormalizeNaNAndZero {
fn hash<H: Hasher>(&self, state: &mut H) {
self.child.hash(state);
self.data_type.hash(state);
}
}
impl NormalizeNaNAndZero {
pub fn new(data_type: DataType, child: Arc<dyn PhysicalExpr>) -> Self {
Self { data_type, child }
}
/// Whether `evaluate` cannot fail, which it does by panicking for anything but a float.
pub(crate) fn is_infallible(&self) -> bool {
matches!(self.data_type, DataType::Float32 | DataType::Float64)
}
/// Wraps a Float32 or Float64 key so that keys equal under Spark's SQL ordering also compare
/// equal in Arrow. Other types are returned as is, and so is a key that is already wrapped,
/// because Spark may have normalized a partition or join key itself.
pub fn wrap_if_needed(
child: Arc<dyn PhysicalExpr>,
schema: &Schema,
) -> Result<Arc<dyn PhysicalExpr>> {
let data_type = child.data_type(schema)?;
if matches!(data_type, DataType::Float32 | DataType::Float64)
&& child.downcast_ref::<NormalizeNaNAndZero>().is_none()
{
Ok(Arc::new(Self::new(data_type, child)))
} else {
Ok(child)
}
}
}
impl PhysicalExpr for NormalizeNaNAndZero {
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
fn data_type(&self, input_schema: &Schema) -> Result<DataType> {
self.child.data_type(input_schema)
}
fn nullable(&self, input_schema: &Schema) -> Result<bool> {
self.child.nullable(input_schema)
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
let cv = self.child.evaluate(batch)?;
let array = cv.into_array(batch.num_rows())?;
match &self.data_type {
DataType::Float32 | DataType::Float64 => {
Ok(ColumnarValue::Array(normalize_floats(&array)))
}
dt => panic!("Unexpected data type {dt:?}"),
}
}
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
vec![&self.child]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
Ok(Arc::new(NormalizeNaNAndZero::new(
self.data_type.clone(),
Arc::clone(&children[0]),
)))
}
}
impl Display for NormalizeNaNAndZero {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "FloatNormalize [child: {}]", self.child)
}
}
/// Normalizes nested IN operands, preserving constants for static membership lookup.
#[derive(Debug, Eq)]
pub struct NormalizeNestedFloats {
child: Arc<dyn PhysicalExpr>,
}
impl NormalizeNestedFloats {
/// Wrap nested floating-point operands only; scalar floats keep their existing semantics.
pub fn wrap_if_needed(
child: Arc<dyn PhysicalExpr>,
schema: &Schema,
) -> Result<Arc<dyn PhysicalExpr>> {
let dt = child.data_type(schema)?;
if matches!(
dt,
DataType::List(_)
| DataType::LargeList(_)
| DataType::FixedSizeList(_, _)
| DataType::Struct(_)
) && has_float_leaf(&dt)
{
Ok(Arc::new(Self { child }))
} else {
Ok(child)
}
}
}
impl PartialEq for NormalizeNestedFloats {
fn eq(&self, other: &Self) -> bool {
self.child.eq(&other.child)
}
}
impl Hash for NormalizeNestedFloats {
fn hash<H: Hasher>(&self, state: &mut H) {
self.child.hash(state);
}
}
impl Display for NormalizeNestedFloats {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "NormalizeNestedFloats({})", self.child)
}
}
impl PhysicalExpr for NormalizeNestedFloats {
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
fn data_type(&self, schema: &Schema) -> Result<DataType> {
self.child.data_type(schema)
}
fn nullable(&self, schema: &Schema) -> Result<bool> {
self.child.nullable(schema)
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
match self.child.evaluate(batch)? {
ColumnarValue::Array(array) => {
Ok(ColumnarValue::Array(normalize_nested_floats(&array)))
}
ColumnarValue::Scalar(value) => {
let array = normalize_nested_floats(&value.to_array()?);
Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(
&array, 0,
)?))
}
}
}
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
vec![&self.child]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
if children.len() != 1 {
return datafusion::common::internal_err!("NormalizeNestedFloats expects one child");
}
Ok(Arc::new(Self {
child: Arc::clone(&children[0]),
}))
}
}
/// Applies [`normalize_float`] to a Float32 or Float64 array. Any other array is returned as is,
/// including a nested one: [`normalize_nested_floats`] reaches floats inside lists and structs.
pub fn normalize_floats(array: &ArrayRef) -> ArrayRef {
match array.data_type() {
DataType::Float32 => {
let normalized: Float32Array =
array.as_primitive::<Float32Type>().unary(normalize_float);
Arc::new(normalized)
}
DataType::Float64 => {
let normalized: Float64Array =
array.as_primitive::<Float64Type>().unary(normalize_float);
Arc::new(normalized)
}
_ => Arc::clone(array),
}
}
/// Whether a Float32 or Float64 field occurs at any depth of a list or struct type. The keys and
/// values of a map are not searched.
pub fn has_float_leaf(dt: &DataType) -> bool {
match dt {
DataType::Float32 | DataType::Float64 => true,
DataType::List(field) | DataType::LargeList(field) | DataType::FixedSizeList(field, _) => {
has_float_leaf(field.data_type())
}
DataType::Struct(fields) => fields.iter().any(|f| has_float_leaf(f.data_type())),
_ => false,
}
}
/// Recursively rebuilds nested arrays with `-0.0` normalized to `0.0` and NaN canonicalized
/// in any Float32/Float64 leaves.
///
/// Subtrees without a float leaf are returned as is. The guard is applied at every level, not
/// just by the caller, so that a float-free sibling of a float field is never rebuilt. That
/// keeps the rebuild proportional to the float data, and it also leaves empty structs alone:
/// `StructArray::new` cannot infer a length from zero columns and would panic. An empty struct
/// reaches this code through Iceberg's `_partition` metadata column on an unpartitioned table.
pub fn normalize_nested_floats(array: &ArrayRef) -> ArrayRef {
if !has_float_leaf(array.data_type()) {
return Arc::clone(array);
}
match array.data_type() {
DataType::Float32 | DataType::Float64 => normalize_floats(array),
DataType::List(field) => {
let list = array.as_list::<i32>();
let normalized_values = normalize_nested_floats(list.values());
Arc::new(ListArray::new(
Arc::clone(field),
list.offsets().clone(),
normalized_values,
list.nulls().cloned(),
))
}
DataType::LargeList(field) => {
let list = array.as_list::<i64>();
let normalized_values = normalize_nested_floats(list.values());
Arc::new(LargeListArray::new(
Arc::clone(field),
list.offsets().clone(),
normalized_values,
list.nulls().cloned(),
))
}
DataType::FixedSizeList(field, size) => {
let list = array.as_fixed_size_list();
let normalized_values = normalize_nested_floats(list.values());
Arc::new(FixedSizeListArray::new(
Arc::clone(field),
*size,
normalized_values,
list.nulls().cloned(),
))
}
DataType::Struct(_) => {
let s = array.as_struct();
let normalized_columns: Vec<ArrayRef> =
s.columns().iter().map(normalize_nested_floats).collect();
Arc::new(StructArray::new(
s.fields().clone(),
normalized_columns,
s.nulls().cloned(),
))
}
_ => Arc::clone(array),
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::Float64Builder;
use arrow::array::Int32Array;
use arrow::array::ListBuilder;
use arrow::datatypes::{Field, Fields};
#[test]
fn test_nested_membership_shapes() -> Result<()> {
use arrow::array::BooleanArray;
use datafusion::physical_expr::expressions::{in_list, Column, Literal};
let left32: ArrayRef = Arc::new(ListArray::from_iter_primitive::<Float32Type, _, _>([
Some(vec![Some(-0.0)]),
Some(vec![Some(0.0)]),
Some(vec![Some(f32::from_bits(0xffc00001))]),
Some(vec![None]),
Some(vec![]),
None,
Some(vec![Some(3.0)]),
]));
let right32: ArrayRef = Arc::new(ListArray::from_iter_primitive::<Float32Type, _, _>([
Some(vec![Some(0.0)]),
Some(vec![Some(-0.0)]),
Some(vec![Some(f32::from_bits(0x7fc00002))]),
Some(vec![None]),
Some(vec![]),
None,
Some(vec![Some(4.0)]),
]));
let left64: ArrayRef = Arc::new(ListArray::from_iter_primitive::<Float64Type, _, _>([
Some(vec![Some(-0.0)]),
Some(vec![Some(0.0)]),
Some(vec![Some(f64::from_bits(0xfff8000000000001))]),
Some(vec![None]),
Some(vec![]),
None,
Some(vec![Some(3.0)]),
]));
let right64: ArrayRef = Arc::new(ListArray::from_iter_primitive::<Float64Type, _, _>([
Some(vec![Some(0.0)]),
Some(vec![Some(-0.0)]),
Some(vec![Some(f64::from_bits(0x7ff8000000000002))]),
Some(vec![None]),
Some(vec![]),
None,
Some(vec![Some(4.0)]),
]));
for (left, right) in [(left32, right32), (left64, right64)] {
// Test lists directly, then lists nested inside another list and a struct.
for depth in 0..3 {
let nest = |array: ArrayRef| -> ArrayRef {
match depth {
0 => array,
1 => Arc::new(ListArray::new(
Arc::new(Field::new("item", array.data_type().clone(), true)),
arrow::buffer::OffsetBuffer::from_lengths(std::iter::repeat_n(
1,
array.len(),
)),
Arc::clone(&array),
array.nulls().cloned(),
)),
_ => Arc::new(StructArray::new(
vec![Arc::new(Field::new("v", array.data_type().clone(), true))].into(),
vec![Arc::clone(&array)],
array.nulls().cloned(),
)),
}
};
let left = nest(Arc::clone(&left));
let right = nest(Arc::clone(&right));
let schema = Arc::new(Schema::new(vec![
Field::new("a", left.data_type().clone(), true),
Field::new("b", right.data_type().clone(), true),
]));
let batch =
RecordBatch::try_new(Arc::clone(&schema), vec![left, Arc::clone(&right)])?;
let a =
NormalizeNestedFloats::wrap_if_needed(Arc::new(Column::new("a", 0)), &schema)?;
let b =
NormalizeNestedFloats::wrap_if_needed(Arc::new(Column::new("b", 1)), &schema)?;
for negated in [false, true] {
let expr = in_list(Arc::clone(&a), vec![Arc::clone(&b)], &negated, &schema)?;
let actual = expr.evaluate(&batch)?.into_array(7)?;
let expected = BooleanArray::from(vec![
Some(!negated),
Some(!negated),
Some(!negated),
Some(!negated),
Some(!negated),
None,
Some(negated),
]);
assert_eq!(actual.as_boolean(), &expected);
// Constant candidates exercise the static hash lookup, including distinct NaN bits.
for row in [0, 1, 2, 3, 4, 6] {
let literal =
Arc::new(Literal::new(ScalarValue::try_from_array(&right, row)?));
let candidate = NormalizeNestedFloats::wrap_if_needed(literal, &schema)?;
let expr = in_list(Arc::clone(&a), vec![candidate], &negated, &schema)?;
let actual = expr.evaluate(&batch)?.into_array(7)?;
assert_eq!(actual.as_boolean().value(row), expected.value(row));
}
}
}
}
Ok(())
}
#[test]
fn test_membership_wrapper() -> Result<()> {
use datafusion::physical_expr::expressions::{in_list, Column, Literal};
// Noncanonical NaNs and negative zero must normalize identically on both paths.
let values: ArrayRef = Arc::new(Float64Array::from(vec![
Some(-0.0),
Some(f64::from_bits(0x7ff8000000000001)),
None,
Some(2.0),
]));
let input: ArrayRef = Arc::new(StructArray::new(
vec![Arc::new(Field::new("v", DataType::Float64, true))].into(),
vec![values],
None,
));
let schema = Arc::new(Schema::new(vec![Field::new(
"a",
input.data_type().clone(),
true,
)]));
let batch = RecordBatch::try_new(Arc::clone(&schema), vec![Arc::clone(&input)])?;
let column: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
let wrapped = NormalizeNestedFloats::wrap_if_needed(Arc::clone(&column), &schema)?;
assert_eq!(wrapped.data_type(&schema)?, column.data_type(&schema)?);
assert_eq!(wrapped.nullable(&schema)?, column.nullable(&schema)?);
assert!(matches!(wrapped.evaluate(&batch)?, ColumnarValue::Array(_)));
for row in 0..input.len() {
let literal: Arc<dyn PhysicalExpr> =
Arc::new(Literal::new(ScalarValue::try_from_array(&input, row)?));
let literal = NormalizeNestedFloats::wrap_if_needed(literal, &schema)?;
assert!(matches!(
literal.evaluate(&RecordBatch::new_empty(Arc::clone(&schema)))?,
ColumnarValue::Scalar(_)
));
let expr = in_list(Arc::clone(&wrapped), vec![literal], &false, &schema)?;
let result = expr.evaluate(&batch)?.into_array(batch.num_rows())?;
assert!(result.as_boolean().value(row));
}
let plain: Arc<dyn PhysicalExpr> = Arc::new(Literal::new(ScalarValue::Int32(Some(1))));
assert!(Arc::ptr_eq(
&plain,
&NormalizeNestedFloats::wrap_if_needed(Arc::clone(&plain), &schema)?
));
Ok(())
}
#[test]
fn test_has_float_leaf() {
assert!(has_float_leaf(&DataType::Float64));
assert!(has_float_leaf(&DataType::List(Arc::new(Field::new(
"item",
DataType::Float32,
true
)))));
assert!(has_float_leaf(&DataType::Struct(
vec![
Arc::new(Field::new("a", DataType::Int32, true)),
Arc::new(Field::new("b", DataType::Float64, true)),
]
.into()
)));
assert!(!has_float_leaf(&DataType::Int32));
assert!(!has_float_leaf(&DataType::List(Arc::new(Field::new(
"item",
DataType::Int32,
true
)))));
}
#[test]
fn test_normalize_flat_floats() {
let arr: ArrayRef = Arc::new(Float64Array::from(vec![
Some(-0.0),
Some(0.0),
Some(f64::NAN),
Some(-f64::NAN),
None,
Some(1.5),
]));
let normalized = normalize_nested_floats(&arr);
let normalized = normalized.as_primitive::<Float64Type>();
assert_eq!(normalized.value(0).to_bits(), 0.0f64.to_bits());
assert_eq!(normalized.value(1).to_bits(), 0.0f64.to_bits());
assert_eq!(normalized.value(2).to_bits(), f64::NAN.to_bits());
assert_eq!(normalized.value(3).to_bits(), f64::NAN.to_bits());
assert!(normalized.is_null(4));
assert_eq!(normalized.value(5), 1.5);
}
/// Range partition boundaries use `normalize_floats` to match sort keys, which are only
/// normalized when they are floats themselves, so a nested array must come back unchanged.
#[test]
fn test_normalize_floats_leaves_nested_arrays_alone() {
let flat: ArrayRef = Arc::new(Float32Array::from(vec![
Some(-0.0),
Some(f32::from_bits(0xffc0_0001)),
None,
]));
let normalized = normalize_floats(&flat);
let normalized = normalized.as_primitive::<Float32Type>();
assert_eq!(normalized.value(0).to_bits(), 0.0f32.to_bits());
assert_eq!(normalized.value(1).to_bits(), f32::NAN.to_bits());
assert!(normalized.is_null(2));
let mut builder = ListBuilder::new(Float64Builder::new());
builder.values().append_value(-0.0);
builder.append(true);
let nested: ArrayRef = Arc::new(builder.finish());
assert!(Arc::ptr_eq(&normalize_floats(&nested), &nested));
}
#[test]
fn test_wrap_float_keys_once() -> Result<()> {
use datafusion::physical_expr::expressions::Column;
let list = DataType::List(Arc::new(Field::new("item", DataType::Float64, true)));
let schema = Schema::new(vec![
Field::new("f32", DataType::Float32, true),
Field::new("f64", DataType::Float64, true),
Field::new("i32", DataType::Int32, true),
Field::new("list", list, true),
]);
for (index, wrapped) in [(0, true), (1, true), (2, false), (3, false)] {
let column: Arc<dyn PhysicalExpr> =
Arc::new(Column::new(schema.field(index).name(), index));
let key = NormalizeNaNAndZero::wrap_if_needed(Arc::clone(&column), &schema)?;
assert_eq!(key.downcast_ref::<NormalizeNaNAndZero>().is_some(), wrapped);
assert_eq!(Arc::ptr_eq(&key, &column), !wrapped);
// A key that is already normalized is not wrapped a second time.
let again = NormalizeNaNAndZero::wrap_if_needed(Arc::clone(&key), &schema)?;
assert!(Arc::ptr_eq(&again, &key));
}
Ok(())
}
#[test]
fn test_normalize_nested_list_floats() {
let mut builder = ListBuilder::new(Float64Builder::new());
builder.values().append_value(-0.0);
builder.values().append_value(-f64::NAN);
builder.append(true);
let arr: ArrayRef = Arc::new(builder.finish());
let normalized = normalize_nested_floats(&arr);
let normalized = normalized.as_list::<i32>();
let inner = normalized.value(0);
let inner = inner.as_primitive::<Float64Type>();
assert_eq!(inner.value(0).to_bits(), 0.0f64.to_bits());
assert_eq!(inner.value(1).to_bits(), f64::NAN.to_bits());
}
/// An empty struct sibling of a float field must survive normalization. Iceberg exposes
/// `_partition` as `struct<>` on an unpartitioned table, and rebuilding it would panic
/// because `StructArray::new` cannot infer a length from zero columns.
#[test]
fn test_normalize_struct_with_empty_struct_sibling() {
let x = Float64Array::from(vec![Some(-0.0), Some(1.0)]);
let partition = StructArray::new_empty_fields(2, None);
let fields = vec![
Arc::new(Field::new("x", DataType::Float64, true)),
Arc::new(Field::new("p", partition.data_type().clone(), true)),
];
let arr: ArrayRef = Arc::new(StructArray::new(
fields.into(),
vec![Arc::new(x), Arc::new(partition)],
None,
));
let normalized = normalize_nested_floats(&arr);
let normalized = normalized.as_struct();
let col_x = normalized.column(0).as_primitive::<Float64Type>();
assert_eq!(col_x.value(0).to_bits(), 0.0f64.to_bits());
assert_eq!(col_x.value(1), 1.0);
let col_p = normalized.column(1).as_struct();
assert_eq!(col_p.num_columns(), 0);
assert_eq!(col_p.len(), 2);
}
/// A float-free subtree is returned as is rather than rebuilt. The child here is a nested
/// struct, which the recursive arms would otherwise rebuild into a fresh array.
#[test]
fn test_normalize_leaves_float_free_subtree_untouched() {
let inner_fields: Fields = vec![Arc::new(Field::new("i", DataType::Int32, true))].into();
let inner: ArrayRef = Arc::new(StructArray::new(
inner_fields.clone(),
vec![Arc::new(Int32Array::from(vec![Some(1), Some(2)]))],
None,
));
let floats = Float64Array::from(vec![Some(-0.0), Some(1.0)]);
let fields: Fields = vec![
Arc::new(Field::new("s", DataType::Struct(inner_fields), true)),
Arc::new(Field::new("f", DataType::Float64, true)),
]
.into();
let arr: ArrayRef = Arc::new(StructArray::new(
fields,
vec![Arc::clone(&inner), Arc::new(floats)],
None,
));
let normalized = normalize_nested_floats(&arr);
let normalized = normalized.as_struct();
assert!(Arc::ptr_eq(normalized.column(0), &inner));
assert_eq!(
normalized
.column(1)
.as_primitive::<Float64Type>()
.value(0)
.to_bits(),
0.0f64.to_bits()
);
}
#[test]
fn test_normalize_struct_floats() {
let a = Float64Array::from(vec![Some(-0.0), Some(1.0)]);
let b = Float64Array::from(vec![Some(-f64::NAN), Some(-0.0)]);
let fields = vec![
Arc::new(Field::new("a", DataType::Float64, true)),
Arc::new(Field::new("b", DataType::Float64, true)),
];
let arr: ArrayRef = Arc::new(StructArray::new(
fields.into(),
vec![Arc::new(a), Arc::new(b)],
None,
));
let normalized = normalize_nested_floats(&arr);
let normalized = normalized.as_struct();
let col_a = normalized.column(0).as_primitive::<Float64Type>();
let col_b = normalized.column(1).as_primitive::<Float64Type>();
assert_eq!(col_a.value(0).to_bits(), 0.0f64.to_bits());
assert_eq!(col_a.value(1), 1.0);
assert_eq!(col_b.value(0).to_bits(), f64::NAN.to_bits());
assert_eq!(col_b.value(1).to_bits(), 0.0f64.to_bits());
}
#[test]
fn test_normalize_fixed_size_list_floats() {
let values = Float64Array::from(vec![Some(-0.0), Some(-f64::NAN), Some(1.0), Some(-0.0)]);
let field = Arc::new(Field::new("item", DataType::Float64, true));
let arr: ArrayRef = Arc::new(FixedSizeListArray::new(
Arc::clone(&field),
2,
Arc::new(values),
None,
));
let normalized = normalize_nested_floats(&arr);
let normalized = normalized.as_fixed_size_list();
let flat = normalized.values().as_primitive::<Float64Type>();
assert_eq!(flat.value(0).to_bits(), 0.0f64.to_bits());
assert_eq!(flat.value(1).to_bits(), f64::NAN.to_bits());
assert_eq!(flat.value(2), 1.0);
assert_eq!(flat.value(3).to_bits(), 0.0f64.to_bits());
}
}