blob: a0da6f52fb37c81ed885b7eb60a10142540f1de2 [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 crate::arithmetic_overflow_error;
use crate::SparkError;
use arrow::array::RecordBatch;
use arrow::compute::kernels::numeric::{neg, neg_wrapping};
use arrow::datatypes::IntervalDayTimeType;
use arrow::datatypes::{DataType, IntervalUnit, Schema};
use arrow::error::ArrowError;
use datafusion::common::{DataFusionError, Result, ScalarValue};
use datafusion::logical_expr::sort_properties::ExprProperties;
use datafusion::{
logical_expr::{interval_arithmetic::Interval, ColumnarValue},
physical_expr::PhysicalExpr,
};
use std::fmt::{Display, Formatter};
use std::hash::Hash;
use std::sync::Arc;
pub fn create_negate_expr(
expr: Arc<dyn PhysicalExpr>,
fail_on_error: bool,
) -> Result<Arc<dyn PhysicalExpr>, DataFusionError> {
Ok(Arc::new(NegativeExpr::new(expr, fail_on_error)))
}
/// Negative expression
#[derive(Debug, Eq)]
pub struct NegativeExpr {
/// Input expression
arg: Arc<dyn PhysicalExpr>,
fail_on_error: bool,
}
impl Hash for NegativeExpr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.arg.hash(state);
self.fail_on_error.hash(state);
}
}
impl PartialEq for NegativeExpr {
fn eq(&self, other: &Self) -> bool {
self.arg.eq(&other.arg) && self.fail_on_error.eq(&other.fail_on_error)
}
}
impl NegativeExpr {
/// Create new not expression
pub fn new(arg: Arc<dyn PhysicalExpr>, fail_on_error: bool) -> Self {
Self { arg, fail_on_error }
}
/// Get the input expression
pub fn arg(&self) -> &Arc<dyn PhysicalExpr> {
&self.arg
}
}
impl std::fmt::Display for NegativeExpr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "(- {})", self.arg)
}
}
fn map_neg_error(err: ArrowError, from_type: &'static str) -> DataFusionError {
match err {
ArrowError::ArithmeticOverflow(_) => arithmetic_overflow_error(from_type).into(),
other => DataFusionError::from(other),
}
}
impl PhysicalExpr for NegativeExpr {
fn data_type(&self, input_schema: &Schema) -> Result<DataType> {
self.arg.data_type(input_schema)
}
fn nullable(&self, input_schema: &Schema) -> Result<bool> {
self.arg.nullable(input_schema)
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
let arg = self.arg.evaluate(batch)?;
// Overflow checks only apply in ANSI mode, and only the types listed in the
// match below have a Spark overflow message. Everything else (float, decimal,
// `Interval(MonthDayNano)`, ...) falls through to `neg_wrapping`.
match arg {
ColumnarValue::Array(array) => {
if !self.fail_on_error {
return Ok(ColumnarValue::Array(neg_wrapping(array.as_ref())?));
}
// The shims render this as `{from_type} overflow` under
// `ARITHMETIC_OVERFLOW`. For byte/short that is byte-identical to Spark
// 4.x, which routes them through `MathUtils.negateExact` ("byte overflow" /
// "short overflow"). Spark 3.4/3.5 instead throw
// `_LEGACY_ERROR_TEMP_2043` ("- <sqlValue> caused overflow."); that error
// class is out of reach here, so no string can match every version.
let from_type = match array.data_type() {
DataType::Int8 => "byte",
DataType::Int16 => "short",
DataType::Int32 => "integer",
DataType::Int64 => "long",
// `neg` checks each `DayTime` component, so either `days` or `ms` at
// `i32::MIN` overflows; routing intervals here maps the Arrow overflow
// error onto Spark's, matching the scalar path below.
DataType::Interval(IntervalUnit::YearMonth | IntervalUnit::DayTime) => {
"interval"
}
// Everything else falls through to `neg_wrapping`. Note it wraps only
// for integers: for any other type, `Interval(MonthDayNano)` included,
// it delegates to `neg`, so overflow is still detected -- it just
// surfaces as an Arrow error rather than a Spark one.
_ => return Ok(ColumnarValue::Array(neg_wrapping(array.as_ref())?)),
};
Ok(ColumnarValue::Array(
neg(array.as_ref()).map_err(|e| map_neg_error(e, from_type))?,
))
}
ColumnarValue::Scalar(scalar) => {
if self.fail_on_error {
match scalar {
// Keep scalar overflow type names aligned with the array path.
ScalarValue::Int8(Some(i8::MIN)) => {
return Err(arithmetic_overflow_error("byte").into());
}
ScalarValue::Int16(Some(i16::MIN)) => {
return Err(arithmetic_overflow_error("short").into());
}
ScalarValue::Int32(Some(i32::MIN)) => {
return Err(arithmetic_overflow_error("integer").into());
}
ScalarValue::Int64(Some(i64::MIN)) => {
return Err(arithmetic_overflow_error("long").into());
}
ScalarValue::IntervalDayTime(value) => {
let (days, ms) =
IntervalDayTimeType::to_parts(value.unwrap_or_default());
if days == i32::MIN || ms == i32::MIN {
return Err(arithmetic_overflow_error("interval").into());
}
}
ScalarValue::IntervalYearMonth(Some(i32::MIN)) => {
return Err(arithmetic_overflow_error("interval").into());
}
ScalarValue::IntervalYearMonth(_) => {}
_ => {
// Overflow checks are not supported for other datatypes
}
}
}
Ok(ColumnarValue::Scalar((scalar.arithmetic_negate())?))
}
}
}
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
vec![&self.arg]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
Ok(Arc::new(NegativeExpr::new(
Arc::clone(&children[0]),
self.fail_on_error,
)))
}
/// Given the child interval of a NegativeExpr, it calculates the NegativeExpr's interval.
/// It replaces the upper and lower bounds after multiplying them with -1.
/// Ex: `(a, b]` => `[-b, -a)`
fn evaluate_bounds(&self, children: &[&Interval]) -> Result<Interval> {
Interval::try_new(
children[0].upper().arithmetic_negate()?,
children[0].lower().arithmetic_negate()?,
)
}
/// Returns a new [`Interval`] of a NegativeExpr that has the existing `interval` given that
/// given the input interval is known to be `children`.
fn propagate_constraints(
&self,
interval: &Interval,
children: &[&Interval],
) -> Result<Option<Vec<Interval>>> {
let child_interval = children[0];
if child_interval.lower() == &ScalarValue::Int32(Some(i32::MIN))
|| child_interval.upper() == &ScalarValue::Int32(Some(i32::MIN))
|| child_interval.lower() == &ScalarValue::Int64(Some(i64::MIN))
|| child_interval.upper() == &ScalarValue::Int64(Some(i64::MIN))
{
return Err(SparkError::ArithmeticOverflow {
from_type: "long".to_string(),
}
.into());
}
let negated_interval = Interval::try_new(
interval.upper().arithmetic_negate()?,
interval.lower().arithmetic_negate()?,
)?;
Ok(child_interval
.intersect(negated_interval)?
.map(|result| vec![result]))
}
/// The ordering of a [`NegativeExpr`] is simply the reverse of its child.
fn get_properties(&self, children: &[ExprProperties]) -> Result<ExprProperties> {
let properties = children[0].clone().with_order(children[0].sort_properties);
Ok(properties)
}
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::{array::*, buffer::NullBuffer, datatypes::*};
use datafusion::{
physical_expr::expressions::{Column, Literal},
physical_plan::ColumnarValue,
};
fn eval_array(array: ArrayRef, fail_on_error: bool) -> Result<ColumnarValue> {
let schema = Arc::new(Schema::new(vec![Field::new(
"a",
array.data_type().clone(),
true,
)]));
let batch = RecordBatch::try_new(Arc::clone(&schema), vec![array])?;
NegativeExpr::new(Arc::new(Column::new("a", 0)), fail_on_error).evaluate(&batch)
}
fn eval_scalar(scalar: ScalarValue, fail_on_error: bool) -> Result<ColumnarValue> {
let batch = RecordBatch::new_empty(Arc::new(Schema::empty()));
NegativeExpr::new(Arc::new(Literal::new(scalar)), fail_on_error).evaluate(&batch)
}
fn assert_spark_overflow(err: DataFusionError, expected_from_type: &str) {
if let DataFusionError::External(ref e) = err {
if let Some(SparkError::ArithmeticOverflow { from_type }) =
e.downcast_ref::<SparkError>()
{
assert_eq!(from_type, expected_from_type);
return;
}
}
panic!(
"Expected SparkError::ArithmeticOverflow {{ from_type: {:?} }}, got: {:?}",
expected_from_type, err
);
}
/// Negate `[min, other]` in ANSI mode with slot 0 marked null, and assert the
/// null slot is skipped instead of raising a spurious overflow.
fn assert_null_min_slot_is_skipped<T: ArrowPrimitiveType>(
min: T::Native,
other: T::Native,
negated_other: T::Native,
) {
let nulls = NullBuffer::from(vec![false, true]);
let array: ArrayRef = Arc::new(PrimitiveArray::<T>::new(
vec![min, other].into(),
Some(nulls),
));
let ColumnarValue::Array(result) = eval_array(array, true).unwrap() else {
panic!("expected array result")
};
let result = result.as_primitive::<T>();
assert!(result.is_null(0));
assert_eq!(result.value(1), negated_other);
}
/// A MIN sentinel left behind in a null slot (by filter, slice or FFI) must not raise a
/// spurious ANSI overflow: the overflow check has to consult the null buffer and skip
/// invalid slots. Each case below places `MIN` in a null slot.
#[test]
fn test_ansi_null_slot_with_min_values_does_not_overflow() {
assert_null_min_slot_is_skipped::<Int8Type>(i8::MIN, 7, -7);
assert_null_min_slot_is_skipped::<Int16Type>(i16::MIN, 7, -7);
assert_null_min_slot_is_skipped::<Int32Type>(i32::MIN, 7, -7);
assert_null_min_slot_is_skipped::<Int64Type>(i64::MIN, 7, -7);
assert_null_min_slot_is_skipped::<IntervalYearMonthType>(i32::MIN, 7, -7);
// `IntervalDayTime::MIN` is both components at `i32::MIN`.
assert_null_min_slot_is_skipped::<IntervalDayTimeType>(
IntervalDayTime::MIN,
IntervalDayTime::new(1, 2),
IntervalDayTime::new(-1, -2),
);
}
#[test]
fn test_ansi_valid_min_values_raise_exact_spark_overflow_errors() {
let arr_i8: ArrayRef = Arc::new(Int8Array::from(vec![i8::MIN]));
assert_spark_overflow(eval_array(arr_i8, true).unwrap_err(), "byte");
let arr_i16: ArrayRef = Arc::new(Int16Array::from(vec![i16::MIN]));
assert_spark_overflow(eval_array(arr_i16, true).unwrap_err(), "short");
let arr_i32: ArrayRef = Arc::new(Int32Array::from(vec![i32::MIN]));
assert_spark_overflow(eval_array(arr_i32, true).unwrap_err(), "integer");
let arr_i64: ArrayRef = Arc::new(Int64Array::from(vec![i64::MIN]));
assert_spark_overflow(eval_array(arr_i64, true).unwrap_err(), "long");
let arr_ym: ArrayRef = Arc::new(IntervalYearMonthArray::from(vec![i32::MIN]));
assert_spark_overflow(eval_array(arr_ym, true).unwrap_err(), "interval");
let arr_dt: ArrayRef = Arc::new(IntervalDayTimeArray::from(vec![IntervalDayTime::MIN]));
assert_spark_overflow(eval_array(arr_dt, true).unwrap_err(), "interval");
}
/// A single `DayTime` component at `i32::MIN` overflows: `neg` checks each component
/// (`neg_wrapping` delegates to `neg` for every non-integer type,
/// `downcast_integer! { ..., _ => neg(array) }`). These surface as Spark overflows like
/// every other ANSI overflow in this expression.
#[test]
fn test_ansi_interval_day_time_component_overflow_maps_to_spark_error() {
for value in [
IntervalDayTime::new(i32::MIN, 0),
IntervalDayTime::new(0, i32::MIN),
] {
let array: ArrayRef = Arc::new(IntervalDayTimeArray::from(vec![value]));
assert_spark_overflow(eval_array(array, true).unwrap_err(), "interval");
}
}
/// Negate `[min, null]` in legacy mode and assert `min` wraps to itself.
fn assert_legacy_wraps_min<T: ArrowPrimitiveType>(min: T::Native) {
let array: ArrayRef = Arc::new(PrimitiveArray::<T>::new(
vec![min, T::Native::default()].into(),
Some(NullBuffer::from(vec![true, false])),
));
let ColumnarValue::Array(result) = eval_array(array, false).unwrap() else {
panic!("expected array result")
};
let result = result.as_primitive::<T>();
assert_eq!(result.value(0), min);
assert!(result.is_null(1));
}
#[test]
fn test_legacy_mode_wraps_min_values() {
assert_legacy_wraps_min::<Int8Type>(i8::MIN);
assert_legacy_wraps_min::<Int16Type>(i16::MIN);
assert_legacy_wraps_min::<Int32Type>(i32::MIN);
assert_legacy_wraps_min::<Int64Type>(i64::MIN);
}
#[test]
fn test_mixed_ordinary_values() {
let arr: ArrayRef = Arc::new(Int32Array::from(vec![Some(-7), Some(0), Some(12), None]));
// ANSI mode
let ColumnarValue::Array(res_ansi) = eval_array(Arc::clone(&arr), true).unwrap() else {
panic!("expected array result")
};
assert_eq!(
res_ansi.as_primitive::<Int32Type>(),
&Int32Array::from(vec![Some(7), Some(0), Some(-12), None])
);
// Legacy mode
let ColumnarValue::Array(res_legacy) = eval_array(Arc::clone(&arr), false).unwrap() else {
panic!("expected array result")
};
assert_eq!(
res_legacy.as_primitive::<Int32Type>(),
&Int32Array::from(vec![Some(7), Some(0), Some(-12), None])
);
}
#[test]
fn test_interval_month_day_nano_preserves_existing_dispatch() {
let arr: ArrayRef = Arc::new(IntervalMonthDayNanoArray::from(vec![
Some(IntervalMonthDayNano::new(1, 2, 3)),
None,
]));
let ColumnarValue::Array(res) = eval_array(arr, true).unwrap() else {
panic!("expected array result")
};
let p = res.as_primitive::<IntervalMonthDayNanoType>();
assert_eq!(p.value(0), IntervalMonthDayNano::new(-1, -2, -3));
assert!(p.is_null(1));
}
#[test]
fn test_scalar_negation() {
// Valid scalar
let ColumnarValue::Scalar(res_valid) =
eval_scalar(ScalarValue::Int32(Some(42)), true).unwrap()
else {
panic!("expected scalar result")
};
assert_eq!(res_valid, ScalarValue::Int32(Some(-42)));
// Null scalar
let ColumnarValue::Scalar(res_null) = eval_scalar(ScalarValue::Int32(None), true).unwrap()
else {
panic!("expected scalar result")
};
assert_eq!(res_null, ScalarValue::Int32(None));
// MIN scalar overflows in ANSI, with the same messages as the array path
for (scalar, from_type) in [
(ScalarValue::Int8(Some(i8::MIN)), "byte"),
(ScalarValue::Int16(Some(i16::MIN)), "short"),
(ScalarValue::Int32(Some(i32::MIN)), "integer"),
(ScalarValue::Int64(Some(i64::MIN)), "long"),
] {
assert_spark_overflow(eval_scalar(scalar, true).unwrap_err(), from_type);
}
}
#[test]
fn test_map_neg_error_preserves_non_overflow_errors() {
let err = map_neg_error(
ArrowError::InvalidArgumentError("test custom error".to_string()),
"integer",
);
assert!(
matches!(&err, DataFusionError::ArrowError(inner, _)
if matches!(inner.as_ref(), ArrowError::InvalidArgumentError(msg) if msg == "test custom error")),
"expected the ArrowError to pass through unchanged, got: {err:?}"
);
}
}