Skip to content

Commit 4387243

Browse files
committed
feat: Support IEEE 754 for SQL ops
1 parent 883c38e commit 4387243

14 files changed

Lines changed: 574 additions & 37 deletions

File tree

datafusion/common/src/hash_utils.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,16 @@ macro_rules! hash_float_value {
188188
($(($t:ty, $i:ty)),+) => {
189189
$(impl HashValue for $t {
190190
fn hash_one(&self, state: &RandomState) -> u64 {
191-
state.hash_one(<$i>::from_ne_bytes(self.to_ne_bytes()))
191+
// +0.0 and -0.0 differ only in the sign bit but compare equal
192+
// under IEEE 754; normalize -0.0 → +0.0 so Hash agrees with Eq.
193+
let bits = <$i>::from_ne_bytes(self.to_ne_bytes());
194+
let bits = if bits << 1 == 0 { 0 } else { bits };
195+
state.hash_one(bits)
192196
}
193197
fn hash_write(&self, hasher: &mut impl Hasher) {
194-
hasher.write(&self.to_ne_bytes())
198+
let bits = <$i>::from_ne_bytes(self.to_ne_bytes());
199+
let bits: $i = if bits << 1 == 0 { 0 } else { bits };
200+
hasher.write(&bits.to_ne_bytes())
195201
}
196202
})+
197203
};

datafusion/common/src/scalar/mod.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,11 @@ pub enum ScalarValue {
469469

470470
impl Hash for Fl<f16> {
471471
fn hash<H: Hasher>(&self, state: &mut H) {
472-
self.0.to_bits().hash(state);
472+
let bits = self.0.to_bits();
473+
// +0.0 and -0.0 differ only in the sign bit but compare equal under
474+
// IEEE 754; normalize -0.0 → +0.0 so Hash agrees with Eq.
475+
let bits = if bits << 1 == 0 { 0 } else { bits };
476+
bits.hash(state);
473477
}
474478
}
475479

@@ -500,17 +504,24 @@ impl PartialEq for ScalarValue {
500504
(Boolean(v1), Boolean(v2)) => v1.eq(v2),
501505
(Boolean(_), _) => false,
502506
(Float32(v1), Float32(v2)) => match (v1, v2) {
503-
(Some(f1), Some(f2)) => f1.to_bits() == f2.to_bits(),
507+
(Some(f1), Some(f2)) => {
508+
*f1 == 0.0 && *f2 == 0.0 || f1.to_bits() == f2.to_bits()
509+
}
504510
_ => v1.eq(v2),
505511
},
506512
(Float16(v1), Float16(v2)) => match (v1, v2) {
507-
(Some(f1), Some(f2)) => f1.to_bits() == f2.to_bits(),
513+
(Some(f1), Some(f2)) => {
514+
let (b1, b2) = (f1.to_bits(), f2.to_bits());
515+
((b1 << 1) == 0 && (b2 << 1) == 0) || b1 == b2
516+
}
508517
_ => v1.eq(v2),
509518
},
510519
(Float32(_), _) => false,
511520
(Float16(_), _) => false,
512521
(Float64(v1), Float64(v2)) => match (v1, v2) {
513-
(Some(f1), Some(f2)) => f1.to_bits() == f2.to_bits(),
522+
(Some(f1), Some(f2)) => {
523+
*f1 == 0.0 && *f2 == 0.0 || f1.to_bits() == f2.to_bits()
524+
}
514525
_ => v1.eq(v2),
515526
},
516527
(Float64(_), _) => false,
@@ -656,17 +667,31 @@ impl PartialOrd for ScalarValue {
656667
(Boolean(v1), Boolean(v2)) => v1.partial_cmp(v2),
657668
(Boolean(_), _) => None,
658669
(Float32(v1), Float32(v2)) => match (v1, v2) {
659-
(Some(f1), Some(f2)) => Some(f1.total_cmp(f2)),
670+
(Some(a), Some(b)) => Some(if *a == 0.0 && *b == 0.0 {
671+
Ordering::Equal
672+
} else {
673+
a.total_cmp(b)
674+
}),
660675
_ => v1.partial_cmp(v2),
661676
},
662677
(Float16(v1), Float16(v2)) => match (v1, v2) {
663-
(Some(f1), Some(f2)) => Some(f1.total_cmp(f2)),
678+
(Some(a), Some(b)) => {
679+
Some(if a.to_bits() << 1 == 0 && b.to_bits() << 1 == 0 {
680+
Ordering::Equal
681+
} else {
682+
a.total_cmp(b)
683+
})
684+
}
664685
_ => v1.partial_cmp(v2),
665686
},
666687
(Float32(_), _) => None,
667688
(Float16(_), _) => None,
668689
(Float64(v1), Float64(v2)) => match (v1, v2) {
669-
(Some(f1), Some(f2)) => Some(f1.total_cmp(f2)),
690+
(Some(a), Some(b)) => Some(if *a == 0.0 && *b == 0.0 {
691+
Ordering::Equal
692+
} else {
693+
a.total_cmp(b)
694+
}),
670695
_ => v1.partial_cmp(v2),
671696
},
672697
(Float64(_), _) => None,
@@ -941,7 +966,11 @@ macro_rules! hash_float_value {
941966
$(impl std::hash::Hash for Fl<$t> {
942967
#[inline]
943968
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
944-
state.write(&<$i>::from_ne_bytes(self.0.to_ne_bytes()).to_ne_bytes())
969+
let bits = <$i>::from_ne_bytes(self.0.to_ne_bytes());
970+
// +0.0 and -0.0 differ only in the sign bit but compare equal
971+
// under IEEE 754; normalize -0.0 → +0.0 so Hash agrees with Eq.
972+
let bits: $i = if bits << 1 == 0 { 0 } else { bits };
973+
state.write(&bits.to_ne_bytes())
945974
}
946975
})+
947976
};

datafusion/common/src/utils/mod.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,64 @@ fn fsl_values_row_number(list_size: i32, array_len: usize) -> Result<Int32Array>
13861386
Ok(PrimitiveArray::new(rows_number.into(), None))
13871387
}
13881388

1389+
/// Replace `-0.0` with `+0.0` in any `Float16`, `Float32`, or `Float64` array.
1390+
/// For non-float arrays returns the input unchanged. NaN payloads are
1391+
/// preserved.
1392+
///
1393+
/// Arrow's comparison kernels (`arrow::compute::kernels::cmp::eq` etc.) and
1394+
/// row-encoding (`arrow::row::RowConverter`) use IEEE 754 totalOrder
1395+
/// semantics, which treats `-0.0` and `+0.0` as distinct. SQL semantics
1396+
/// (PostgreSQL / IEEE 754 equality) require them to compare equal, so
1397+
/// callers normalize before invoking those kernels.
1398+
pub fn normalize_float_zero(array: &ArrayRef) -> ArrayRef {
1399+
use arrow::array::{Float16Array, Float32Array, Float64Array};
1400+
use arrow::datatypes::{Float16Type, Float32Type, Float64Type};
1401+
match array.data_type() {
1402+
DataType::Float32 => {
1403+
let arr: &Float32Array = array.as_primitive::<Float32Type>();
1404+
let normalized: Float32Array =
1405+
arr.unary(|v| if v.to_bits() << 1 == 0 { 0.0_f32 } else { v });
1406+
Arc::new(normalized)
1407+
}
1408+
DataType::Float64 => {
1409+
let arr: &Float64Array = array.as_primitive::<Float64Type>();
1410+
let normalized: Float64Array =
1411+
arr.unary(|v| if v.to_bits() << 1 == 0 { 0.0_f64 } else { v });
1412+
Arc::new(normalized)
1413+
}
1414+
DataType::Float16 => {
1415+
let arr: &Float16Array = array.as_primitive::<Float16Type>();
1416+
let normalized: Float16Array = arr.unary(|v| {
1417+
if v.to_bits() << 1 == 0 {
1418+
half::f16::from_bits(0)
1419+
} else {
1420+
v
1421+
}
1422+
});
1423+
Arc::new(normalized)
1424+
}
1425+
_ => Arc::clone(array),
1426+
}
1427+
}
1428+
1429+
/// Replace `-0.0` with `+0.0` in `Float16`, `Float32`, or `Float64` scalar
1430+
/// values. Other variants are returned unchanged. See [`normalize_float_zero`]
1431+
/// for context.
1432+
pub fn normalize_float_zero_scalar(scalar: ScalarValue) -> ScalarValue {
1433+
match scalar {
1434+
ScalarValue::Float32(Some(v)) if v.to_bits() << 1 == 0 => {
1435+
ScalarValue::Float32(Some(0.0))
1436+
}
1437+
ScalarValue::Float64(Some(v)) if v.to_bits() << 1 == 0 => {
1438+
ScalarValue::Float64(Some(0.0))
1439+
}
1440+
ScalarValue::Float16(Some(v)) if v.to_bits() << 1 == 0 => {
1441+
ScalarValue::Float16(Some(half::f16::from_bits(0)))
1442+
}
1443+
other => other,
1444+
}
1445+
}
1446+
13891447
#[cfg(test)]
13901448
mod tests {
13911449
use std::sync::Arc;

datafusion/functions-nested/src/except.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use arrow::buffer::{NullBuffer, OffsetBuffer};
2727
use arrow::compute::take;
2828
use arrow::datatypes::{DataType, FieldRef};
2929
use arrow::row::{RowConverter, SortField};
30-
use datafusion_common::utils::{ListCoercion, take_function_args};
30+
use datafusion_common::utils::{ListCoercion, normalize_float_zero, take_function_args};
3131
use datafusion_common::{HashSet, Result, internal_err};
3232
use datafusion_expr::{
3333
ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
@@ -169,16 +169,21 @@ fn general_except<OffsetSize: OffsetSizeTrait>(
169169
) -> Result<GenericListArray<OffsetSize>> {
170170
let converter = RowConverter::new(vec![SortField::new(l.value_type())])?;
171171

172+
// Normalize -0.0 → +0.0 so RowConverter (IEEE 754 totalOrder) groups
173+
// ±0 together for both the rhs lookup set and the lhs probe.
174+
let l_values_norm = normalize_float_zero(l.values());
175+
let r_values_norm = normalize_float_zero(r.values());
176+
172177
// Only convert the visible portion of the values array. For sliced
173178
// ListArrays, values() returns the full underlying array but only
174179
// elements between the first and last offset are referenced.
175180
let l_first = l.offsets()[0].as_usize();
176181
let l_len = l.offsets()[l.len()].as_usize() - l_first;
177-
let l_values = converter.convert_columns(&[l.values().slice(l_first, l_len)])?;
182+
let l_values = converter.convert_columns(&[l_values_norm.slice(l_first, l_len)])?;
178183

179184
let r_first = r.offsets()[0].as_usize();
180185
let r_len = r.offsets()[r.len()].as_usize() - r_first;
181-
let r_values = converter.convert_columns(&[r.values().slice(r_first, r_len)])?;
186+
let r_values = converter.convert_columns(&[r_values_norm.slice(r_first, r_len)])?;
182187

183188
let mut offsets = Vec::<OffsetSize>::with_capacity(l.len() + 1);
184189
offsets.push(OffsetSize::usize_as(0));
@@ -223,11 +228,11 @@ fn general_except<OffsetSize: OffsetSizeTrait>(
223228
} else if OffsetSize::IS_LARGE {
224229
let indices =
225230
UInt64Array::from(indices.into_iter().map(|i| i as u64).collect::<Vec<_>>());
226-
take(l.values().as_ref(), &indices, None)?
231+
take(l_values_norm.as_ref(), &indices, None)?
227232
} else {
228233
let indices =
229234
UInt32Array::from(indices.into_iter().map(|i| i as u32).collect::<Vec<_>>());
230-
take(l.values().as_ref(), &indices, None)?
235+
take(l_values_norm.as_ref(), &indices, None)?
231236
};
232237

233238
Ok(GenericListArray::<OffsetSize>::new(

datafusion/functions-nested/src/set_ops.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use arrow::datatypes::DataType::{LargeList, List, Null};
2828
use arrow::datatypes::{DataType, Field, FieldRef};
2929
use arrow::row::{RowConverter, SortField};
3030
use datafusion_common::cast::{as_large_list_array, as_list_array};
31-
use datafusion_common::utils::ListCoercion;
31+
use datafusion_common::utils::{ListCoercion, normalize_float_zero};
3232
use datafusion_common::{
3333
Result, assert_eq_or_internal_err, exec_err, internal_err, utils::take_function_args,
3434
};
@@ -351,21 +351,27 @@ fn generic_set_lists<OffsetSize: OffsetSizeTrait>(
351351

352352
let converter = RowConverter::new(vec![SortField::new(l.value_type())])?;
353353

354+
// Normalize -0.0 → +0.0 so RowConverter (which uses IEEE 754 totalOrder
355+
// and treats ±0 as distinct) groups them together. Use the normalized
356+
// arrays for both row conversion and the final output values.
357+
let l_values_norm = normalize_float_zero(l.values());
358+
let r_values_norm = normalize_float_zero(r.values());
359+
354360
// Only convert the visible portion of the values array. For sliced
355361
// ListArrays, values() returns the full underlying array but only
356362
// elements between the first and last offset are referenced.
357363
let l_first = l.offsets()[0].as_usize();
358364
let l_len = l.offsets()[l.len()].as_usize() - l_first;
359-
let rows_l = converter.convert_columns(&[l.values().slice(l_first, l_len)])?;
365+
let rows_l = converter.convert_columns(&[l_values_norm.slice(l_first, l_len)])?;
360366

361367
let r_first = r.offsets()[0].as_usize();
362368
let r_len = r.offsets()[r.len()].as_usize() - r_first;
363-
let rows_r = converter.convert_columns(&[r.values().slice(r_first, r_len)])?;
369+
let rows_r = converter.convert_columns(&[r_values_norm.slice(r_first, r_len)])?;
364370

365371
// Combine the *sliced* value arrays so 0-based indices from the row
366372
// converter map directly into the concatenated array.
367-
let l_values = l.values().slice(l_first, l_len);
368-
let r_values = r.values().slice(r_first, r_len);
373+
let l_values = l_values_norm.slice(l_first, l_len);
374+
let r_values = r_values_norm.slice(r_first, r_len);
369375
let combined_values = concat(&[l_values.as_ref(), r_values.as_ref()])?;
370376
let r_offset = l_len;
371377

@@ -558,13 +564,18 @@ fn general_array_distinct<OffsetSize: OffsetSizeTrait>(
558564

559565
let converter = RowConverter::new(vec![SortField::new(dt.clone())])?;
560566

567+
// Normalize -0.0 → +0.0 so RowConverter (which uses IEEE 754 totalOrder
568+
// and treats ±0 as distinct) groups them together, and so the output
569+
// carries the canonical sign.
570+
let values_norm = normalize_float_zero(array.values());
571+
561572
// Only convert the visible portion of the values array. For sliced
562573
// ListArrays, values() returns the full underlying array but only
563574
// elements between the first and last offset are referenced.
564575
let first_offset = value_offsets[0].as_usize();
565576
let visible_len = value_offsets[array.len()].as_usize() - first_offset;
566577
let rows =
567-
converter.convert_columns(&[array.values().slice(first_offset, visible_len)])?;
578+
converter.convert_columns(&[values_norm.slice(first_offset, visible_len)])?;
568579

569580
let mut indices: Vec<usize> = Vec::with_capacity(rows.num_rows());
570581
let mut seen = HashSet::new();
@@ -593,19 +604,19 @@ fn general_array_distinct<OffsetSize: OffsetSizeTrait>(
593604
}
594605

595606
// Gather distinct values in a single pass, using the computed `indices`.
596-
// Indices are absolute positions in array.values() (first_offset was added
597-
// back when collecting them), so we can take directly from the full values.
607+
// Indices are absolute positions in the (normalized) values array, so we
608+
// can take directly from the full values.
598609
// Use UInt64Array for LargeList to support values arrays exceeding u32::MAX.
599610
let final_values = if indices.is_empty() {
600611
new_empty_array(&dt)
601612
} else if OffsetSize::IS_LARGE {
602613
let indices =
603614
UInt64Array::from(indices.into_iter().map(|i| i as u64).collect::<Vec<_>>());
604-
take(array.values().as_ref(), &indices, None)?
615+
take(values_norm.as_ref(), &indices, None)?
605616
} else {
606617
let indices =
607618
UInt32Array::from(indices.into_iter().map(|i| i as u32).collect::<Vec<_>>());
608-
take(array.values().as_ref(), &indices, None)?
619+
take(values_norm.as_ref(), &indices, None)?
609620
};
610621

611622
Ok(Arc::new(GenericListArray::<OffsetSize>::try_new(

datafusion/physical-expr-common/src/datum.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use arrow::compute::kernels::cmp::{
2323
};
2424
use arrow::compute::{SortOptions, ilike, like, nilike, nlike};
2525
use arrow::error::ArrowError;
26+
use datafusion_common::utils::{normalize_float_zero, normalize_float_zero_scalar};
2627
use datafusion_common::{Result, ScalarValue};
2728
use datafusion_common::{arrow_datafusion_err, assert_or_internal_err, internal_err};
2829
use datafusion_expr_common::columnar_value::ColumnarValue;
@@ -84,7 +85,22 @@ pub fn apply_cmp(
8485
}
8586
};
8687

87-
apply(lhs, rhs, |l, r| Ok(Arc::new(f(l, r)?)))
88+
// Arrow's comparison kernels use IEEE 754 totalOrder semantics for
89+
// floats, which treats `-0.0` and `+0.0` as distinct. Normalize float
90+
// operands so SQL semantics (`+0.0 == -0.0`) hold. No-op for
91+
// non-float types.
92+
let lhs = normalize_cmp_input(lhs);
93+
let rhs = normalize_cmp_input(rhs);
94+
apply(&lhs, &rhs, |l, r| Ok(Arc::new(f(l, r)?)))
95+
}
96+
}
97+
98+
fn normalize_cmp_input(cv: &ColumnarValue) -> ColumnarValue {
99+
match cv {
100+
ColumnarValue::Array(a) => ColumnarValue::Array(normalize_float_zero(a)),
101+
ColumnarValue::Scalar(s) => {
102+
ColumnarValue::Scalar(normalize_float_zero_scalar(s.clone()))
103+
}
88104
}
89105
}
90106

0 commit comments

Comments
 (0)