blob: 24c01e1e9df3903752c69705e60d6938b1b25cd6 [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::{compare_floats, has_float_leaf};
use arrow::array::{make_comparator, Array, AsArray, DynComparator, OffsetSizeTrait};
use arrow::compute::SortOptions;
use arrow::datatypes::{ArrowPrimitiveType, DataType, Float32Type, Float64Type};
use datafusion::common::{internal_err, DFSchema, Result};
use num::Float;
use std::cmp::Ordering;
use std::ops::Range;
/// Builds a comparator of `left[i]` against `right[j]` in Spark's SQL ordering, in which floats
/// compare as [`compare_floats`] does at any depth. Inner nulls sort first, lists compare element
/// by element and then by length, and structs compare field by field.
///
/// Subtrees without a float leaf use Arrow's comparator, which orders them the same way. The two
/// arrays must have the same type, ignoring field names and nullability.
pub fn spark_comparator(left: &dyn Array, right: &dyn Array) -> Result<DynComparator> {
check_types(left, right)?;
comparator(left, right, false)
}
/// Builds a test of whether `left[i]` equals `right[j]` in the ordering of [`spark_comparator`].
/// Lists of different lengths are unequal without comparing their elements.
pub fn spark_equality(
left: &dyn Array,
right: &dyn Array,
) -> Result<Box<dyn Fn(usize, usize) -> bool + Send + Sync>> {
check_types(left, right)?;
let compare = comparator(left, right, true)?;
Ok(Box::new(move |i, j| compare(i, j).is_eq()))
}
fn check_types(left: &dyn Array, right: &dyn Array) -> Result<()> {
if DFSchema::datatype_is_logically_equal(left.data_type(), right.data_type()) {
Ok(())
} else {
internal_err!(
"Spark comparison requires matching types, got {} and {}",
left.data_type(),
right.data_type()
)
}
}
/// With `equality` set, the comparator only has to tell equal from unequal values.
fn comparator(left: &dyn Array, right: &dyn Array, equality: bool) -> Result<DynComparator> {
if !has_float_leaf(left.data_type()) {
let options = SortOptions {
descending: false,
nulls_first: true,
};
return Ok(make_comparator(left, right, options)?);
}
match (left.data_type(), right.data_type()) {
(DataType::Float32, DataType::Float32) => Ok(float_comparator::<Float32Type>(left, right)),
(DataType::Float64, DataType::Float64) => Ok(float_comparator::<Float64Type>(left, right)),
(DataType::List(_), DataType::List(_)) => list_comparator::<i32>(left, right, equality),
(DataType::LargeList(_), DataType::LargeList(_)) => {
list_comparator::<i64>(left, right, equality)
}
(DataType::FixedSizeList(_, _), DataType::FixedSizeList(_, _)) => {
fixed_size_list_comparator(left, right, equality)
}
(DataType::Struct(_), DataType::Struct(_)) => struct_comparator(left, right, equality),
(l, r) => internal_err!("Unsupported types for Spark comparison: {l} and {r}"),
}
}
fn float_comparator<T: ArrowPrimitiveType>(left: &dyn Array, right: &dyn Array) -> DynComparator
where
T::Native: Float,
{
let l = left.as_primitive::<T>().values().clone();
let r = right.as_primitive::<T>().values().clone();
nulls_first(left, right, move |i, j| compare_floats(l[i], r[j]))
}
fn list_comparator<O: OffsetSizeTrait>(
left: &dyn Array,
right: &dyn Array,
equality: bool,
) -> Result<DynComparator> {
let (l, r) = (left.as_list::<O>(), right.as_list::<O>());
let compare = comparator(l.values().as_ref(), r.values().as_ref(), equality)?;
let (l, r) = (l.offsets().clone(), r.offsets().clone());
Ok(nulls_first(left, right, move |i, j| {
let left = l[i].as_usize()..l[i + 1].as_usize();
let right = r[j].as_usize()..r[j + 1].as_usize();
lexicographic(&compare, left, right, equality)
}))
}
fn fixed_size_list_comparator(
left: &dyn Array,
right: &dyn Array,
equality: bool,
) -> Result<DynComparator> {
let (l, r) = (left.as_fixed_size_list(), right.as_fixed_size_list());
let compare = comparator(l.values().as_ref(), r.values().as_ref(), equality)?;
let (l, r) = (l.value_length() as usize, r.value_length() as usize);
Ok(nulls_first(left, right, move |i, j| {
lexicographic(&compare, i * l..(i + 1) * l, j * r..(j + 1) * r, equality)
}))
}
/// Compares two runs of child values element by element, then by length. When only equality
/// matters, runs of different lengths are unequal without comparing any elements.
fn lexicographic(
compare: &DynComparator,
left: Range<usize>,
right: Range<usize>,
equality: bool,
) -> Ordering {
let lengths = left.len().cmp(&right.len());
if equality && lengths.is_ne() {
return lengths;
}
left.zip(right)
.map(|(i, j)| compare(i, j))
.find(|ordering| ordering.is_ne())
.unwrap_or(lengths)
}
fn struct_comparator(left: &dyn Array, right: &dyn Array, equality: bool) -> Result<DynComparator> {
let fields = left
.as_struct()
.columns()
.iter()
.zip(right.as_struct().columns())
.map(|(l, r)| comparator(l.as_ref(), r.as_ref(), equality))
.collect::<Result<Vec<_>>>()?;
Ok(nulls_first(left, right, move |i, j| {
fields
.iter()
.map(|compare| compare(i, j))
.find(|ordering| ordering.is_ne())
.unwrap_or(Ordering::Equal)
}))
}
/// Orders nulls before values. A null slot never reaches `compare`, because the child values
/// under a null list or struct can be anything.
fn nulls_first(
left: &dyn Array,
right: &dyn Array,
compare: impl Fn(usize, usize) -> Ordering + Send + Sync + 'static,
) -> DynComparator {
let left = left.nulls().filter(|nulls| nulls.null_count() > 0).cloned();
let right = right
.nulls()
.filter(|nulls| nulls.null_count() > 0)
.cloned();
if left.is_none() && right.is_none() {
return Box::new(compare);
}
Box::new(move |i, j| {
let l = left.as_ref().is_some_and(|nulls| nulls.is_null(i));
let r = right.as_ref().is_some_and(|nulls| nulls.is_null(j));
match (l, r) {
(false, false) => compare(i, j),
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::float_semantics::{NEGATIVE_NAN, PAYLOAD_NAN};
use arrow::array::{
ArrayRef, DictionaryArray, FixedSizeListArray, Float64Array, Int32Array, LargeListArray,
ListArray, StructArray,
};
use arrow::buffer::{NullBuffer, OffsetBuffer};
use arrow::datatypes::Field;
use std::sync::Arc;
/// Wraps every value in a one-element list, large list and fixed-size list, and in a struct,
/// so that each nesting compares the same pairs of leaves.
fn nestings(values: ArrayRef) -> Vec<ArrayRef> {
let field = Arc::new(Field::new("item", values.data_type().clone(), true));
let lengths = || std::iter::repeat_n(1, values.len());
vec![
Arc::clone(&values),
Arc::new(ListArray::new(
Arc::clone(&field),
OffsetBuffer::from_lengths(lengths()),
Arc::clone(&values),
None,
)),
Arc::new(LargeListArray::new(
Arc::clone(&field),
OffsetBuffer::from_lengths(lengths()),
Arc::clone(&values),
None,
)),
Arc::new(FixedSizeListArray::new(
Arc::clone(&field),
1,
Arc::clone(&values),
None,
)),
Arc::new(StructArray::new(
vec![field].into(),
vec![Arc::clone(&values)],
None,
)),
]
}
#[test]
fn floats_compare_as_spark_orders_them_at_every_depth() -> Result<()> {
let left = vec![
Some(-0.0),
Some(NEGATIVE_NAN),
None,
Some(1.0),
Some(f64::INFINITY),
];
let right = vec![
Some(0.0),
Some(f64::INFINITY),
None,
Some(PAYLOAD_NAN),
Some(f64::NEG_INFINITY),
Some(-1.0),
];
let expected = |l: Option<f64>, r: Option<f64>| match (l, r) {
(Some(l), Some(r)) => compare_floats(l, r),
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
};
let nested_left = nestings(Arc::new(Float64Array::from(left.clone())));
let nested_right = nestings(Arc::new(Float64Array::from(right.clone())));
for (l_array, r_array) in nested_left.iter().zip(&nested_right) {
let compare = spark_comparator(l_array.as_ref(), r_array.as_ref())?;
let equal = spark_equality(l_array.as_ref(), r_array.as_ref())?;
for (i, &l) in left.iter().enumerate() {
for (j, &r) in right.iter().enumerate() {
let expected = expected(l, r);
let context = format!("{l:?} vs {r:?} in {}", l_array.data_type());
assert_eq!(compare(i, j), expected, "{context}");
assert_eq!(equal(i, j), expected.is_eq(), "{context}");
}
}
}
Ok(())
}
#[test]
fn lists_compare_elements_then_length_and_skip_null_slots() -> Result<()> {
let field = Arc::new(Field::new("item", DataType::Float64, true));
// The last row of each side is a null list over values that differ between the sides.
let left: ArrayRef = Arc::new(ListArray::new(
Arc::clone(&field),
OffsetBuffer::from_lengths([2, 1, 0, 1]),
Arc::new(Float64Array::from(vec![
Some(0.0),
Some(1.0),
Some(f64::NAN),
Some(1.0),
])),
Some(NullBuffer::from(vec![true, true, true, false])),
));
let right: ArrayRef = Arc::new(ListArray::new(
field,
OffsetBuffer::from_lengths([1, 2, 1, 1]),
Arc::new(Float64Array::from(vec![
Some(-0.0),
Some(f64::INFINITY),
Some(5.0),
None,
Some(9.0),
])),
Some(NullBuffer::from(vec![true, true, true, false])),
));
let compare = spark_comparator(left.as_ref(), right.as_ref())?;
// `[0.0, 1.0]` extends `[-0.0]`, so it is greater.
assert_eq!(compare(0, 0), Ordering::Greater);
// `[NaN]` against `[Infinity, 5.0]`: the first element decides before the length.
assert_eq!(compare(1, 1), Ordering::Greater);
// `[]` is a prefix of `[null]`.
assert_eq!(compare(2, 2), Ordering::Less);
assert_eq!(compare(3, 3), Ordering::Equal);
assert_eq!(compare(3, 2), Ordering::Less);
assert_eq!(compare(2, 3), Ordering::Greater);
let equal = spark_equality(left.as_ref(), right.as_ref())?;
let pairs = [(0, 0), (1, 1), (2, 2), (3, 3), (3, 2), (2, 3), (0, 1)];
for (i, j) in pairs {
assert_eq!(equal(i, j), compare(i, j).is_eq(), "({i}, {j})");
}
Ok(())
}
#[test]
fn struct_fields_without_floats_break_ties() -> Result<()> {
let structs = |floats: Vec<f64>, ints: Vec<i32>| -> ArrayRef {
Arc::new(StructArray::from(vec![
(
Arc::new(Field::new("f", DataType::Float64, true)),
Arc::new(Float64Array::from(floats)) as ArrayRef,
),
(
Arc::new(Field::new("i", DataType::Int32, true)),
Arc::new(Int32Array::from(ints)) as ArrayRef,
),
]))
};
let left = structs(vec![-0.0, f64::NAN, 1.0], vec![2, 1, 1]);
let right = structs(vec![0.0, NEGATIVE_NAN, 2.0], vec![1, 1, 0]);
let compare = spark_comparator(left.as_ref(), right.as_ref())?;
assert_eq!(compare(0, 0), Ordering::Greater);
assert_eq!(compare(1, 1), Ordering::Equal);
assert_eq!(compare(2, 2), Ordering::Less);
Ok(())
}
#[test]
fn mismatched_types_are_rejected() {
let floats: ArrayRef = Arc::new(Float64Array::from(vec![1.0]));
let ints: ArrayRef = Arc::new(Int32Array::from(vec![1]));
// A dictionary passes the logical type check, so it must not reach a downcast either.
let dictionary: ArrayRef = Arc::new(DictionaryArray::new(
Int32Array::from(vec![0]),
Arc::clone(&floats),
));
let list = |values: &ArrayRef| -> ArrayRef {
let field = Arc::new(Field::new("item", values.data_type().clone(), true));
Arc::new(ListArray::new(
field,
OffsetBuffer::from_lengths([1]),
Arc::clone(values),
None,
))
};
let (float_list, dictionary_list) = (list(&floats), list(&dictionary));
for (left, right) in [
(&floats, &ints),
(&floats, &dictionary),
(&dictionary, &floats),
(&float_list, &dictionary_list),
(&dictionary_list, &float_list),
] {
let types = format!("{} and {}", left.data_type(), right.data_type());
assert!(
spark_comparator(left.as_ref(), right.as_ref()).is_err(),
"{types}"
);
assert!(
spark_equality(left.as_ref(), right.as_ref()).is_err(),
"{types}"
);
}
}
}