blob: d70c062987c3330c7b228aea2fd69581da8a8267 [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 arrow::array::{Array, ArrowNativeTypeOp, BooleanBufferBuilder, PrimitiveArray};
use arrow::array::{ArrayRef, AsArray};
use crate::{divide_by_zero_error, EvalMode, SparkError};
use array::{Datum, Scalar};
use arrow::array;
use arrow::buffer::NullBuffer;
use arrow::compute::kernels::{arity, numeric};
use arrow::datatypes::{
ArrowPrimitiveType, DataType, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type,
Int64Type, Int8Type,
};
use arrow::error::ArrowError;
use datafusion::common::DataFusionError;
use datafusion::common::ScalarValue;
use datafusion::physical_plan::ColumnarValue;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MathOp {
Add,
Sub,
Mul,
Div,
}
impl MathOp {
fn function_name(self) -> &'static str {
match self {
Self::Add => "try_add",
Self::Sub => "try_subtract",
Self::Mul => "try_multiply",
Self::Div => "try_divide",
}
}
}
/// Arrow's error does not carry the failing row. Only after its fast kernel reports an
/// overflow, recover the first non-null overflowing pair for Spark's Byte/Short error.
/// Keep scalar broadcasting and array offsets intact; never inspect values beneath NULLs.
fn binary_overflow_error<T: ArrowPrimitiveType>(
left: &dyn Datum,
right: &dyn Datum,
op: MathOp,
) -> Option<SparkError>
where
T::Native: std::fmt::Display,
{
let (left, left_scalar) = left.get();
let (right, right_scalar) = right.get();
let left = left.as_primitive::<T>();
let right = right.as_primitive::<T>();
let len = if left_scalar { right.len() } else { left.len() };
for row in 0..len {
let l = if left_scalar { 0 } else { row };
let r = if right_scalar { 0 } else { row };
if left.is_null(l) || right.is_null(r) {
continue;
}
let l = left.value(l);
let r = right.value(r);
let (result, symbol) = match op {
MathOp::Add => (l.add_checked(r), "+"),
MathOp::Sub => (l.sub_checked(r), "-"),
MathOp::Mul => (l.mul_checked(r), "*"),
MathOp::Div => return None,
};
if result.is_err() {
return Some(SparkError::BinaryArithmeticOverflow {
value1: l.to_string(),
symbol: symbol.to_string(),
value2: r.to_string(),
function_name: op.function_name().to_string(),
});
}
}
None
}
fn try_arithmetic_kernel<T>(
left: &PrimitiveArray<T>,
right: &PrimitiveArray<T>,
op: MathOp,
) -> Result<ArrayRef, DataFusionError>
where
T: ArrowPrimitiveType,
{
match op {
MathOp::Add => checked_binary(left, right, |l, r| l.add_checked(r)),
MathOp::Sub => checked_binary(left, right, |l, r| l.sub_checked(r)),
MathOp::Mul => checked_binary(left, right, |l, r| l.mul_checked(r)),
MathOp::Div => checked_binary(left, right, |l, r| l.div_checked(r)),
}
}
fn integer_type_name(data_type: &DataType) -> Result<&'static str, DataFusionError> {
match data_type {
DataType::Int64 => Ok("long"),
DataType::Int32 => Ok("integer"),
DataType::Int16 => Ok("short"),
DataType::Int8 => Ok("byte"),
_ => Err(DataFusionError::Internal(format!(
"Unsupported integer data type: {:?}",
data_type
))),
}
}
fn float_type_name(data_type: &DataType) -> Result<&'static str, DataFusionError> {
match data_type {
DataType::Float64 => Ok("double"),
DataType::Float32 | DataType::Float16 => Ok("float"),
_ => Err(DataFusionError::Internal(format!(
"Unsupported float data type: {:?}",
data_type
))),
}
}
fn ansi_arithmetic_kernel(
left: &ColumnarValue,
right: &ColumnarValue,
op: MathOp,
data_type: &DataType,
) -> Result<ColumnarValue, DataFusionError> {
let from_type = integer_type_name(data_type)?;
let run_kernel = |l: &dyn Datum, r: &dyn Datum| {
let result = match op {
MathOp::Add => numeric::add(l, r),
MathOp::Sub => numeric::sub(l, r),
MathOp::Mul => numeric::mul(l, r),
MathOp::Div => numeric::div(l, r),
};
result.map_err(|error| match error {
ArrowError::DivideByZero => divide_by_zero_error().into(),
ArrowError::ArithmeticOverflow(_) => {
let binary_error = if op != MathOp::Div {
match data_type {
DataType::Int8 => binary_overflow_error::<Int8Type>(l, r, op),
DataType::Int16 => binary_overflow_error::<Int16Type>(l, r, op),
_ => None,
}
} else {
None
};
DataFusionError::from(binary_error.unwrap_or_else(|| {
SparkError::ArithmeticOverflow {
from_type: from_type.to_string(),
function_name: op.function_name().to_string(),
}
}))
}
_ => error.into(),
})
};
let is_scalars = matches!(
(left, right),
(ColumnarValue::Scalar(_), ColumnarValue::Scalar(_))
);
let result_array = match (left, right) {
(ColumnarValue::Array(l), ColumnarValue::Array(r)) => run_kernel(&l, &r),
(ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
let l_arr = l.to_array()?;
let l_scalar = Scalar::new(l_arr);
run_kernel(&l_scalar, &r)
}
(ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
let r_arr = r.to_array()?;
let r_scalar = Scalar::new(r_arr);
run_kernel(&l, &r_scalar)
}
(ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => {
let l_arr = l.to_array()?;
let r_arr = r.to_array()?;
let l_scalar = Scalar::new(l_arr);
let r_scalar = Scalar::new(r_arr);
run_kernel(&l_scalar, &r_scalar)
}
};
let array = result_array?;
if is_scalars {
let scalar_val = ScalarValue::try_from_array(array.as_ref(), 0)?;
Ok(ColumnarValue::Scalar(scalar_val))
} else {
Ok(ColumnarValue::Array(array))
}
}
fn ansi_float_div<T>(
left: &PrimitiveArray<T>,
right: &PrimitiveArray<T>,
) -> Result<ArrayRef, DataFusionError>
where
T: ArrowPrimitiveType,
{
let from_type = float_type_name(left.data_type())?;
arity::try_binary::<_, _, _, T>(left, right, |l, r| l.div_checked(r))
.map(|array| Arc::new(array) as ArrayRef)
.map_err(|e| match e {
ArrowError::DivideByZero => divide_by_zero_error().into(),
_ => DataFusionError::from(SparkError::ArithmeticOverflow {
from_type: String::from(from_type),
function_name: String::new(),
}),
})
}
fn checked_binary<T, F>(
left: &PrimitiveArray<T>,
right: &PrimitiveArray<T>,
op: F,
) -> Result<ArrayRef, DataFusionError>
where
T: ArrowPrimitiveType,
F: Fn(T::Native, T::Native) -> Result<T::Native, ArrowError>,
{
let len = left.len();
let lhs = &left.values()[..len];
let rhs = &right.values()[..len];
let nulls = NullBuffer::union(left.nulls(), right.nulls());
let mut values = vec![T::Native::default(); len];
let mut overflowed: Vec<usize> = Vec::new();
for (i, (out, (&l, &r))) in values.iter_mut().zip(lhs.iter().zip(rhs)).enumerate() {
match op(l, r) {
Ok(v) => *out = v,
Err(_) => {
overflowed.push(i);
}
}
}
let nulls = if overflowed.is_empty() {
nulls
} else {
let mut validity = BooleanBufferBuilder::new(len);
match &nulls {
Some(n) => validity.append_buffer(n.inner()),
None => validity.append_n(len, true),
}
for i in overflowed {
validity.set_bit(i, false);
}
Some(NullBuffer::new(validity.finish()))
};
if let Some(n) = &nulls {
if n.null_count() > 0 {
for (out, valid) in values.iter_mut().zip(n.iter()) {
if !valid {
*out = T::Native::default();
}
}
}
}
Ok(Arc::new(PrimitiveArray::<T>::new(values.into(), nulls)) as ArrayRef)
}
pub fn checked_add(
args: &[ColumnarValue],
data_type: &DataType,
eval_mode: EvalMode,
) -> Result<ColumnarValue, DataFusionError> {
checked_arithmetic_internal(args, data_type, MathOp::Add, eval_mode)
}
pub fn checked_sub(
args: &[ColumnarValue],
data_type: &DataType,
eval_mode: EvalMode,
) -> Result<ColumnarValue, DataFusionError> {
checked_arithmetic_internal(args, data_type, MathOp::Sub, eval_mode)
}
pub fn checked_mul(
args: &[ColumnarValue],
data_type: &DataType,
eval_mode: EvalMode,
) -> Result<ColumnarValue, DataFusionError> {
checked_arithmetic_internal(args, data_type, MathOp::Mul, eval_mode)
}
pub fn checked_div(
args: &[ColumnarValue],
data_type: &DataType,
eval_mode: EvalMode,
) -> Result<ColumnarValue, DataFusionError> {
checked_arithmetic_internal(args, data_type, MathOp::Div, eval_mode)
}
#[inline]
fn is_integer_type(data_type: &DataType) -> bool {
matches!(
data_type,
DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64
)
}
fn checked_arithmetic_internal(
args: &[ColumnarValue],
data_type: &DataType,
op: MathOp,
eval_mode: EvalMode,
) -> Result<ColumnarValue, DataFusionError> {
let left = &args[0];
let right = &args[1];
let is_ansi_mode = match eval_mode {
EvalMode::Try => false,
EvalMode::Ansi => true,
_ => {
return Err(DataFusionError::Internal(format!(
"Unsupported mode : {:?}",
eval_mode
)))
}
};
// Early return for integer types in ANSI mode using the fast Datum/Scalar path
if is_ansi_mode && is_integer_type(data_type) {
return ansi_arithmetic_kernel(left, right, op, data_type);
}
// Materialize operands for Try-mode and float division
let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (left, right) {
(ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
(ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
(l.to_array_of_size(r.len())?, Arc::clone(r))
}
(ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
(Arc::clone(l), r.to_array_of_size(l.len())?)
}
(ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
};
let result_array = match data_type {
DataType::Int8 => try_arithmetic_kernel(
left_arr.as_primitive::<Int8Type>(),
right_arr.as_primitive::<Int8Type>(),
op,
),
DataType::Int16 => try_arithmetic_kernel(
left_arr.as_primitive::<Int16Type>(),
right_arr.as_primitive::<Int16Type>(),
op,
),
DataType::Int32 => try_arithmetic_kernel(
left_arr.as_primitive::<Int32Type>(),
right_arr.as_primitive::<Int32Type>(),
op,
),
DataType::Int64 => try_arithmetic_kernel(
left_arr.as_primitive::<Int64Type>(),
right_arr.as_primitive::<Int64Type>(),
op,
),
DataType::Float16 if op == MathOp::Div => {
if is_ansi_mode {
ansi_float_div(
left_arr.as_primitive::<Float16Type>(),
right_arr.as_primitive::<Float16Type>(),
)
} else {
try_arithmetic_kernel(
left_arr.as_primitive::<Float16Type>(),
right_arr.as_primitive::<Float16Type>(),
op,
)
}
}
DataType::Float32 if op == MathOp::Div => {
if is_ansi_mode {
ansi_float_div(
left_arr.as_primitive::<Float32Type>(),
right_arr.as_primitive::<Float32Type>(),
)
} else {
try_arithmetic_kernel(
left_arr.as_primitive::<Float32Type>(),
right_arr.as_primitive::<Float32Type>(),
op,
)
}
}
DataType::Float64 if op == MathOp::Div => {
if is_ansi_mode {
ansi_float_div(
left_arr.as_primitive::<Float64Type>(),
right_arr.as_primitive::<Float64Type>(),
)
} else {
try_arithmetic_kernel(
left_arr.as_primitive::<Float64Type>(),
right_arr.as_primitive::<Float64Type>(),
op,
)
}
}
_ => Err(DataFusionError::Internal(format!(
"Unsupported data type: {:?}",
data_type
))),
};
Ok(ColumnarValue::Array(result_array?))
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Float64Array, Int32Array};
fn int32_args(left: Vec<Option<i32>>, right: Vec<Option<i32>>) -> Vec<ColumnarValue> {
vec![
ColumnarValue::Array(Arc::new(Int32Array::from(left))),
ColumnarValue::Array(Arc::new(Int32Array::from(right))),
]
}
fn as_int32(value: ColumnarValue) -> Int32Array {
let ColumnarValue::Array(array) = value else {
unreachable!()
};
array.as_primitive::<Int32Type>().clone()
}
#[test]
fn test_checked_add_propagates_nulls() {
let args = int32_args(
vec![Some(1), None, Some(3), None],
vec![Some(10), Some(20), None, None],
);
let result = as_int32(checked_add(&args, &DataType::Int32, EvalMode::Ansi).unwrap());
assert_eq!(result, Int32Array::from(vec![Some(11), None, None, None]));
}
#[test]
fn test_checked_add_overflow_is_null_in_try_mode() {
let args = int32_args(vec![Some(i32::MAX), Some(1)], vec![Some(1), Some(1)]);
let result = as_int32(checked_add(&args, &DataType::Int32, EvalMode::Try).unwrap());
assert_eq!(result, Int32Array::from(vec![None, Some(2)]));
}
#[test]
fn test_checked_add_overflow_errors_in_ansi_mode() {
let args = int32_args(vec![Some(i32::MAX)], vec![Some(1)]);
let result = checked_add(&args, &DataType::Int32, EvalMode::Ansi);
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.message()
.contains("[ARITHMETIC_OVERFLOW] integer overflow"));
}
#[test]
fn test_checked_sub_and_mul_overflow() {
let args = int32_args(vec![Some(i32::MIN), Some(5)], vec![Some(1), Some(3)]);
let result = as_int32(checked_sub(&args, &DataType::Int32, EvalMode::Try).unwrap());
assert_eq!(result, Int32Array::from(vec![None, Some(2)]));
let args = int32_args(vec![Some(i32::MAX), Some(5)], vec![Some(2), Some(3)]);
let result = as_int32(checked_mul(&args, &DataType::Int32, EvalMode::Try).unwrap());
assert_eq!(result, Int32Array::from(vec![None, Some(15)]));
}
#[test]
fn test_checked_div_by_zero() {
let args = vec![
ColumnarValue::Array(Arc::new(Float64Array::from(vec![Some(1.0), Some(8.0)]))),
ColumnarValue::Array(Arc::new(Float64Array::from(vec![Some(0.0), Some(2.0)]))),
];
let ColumnarValue::Array(array) =
checked_div(&args, &DataType::Float64, EvalMode::Try).unwrap()
else {
unreachable!()
};
assert_eq!(
array.as_primitive::<Float64Type>(),
&Float64Array::from(vec![None, Some(4.0)])
);
assert!(checked_div(&args, &DataType::Float64, EvalMode::Ansi).is_err());
}
/// A null slot may hold any value in its underlying buffer (e.g. after a filter or a slice).
/// Such a row must not overflow-error in ANSI mode, and its result slot must read back as a
/// null holding the default value.
#[test]
fn test_null_row_with_garbage_value_does_not_error_in_ansi_mode() {
let nulls = NullBuffer::from(vec![false, true]);
let left = Int32Array::new(vec![i32::MAX, 1].into(), Some(nulls));
let right = Int32Array::from(vec![Some(1), Some(1)]);
let args = vec![
ColumnarValue::Array(Arc::new(left)),
ColumnarValue::Array(Arc::new(right)),
];
let result = as_int32(checked_add(&args, &DataType::Int32, EvalMode::Ansi).unwrap());
assert_eq!(result, Int32Array::from(vec![None, Some(2)]));
assert_eq!(result.values()[0], 0);
}
#[test]
fn test_ansi_integer_div_by_zero() {
let args = int32_args(vec![Some(10)], vec![Some(0)]);
let result = checked_div(&args, &DataType::Int32, EvalMode::Ansi);
assert!(result.is_err());
}
#[test]
fn test_ansi_scalar_operand() {
let args = vec![
ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(10), Some(20)]))),
ColumnarValue::Scalar(ScalarValue::Int32(Some(5))),
];
let result = as_int32(checked_add(&args, &DataType::Int32, EvalMode::Ansi).unwrap());
assert_eq!(result, Int32Array::from(vec![Some(15), Some(25)]));
}
#[test]
fn test_ansi_int64_overflow() {
let args = vec![
ColumnarValue::Array(Arc::new(array::Int64Array::from(vec![Some(i64::MAX)]))),
ColumnarValue::Array(Arc::new(array::Int64Array::from(vec![Some(1)]))),
];
let result = checked_add(&args, &DataType::Int64, EvalMode::Ansi);
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.message()
.contains("[ARITHMETIC_OVERFLOW] long overflow"));
}
#[test]
fn test_ansi_scalar_operands() {
let args_right = vec![
ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(10), Some(20)]))),
ColumnarValue::Scalar(ScalarValue::Int32(Some(5))),
];
let res_right =
as_int32(checked_add(&args_right, &DataType::Int32, EvalMode::Ansi).unwrap());
assert_eq!(res_right, Int32Array::from(vec![Some(15), Some(25)]));
let args_left = vec![
ColumnarValue::Scalar(ScalarValue::Int32(Some(5))),
ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(10), Some(20)]))),
];
let res_left = as_int32(checked_add(&args_left, &DataType::Int32, EvalMode::Ansi).unwrap());
assert_eq!(res_left, Int32Array::from(vec![Some(15), Some(25)]));
let args_scalar = vec![
ColumnarValue::Scalar(ScalarValue::Int32(Some(10))),
ColumnarValue::Scalar(ScalarValue::Int32(Some(20))),
];
let res_scalar = checked_add(&args_scalar, &DataType::Int32, EvalMode::Ansi).unwrap();
match res_scalar {
ColumnarValue::Scalar(ScalarValue::Int32(v)) => assert_eq!(v, Some(30)),
_ => panic!("Expected scalar result"),
}
}
fn overflow_json(error: DataFusionError) -> serde_json::Value {
let DataFusionError::External(error) = error else {
panic!("Expected structured Spark error, got {error:?}")
};
let error = error.downcast_ref::<SparkError>().unwrap();
serde_json::from_str(&error.to_json()).unwrap()
}
#[test]
fn test_ansi_integral_overflow_payloads() {
// Both overflow signs and all scalar/array shapes. In particular, subtraction must
// preserve operand order when the scalar is on the left.
for (data_type, min, max) in [
(DataType::Int8, i8::MIN as i64, i8::MAX as i64),
(DataType::Int16, i16::MIN as i64, i16::MAX as i64),
(DataType::Int32, i32::MIN as i64, i32::MAX as i64),
(DataType::Int64, i64::MIN, i64::MAX),
] {
let value = |v| match data_type {
DataType::Int8 => ScalarValue::Int8(Some(v as i8)),
DataType::Int16 => ScalarValue::Int16(Some(v as i16)),
DataType::Int32 => ScalarValue::Int32(Some(v as i32)),
DataType::Int64 => ScalarValue::Int64(Some(v)),
_ => unreachable!(),
};
for (op, symbol, function, pairs) in [
(MathOp::Add, "+", "try_add", [(max, 1), (min, -1)]),
(MathOp::Sub, "-", "try_subtract", [(min, 1), (max, -1)]),
(MathOp::Mul, "*", "try_multiply", [(max, 2), (min, -1)]),
] {
for (l, r) in pairs {
for (left_scalar, right_scalar) in
[(false, false), (true, false), (false, true), (true, true)]
{
let operand = |v: ScalarValue, scalar| {
if scalar {
ColumnarValue::Scalar(v)
} else {
ColumnarValue::Array(v.to_array().unwrap())
}
};
let args = [
operand(value(l), left_scalar),
operand(value(r), right_scalar),
];
let error = overflow_json(
checked_arithmetic_internal(&args, &data_type, op, EvalMode::Ansi)
.unwrap_err(),
);
let expected = if matches!(data_type, DataType::Int8 | DataType::Int16) {
serde_json::json!({
"value1": l.to_string(), "symbol": symbol,
"value2": r.to_string(), "functionName": function
})
} else {
serde_json::json!({
"fromType": integer_type_name(&data_type).unwrap(),
"functionName": function
})
};
let class = if matches!(data_type, DataType::Int8 | DataType::Int16) {
"BINARY_ARITHMETIC_OVERFLOW"
} else {
"ARITHMETIC_OVERFLOW"
};
assert_eq!(error["errorClass"], class, "{data_type:?} {op:?}");
assert_eq!(error["params"], expected, "{data_type:?} {op:?}");
// TRY must still return a typed NULL for the identical inputs.
let result =
checked_arithmetic_internal(&args, &data_type, op, EvalMode::Try)
.unwrap();
assert_eq!(result.into_array(1).unwrap().null_count(), 1);
}
}
}
}
}
#[test]
fn test_ansi_wide_integer_overflow_suggestion() {
// A separate regression so the baseline also demonstrates the missing suggestion.
let args = int32_args(vec![Some(i32::MAX)], vec![Some(1)]);
let error =
overflow_json(checked_add(&args, &DataType::Int32, EvalMode::Ansi).unwrap_err());
assert_eq!(error["params"]["functionName"], "try_add");
}
#[test]
fn test_ansi_small_integer_overflow_skips_nulls_and_respects_slices() {
macro_rules! check {
($ty:ty, $native:ty) => {{
// Ignore both the row before the slice and overflowing garbage beneath NULLs.
let left = PrimitiveArray::<$ty>::new(
vec![<$native>::MAX, <$native>::MAX, 1, <$native>::MAX].into(),
Some(NullBuffer::from(vec![true, false, true, true])),
)
.slice(1, 3);
let right = PrimitiveArray::<$ty>::from_iter_values([1, 1, 2]);
let args = [
ColumnarValue::Array(Arc::new(left.clone())),
ColumnarValue::Array(Arc::new(right)),
];
let error = overflow_json(
checked_add(&args, left.data_type(), EvalMode::Ansi).unwrap_err(),
);
assert_eq!(error["params"]["value1"], <$native>::MAX.to_string());
assert_eq!(error["params"]["value2"], "2");
let args = [
ColumnarValue::Array(Arc::new(left.slice(0, 2))),
ColumnarValue::Array(Arc::new(PrimitiveArray::<$ty>::from_iter_values([1, 1]))),
];
let result = checked_add(&args, left.data_type(), EvalMode::Ansi)
.unwrap()
.into_array(2)
.unwrap();
assert_eq!(
result.as_primitive::<$ty>(),
&PrimitiveArray::<$ty>::from_iter([None, Some(2)])
);
}};
}
check!(Int8Type, i8);
check!(Int16Type, i16);
}
#[test]
fn test_ansi_small_integer_null_and_empty_scalar_broadcasts() {
for data_type in [DataType::Int8, DataType::Int16] {
let null = ScalarValue::try_from(&data_type).unwrap();
let max = if data_type == DataType::Int8 {
ScalarValue::Int8(Some(i8::MAX))
} else {
ScalarValue::Int16(Some(i16::MAX))
};
for op in [MathOp::Add, MathOp::Sub, MathOp::Mul] {
for args in [
[
ColumnarValue::Scalar(null.clone()),
ColumnarValue::Array(max.to_array_of_size(3).unwrap()),
],
[
ColumnarValue::Array(max.to_array_of_size(3).unwrap()),
ColumnarValue::Scalar(null.clone()),
],
[
ColumnarValue::Scalar(max.clone()),
ColumnarValue::Array(max.to_array_of_size(0).unwrap()),
],
[
ColumnarValue::Array(max.to_array_of_size(0).unwrap()),
ColumnarValue::Scalar(max.clone()),
],
[
ColumnarValue::Scalar(null.clone()),
ColumnarValue::Scalar(max.clone()),
],
] {
let result =
checked_arithmetic_internal(&args, &data_type, op, EvalMode::Ansi).unwrap();
match result {
ColumnarValue::Array(array) => assert_eq!(array.len(), array.null_count()),
ColumnarValue::Scalar(value) => assert!(value.is_null()),
}
}
}
}
}
}