blob: 7688435e433bdbc083569a9065915e2e68a7e088 [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 std::sync::Arc;
use super::with_values;
use crate::float_semantics::{compare_floats, spark_equality};
use arrow::array::{
Array, ArrayRef, AsArray, BooleanArray, BooleanBufferBuilder, ListArray, PrimitiveArray,
};
use arrow::buffer::{BooleanBuffer, NullBuffer, OffsetBuffer};
use arrow::compute::filter;
use arrow::datatypes::{ArrowPrimitiveType, DataType, Float32Type, Float64Type};
use datafusion::common::{exec_err, utils::take_function_args, Result, ScalarValue};
use datafusion::logical_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use num::Float;
/// Spark's `array_remove` for arrays whose elements hold floats at any depth.
///
/// Spark removes every element equal to the value under `genEqual`, in which `-0.0` equals `0.0`
/// and all NaNs are equal, at any depth of an array or struct element. It keeps null elements,
/// and returns null when the array or the value is null. DataFusion's `array_remove_all`, which
/// Comet uses for other element types, compares the bits instead, so it keeps a `-0.0` when
/// removing `0.0`, and a NaN whose bits differ.
#[derive(Debug, Hash, Eq, PartialEq)]
pub struct SparkArrayRemove {
signature: Signature,
}
impl Default for SparkArrayRemove {
fn default() -> Self {
Self {
signature: Signature::any(2, Volatility::Immutable),
}
}
}
impl ScalarUDFImpl for SparkArrayRemove {
fn name(&self) -> &str {
"spark_array_remove"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
Ok(arg_types[0].clone())
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let [array, value] = take_function_args(self.name(), &args.args)?;
let all_scalars = matches!(
(array, value),
(ColumnarValue::Scalar(_), ColumnarValue::Scalar(_))
);
let rows = if all_scalars { 1 } else { args.number_rows };
let array = array.to_array(rows)?;
// Spark arrays use Arrow's 32-bit List layout.
let Some(list) = array.as_list_opt::<i32>() else {
return exec_err!("spark_array_remove takes a list, got {}", array.data_type());
};
let result = match value {
ColumnarValue::Scalar(needle) => remove_constant(list, needle)?,
ColumnarValue::Array(value) => array_remove(list, value)?,
};
if all_scalars {
Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(
&result, 0,
)?))
} else {
Ok(ColumnarValue::Array(result))
}
}
}
fn array_remove(array: &ListArray, value: &ArrayRef) -> Result<ArrayRef> {
// Spark casts the value to the element type. Anything else fails in `spark_equality`.
match (array.value_type(), value.data_type()) {
(DataType::Float32, DataType::Float32) => Ok(remove_floats::<Float32Type>(array, value)),
(DataType::Float64, DataType::Float64) => Ok(remove_floats::<Float64Type>(array, value)),
_ => {
let equal = spark_equality(array.values().as_ref(), value.as_ref())?;
remove_where(array, value, equal)
}
}
}
/// [`array_remove`] of the same value from every row, which a float array does in one pass over
/// all the values, as DataFusion does for its own `array_remove`.
fn remove_constant(array: &ListArray, needle: &ScalarValue) -> Result<ArrayRef> {
match (array.value_type(), needle) {
(DataType::Float32, ScalarValue::Float32(Some(needle))) => {
remove_float_constant::<Float32Type>(array, *needle)
}
(DataType::Float64, ScalarValue::Float64(Some(needle))) => {
remove_float_constant::<Float64Type>(array, *needle)
}
_ => array_remove(array, &needle.to_array_of_size(array.len())?),
}
}
fn remove_float_constant<T>(array: &ListArray, needle: T::Native) -> Result<ArrayRef>
where
T: ArrowPrimitiveType,
T::Native: Float,
{
let offsets = array.offsets();
let first = offsets[0] as usize;
let values = array
.values()
.slice(first, offsets[array.len()] as usize - first);
let floats = values.as_primitive::<T>();
let buffer = floats.values();
// For a fixed value, Spark's genEqual is one test per element: IEEE `!=`, under which the two
// zeros are equal and a NaN differs from every number, or `is_nan` for a NaN value.
let differs = if needle.is_nan() {
BooleanBuffer::collect_bool(buffer.len(), |index| !buffer[index].is_nan())
} else {
BooleanBuffer::collect_bool(buffer.len(), |index| buffer[index] != needle)
};
// Null elements stay.
let keep = match floats.nulls() {
Some(nulls) => &differs | &!nulls.inner(),
None => differs,
};
// A null row keeps its elements in the values; the list's null hides them.
let kept_offsets = kept_offsets(&keep, offsets);
let kept_values = filter(&values, &BooleanArray::new(keep, None))?;
Ok(with_values(
array,
kept_offsets,
kept_values,
array.nulls().cloned(),
))
}
/// The offsets of the elements that `keep` keeps, for a `keep` that starts at `offsets[0]`: the
/// number of its set bits before each offset, counted a word at a time in one pass.
fn kept_offsets(keep: &BooleanBuffer, offsets: &OffsetBuffer<i32>) -> OffsetBuffer<i32> {
let chunks = keep.inner().bit_chunks(keep.offset(), keep.len());
let mut words = chunks.iter_padded();
let (mut word, mut word_start, mut before_word) = (words.next().unwrap_or(0), 0, 0);
let kept = offsets.iter().map(|offset| {
let end = (offset - offsets[0]) as usize;
while end >= word_start + 64 {
before_word += word.count_ones() as usize;
word = words.next().unwrap_or(0);
word_start += 64;
}
let below_end = word & ((1u64 << (end - word_start)) - 1);
(before_word + below_end.count_ones() as usize) as i32
});
OffsetBuffer::new(kept.collect::<Vec<i32>>().into())
}
/// Copies the floats of each row that are null or differ from the row's value in Spark's
/// `genEqual`, straight from the flattened values. A row is null when the array or the value is.
fn remove_floats<T>(array: &ListArray, value: &ArrayRef) -> ArrayRef
where
T: ArrowPrimitiveType,
T::Native: Float,
{
let values = array.values().as_primitive::<T>();
let needles = value.as_primitive::<T>().values();
let (buffer, nulls) = (values.values(), values.nulls());
let row_nulls = NullBuffer::union(array.nulls(), value.nulls());
let mut kept: Vec<T::Native> = Vec::with_capacity(values.len());
let mut validity = nulls.map(|_| BooleanBufferBuilder::new(values.len()));
let mut offsets = Vec::with_capacity(array.len() + 1);
offsets.push(0i32);
for (row, bounds) in array.offsets().windows(2).enumerate() {
if row_nulls.as_ref().is_none_or(|nulls| nulls.is_valid(row)) {
let needle = needles[row];
let (start, end) = (bounds[0] as usize, bounds[1] as usize);
match (nulls, validity.as_mut()) {
(Some(nulls), Some(validity)) => {
for index in start..end {
let valid = nulls.is_valid(index);
if !valid || !compare_floats(buffer[index], needle).is_eq() {
kept.push(buffer[index]);
validity.append(valid);
}
}
}
_ => kept.extend(
buffer[start..end]
.iter()
.copied()
.filter(|&element| !compare_floats(element, needle).is_eq()),
),
}
}
offsets.push(kept.len() as i32);
}
let kept = PrimitiveArray::<T>::new(
kept.into(),
validity.map(|mut validity| NullBuffer::new(validity.finish())),
);
with_values(
array,
OffsetBuffer::new(offsets.into()),
Arc::new(kept),
row_nulls,
)
}
/// Copies each row's elements except the non-null ones for which `equal(element, row)` holds. A
/// row is null when the array or the value is.
fn remove_where<F>(array: &ListArray, value: &ArrayRef, equal: F) -> Result<ArrayRef>
where
F: Fn(usize, usize) -> bool,
{
let values = array.values();
let element_nulls = values.logical_nulls();
let row_nulls = NullBuffer::union(array.nulls(), value.nulls());
let mut keep = BooleanBufferBuilder::new(values.len());
keep.append_n(values.len(), false);
let mut offsets = Vec::with_capacity(array.len() + 1);
offsets.push(0i32);
let mut kept = 0i32;
for (row, bounds) in array.offsets().windows(2).enumerate() {
if row_nulls.as_ref().is_none_or(|nulls| nulls.is_valid(row)) {
for element in bounds[0] as usize..bounds[1] as usize {
let is_null = element_nulls
.as_ref()
.is_some_and(|nulls| nulls.is_null(element));
if is_null || !equal(element, row) {
keep.set_bit(element, true);
kept += 1;
}
}
}
offsets.push(kept);
}
let kept_values = filter(values, &BooleanArray::new(keep.finish(), None))?;
Ok(with_values(
array,
OffsetBuffer::new(offsets.into()),
kept_values,
row_nulls,
))
}
#[cfg(test)]
mod tests {
use super::super::test_util::{bits, first_row_field_bits, list};
use super::*;
use crate::float_semantics::{EDGE_VALUES, NEGATIVE_NAN};
use arrow::array::{Float32Array, Float64Array, StructArray};
use arrow::datatypes::{Field, Fields};
use datafusion::config::ConfigOptions;
fn invoke(array: ColumnarValue, value: ColumnarValue, rows: usize) -> Result<ColumnarValue> {
let return_field = Arc::new(Field::new("result", array.data_type(), true));
SparkArrayRemove::default().invoke_with_args(ScalarFunctionArgs {
args: vec![array, value],
arg_fields: vec![],
number_rows: rows,
return_field,
config_options: Arc::new(ConfigOptions::default()),
})
}
/// Spark's result for one row: drop the non-null elements that `genEqual` the value.
fn spark_remove(row: &[Option<f64>], value: f64) -> Vec<Option<u64>> {
row.iter()
.filter(|element| element.is_none_or(|e| !compare_floats(e, value).is_eq()))
.map(|element| element.map(f64::to_bits))
.collect()
}
/// Every edge value removed from an array of all edge values, so each zero and each NaN meets
/// every other, and the kept elements keep their bits.
#[test]
fn floats_follow_spark_equality() -> Result<()> {
let elements: Vec<Option<f64>> = EDGE_VALUES.to_vec();
let removable: Vec<f64> = EDGE_VALUES.iter().flatten().copied().collect();
let rows: Vec<Option<Vec<Option<f64>>>> =
removable.iter().map(|_| Some(elements.clone())).collect();
let result = invoke(
ColumnarValue::Array(list(&rows)),
ColumnarValue::Array(Arc::new(Float64Array::from(removable.clone()))),
rows.len(),
)?
.into_array(rows.len())?;
let expected: Vec<Option<Vec<Option<u64>>>> = removable
.iter()
.map(|&value| Some(spark_remove(&elements, value)))
.collect();
assert_eq!(bits(&result), expected);
Ok(())
}
/// Removing a constant takes one pass over all the values, which must still respect each row's
/// bounds, null rows and null elements, in a sliced array too.
#[test]
fn constant_value() -> Result<()> {
let rows = vec![
Some(vec![Some(1.0), Some(-0.0)]),
Some(vec![Some(0.0), None, Some(NEGATIVE_NAN)]),
None,
Some(vec![Some(f64::NAN), Some(0.0), Some(2.0)]),
];
let full = list(&rows);
for (array, rows) in [
(Arc::clone(&full), &rows[..]),
(full.slice(1, 3), &rows[1..]),
] {
for needle in [Some(0.0), Some(f64::NAN), None] {
let result = invoke(
ColumnarValue::Array(Arc::clone(&array)),
ColumnarValue::Scalar(ScalarValue::Float64(needle)),
rows.len(),
)?
.into_array(rows.len())?;
let expected: Vec<Option<Vec<Option<u64>>>> = rows
.iter()
.map(|row| {
let needle = needle?;
row.as_ref().map(|row| spark_remove(row, needle))
})
.collect();
assert_eq!(bits(&result), expected, "remove {needle:?}");
}
}
Ok(())
}
/// Kept counts are read from the mask a 64-bit word at a time, so these rows end on word
/// boundaries and inside words, span several words, or are empty or null, and so do those of
/// the slice, whose values start at a word boundary.
#[test]
fn constant_value_across_words() -> Result<()> {
let lengths = [64, 0, 64, 1, 63, 128, 5, 59, 130, 0, 70, 3];
let rows: Vec<Option<Vec<Option<f64>>>> = lengths
.iter()
.enumerate()
.map(|(row, &length)| {
(row != 7).then(|| {
(0..length)
.map(|i| EDGE_VALUES[(row + i) % EDGE_VALUES.len()])
.collect()
})
})
.collect();
let full = list(&rows);
for (array, rows) in [
(Arc::clone(&full), &rows[..]),
(full.slice(2, 8), &rows[2..10]),
] {
for needle in [0.0, f64::NAN, 1.0] {
let result = invoke(
ColumnarValue::Array(Arc::clone(&array)),
ColumnarValue::Scalar(ScalarValue::Float64(Some(needle))),
rows.len(),
)?
.into_array(rows.len())?;
let expected: Vec<Option<Vec<Option<u64>>>> = rows
.iter()
.map(|row| row.as_ref().map(|row| spark_remove(row, needle)))
.collect();
assert_eq!(bits(&result), expected, "remove {needle}");
}
}
Ok(())
}
#[test]
fn nulls() -> Result<()> {
let rows = vec![
Some(vec![Some(0.0), None, Some(-0.0)]),
None,
Some(vec![Some(1.0)]),
];
let value = Float64Array::from(vec![Some(0.0), Some(0.0), None]);
let result = invoke(
ColumnarValue::Array(list(&rows)),
ColumnarValue::Array(Arc::new(value)),
3,
)?
.into_array(3)?;
// The null element stays; a null array or a null value gives a null row.
assert_eq!(bits(&result), vec![Some(vec![None]), None, None]);
Ok(())
}
#[test]
fn float32_and_scalars() -> Result<()> {
let array = ScalarValue::List(Arc::new(
ListArray::from_iter_primitive::<Float32Type, _, _>(vec![Some(vec![
Some(-0.0f32),
Some(f32::from_bits(0xffc0_0000)),
Some(1.0),
])]),
));
let value = ScalarValue::Float32(Some(0.0));
let result = invoke(
ColumnarValue::Scalar(array.clone()),
ColumnarValue::Scalar(value),
4,
)?;
let ColumnarValue::Scalar(ScalarValue::List(result)) = result else {
panic!("expected a list scalar");
};
let values = result.value(0);
let values: &Float32Array = values.as_primitive();
assert_eq!(values.len(), 2, "only the zero is removed");
let result = invoke(
ColumnarValue::Scalar(array),
ColumnarValue::Scalar(ScalarValue::Float32(Some(f32::NAN))),
1,
)?;
let ColumnarValue::Scalar(ScalarValue::List(result)) = result else {
panic!("expected a list scalar");
};
let values = result.value(0);
let values: &Float32Array = values.as_primitive();
assert_eq!(values.values().as_ref(), &[-0.0, 1.0]);
assert_eq!(values.value(0).to_bits(), (-0.0f32).to_bits());
Ok(())
}
/// Structs compare their float fields in Spark's order too.
#[test]
fn nested_elements() -> Result<()> {
let fields = Fields::from(vec![Field::new("x", DataType::Float64, true)]);
let structs = |values: Vec<f64>| -> ArrayRef {
Arc::new(StructArray::new(
fields.clone(),
vec![Arc::new(Float64Array::from(values))],
None,
))
};
let element = Field::new("item", DataType::Struct(fields.clone()), true);
let array = ListArray::new(
Arc::new(element),
OffsetBuffer::from_lengths([3]),
structs(vec![-0.0, NEGATIVE_NAN, 1.0]),
None,
);
for (value, expected) in [(0.0, vec![NEGATIVE_NAN, 1.0]), (f64::NAN, vec![-0.0, 1.0])] {
let result = invoke(
ColumnarValue::Array(Arc::new(array.clone())),
ColumnarValue::Array(structs(vec![value])),
1,
)?
.into_array(1)?;
let expected: Vec<u64> = expected.iter().map(|v| v.to_bits()).collect();
assert_eq!(first_row_field_bits(&result), expected, "remove {value}");
}
Ok(())
}
/// Spark casts the value to the element type, so a value of another type is an error, not a
/// panic, whether it is a constant or a column.
#[test]
fn mismatched_value_type() {
let array = list(&[Some(vec![Some(1.0)])]);
for value in [
ColumnarValue::Scalar(ScalarValue::Float32(Some(1.0))),
ColumnarValue::Array(Arc::new(Float32Array::from(vec![1.0f32]))),
] {
assert!(invoke(ColumnarValue::Array(Arc::clone(&array)), value, 1).is_err());
}
}
}