Skip to content

Commit cc60f57

Browse files
Fix NULL semantics in ScalarValue::partial_cmp (fixes #19579)
1 parent 62cdc8d commit cc60f57

File tree

2 files changed

+13
-31
lines changed

2 files changed

+13
-31
lines changed

datafusion/common/src/error.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,6 @@ mod test {
12431243
// To pass the test the environment variable RUST_BACKTRACE should be set to 1 to enforce backtrace
12441244
#[cfg(feature = "backtrace")]
12451245
#[test]
1246-
#[expect(clippy::unnecessary_literal_unwrap)]
12471246
fn test_enabled_backtrace() {
12481247
match std::env::var("RUST_BACKTRACE") {
12491248
Ok(val) if val == "1" => {}

datafusion/common/src/scalar/mod.rs

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -569,12 +569,14 @@ impl PartialOrd for ScalarValue {
569569
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
570570
use ScalarValue::*;
571571

572+
// NULL values are incomparable (SQL tri-state logic)
572573
if self.is_null() || other.is_null() {
573574
return None;
574575
}
575576
// This purposely doesn't have a catch-all "(_, _)" so that
576577
// any newly added enum variant will require editing this list
577578
// or else face a compile error
579+
#[expect(unreachable_patterns)]
578580
match (self, other) {
579581
(Decimal32(v1, p1, s1), Decimal32(v2, p2, s2)) => {
580582
if p1.eq(p2) && s1.eq(s2) {
@@ -727,10 +729,9 @@ impl PartialOrd for ScalarValue {
727729
if k1 == k2 { v1.partial_cmp(v2) } else { None }
728730
}
729731
(Dictionary(_, _), _) => None,
730-
// Nulls are handled by the early return above
731-
(Null, _) | (_, Null) => unreachable!(
732-
"Nulls are already handled before entering ScalarValue::partial_cmp match"
733-
),
732+
// These patterns are unreachable due to the early return above,
733+
// but needed for exhaustiveness checking
734+
(Null, _) | (_, Null) => unreachable!(),
734735
}
735736
}
736737
}
@@ -4023,15 +4024,15 @@ impl ScalarValue {
40234024
arr1 == &right
40244025
}
40254026

4026-
/// Compare two `ScalarValue`s.
4027+
/// Compare two `ScalarValue`s.
40274028
///
40284029
/// Returns an error if:
40294030
/// * the values are of incompatible types, or
40304031
/// * either value is NULL.
40314032
///
40324033
/// This differs from `partial_cmp`, which returns `None` for NULL inputs
40334034
/// instead of an error.
4034-
pub fn try_cmp(&self, other: &Self) -> Result<Ordering> {
4035+
pub fn try_cmp(&self, other: &Self) -> Result<Ordering> {
40354036
self.partial_cmp(other).ok_or_else(|| {
40364037
_internal_datafusion_err!("Uncomparable values: {self:?}, {other:?}")
40374038
})
@@ -9361,34 +9362,16 @@ mod tests {
93619362
fn scalar_partial_ordering_nulls() {
93629363
use ScalarValue::*;
93639364

9364-
assert_eq!(
9365-
Int32(Some(3)).partial_cmp(&Int32(None)),
9366-
None
9367-
);
9365+
assert_eq!(Int32(Some(3)).partial_cmp(&Int32(None)), None);
93689366

9369-
assert_eq!(
9370-
Int32(None).partial_cmp(&Int32(Some(3))),
9371-
None
9372-
);
9367+
assert_eq!(Int32(None).partial_cmp(&Int32(Some(3))), None);
93739368

9374-
assert_eq!(
9375-
Int32(None).partial_cmp(&Int32(None)),
9376-
None
9377-
);
9369+
assert_eq!(Int32(None).partial_cmp(&Int32(None)), None);
93789370

9379-
assert_eq!(
9380-
Null.partial_cmp(&Int32(Some(3))),
9381-
None
9382-
);
9371+
assert_eq!(Null.partial_cmp(&Int32(Some(3))), None);
93839372

9384-
assert_eq!(
9385-
Int32(Some(3)).partial_cmp(&Null),
9386-
None
9387-
);
9373+
assert_eq!(Int32(Some(3)).partial_cmp(&Null), None);
93889374

9389-
assert_eq!(
9390-
Null.partial_cmp(&Null),
9391-
None
9392-
);
9375+
assert_eq!(Null.partial_cmp(&Null), None);
93939376
}
93949377
}

0 commit comments

Comments
 (0)