| // 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(), | |
| function_name: String::new(), | |
| } | |
| .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:?}" | |
| ); | |
| } | |
| } |