blob: cb141880d0c3d71f8945c0761a50a2fc28d8c910 [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::conversion_funcs::utils::cast_overflow;
use crate::conversion_funcs::utils::MICROS_PER_SECOND;
use crate::{EvalMode, SparkError, SparkResult};
use arrow::array::{
Array, ArrayRef, AsArray, BooleanBuilder, Decimal128Array, Decimal128Builder, Float32Array,
Float64Array, GenericStringBuilder, Int16Array, Int32Array, Int64Array, Int8Array,
OffsetSizeTrait, PrimitiveArray, StringBuilder, TimestampMicrosecondBuilder,
};
use arrow::datatypes::{
i256, is_validate_decimal_precision, ArrowPrimitiveType, DataType, Decimal128Type, Float32Type,
Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
};
use num::{cast::AsPrimitive, ToPrimitive, Zero};
use std::sync::Arc;
/// Check if DataFusion cast from integer types is Spark compatible
pub(crate) fn is_df_cast_from_int_spark_compatible(to_type: &DataType) -> bool {
matches!(
to_type,
DataType::Boolean
| DataType::Int8
| DataType::Int16
| DataType::Int32
| DataType::Int64
| DataType::Float32
| DataType::Float64
| DataType::Utf8
)
}
/// Check if DataFusion cast from float types is Spark compatible
pub(crate) fn is_df_cast_from_float_spark_compatible(to_type: &DataType) -> bool {
matches!(
to_type,
DataType::Boolean
| DataType::Int8
| DataType::Int16
| DataType::Int32
| DataType::Int64
| DataType::Float32
| DataType::Float64
)
}
/// Check if DataFusion cast from decimal types is Spark compatible
pub(crate) fn is_df_cast_from_decimal_spark_compatible(to_type: &DataType) -> bool {
matches!(
to_type,
DataType::Int8
| DataType::Int16
| DataType::Int32
| DataType::Int64
| DataType::Float32 // DataFusion divides i128 by 10^scale in f64, then narrows to
| DataType::Float64 // f32 if needed; empirically matches Spark's BigDecimal.doubleValue
// / floatValue for all tested values
| DataType::Decimal128(_, _)
| DataType::Decimal256(_, _)
// DataFusion's Decimal128→Utf8 cast uses plain notation (toPlainString semantics),
// matching Spark's TRY and ANSI modes. LEGACY mode is handled by a separate match
// arm in cast_array that applies Java BigDecimal.toString() (scientific notation
// for values where adjusted_exponent < -6, e.g. "0E-18" for zero with scale=18).
| DataType::Utf8
)
// Note: Boolean is intentionally absent. Decimal-to-boolean uses a dedicated
// spark_cast_decimal_to_boolean function (in cast.rs) that checks the raw i128
// value, bypassing the DataFusion cast kernel entirely.
}
macro_rules! cast_float_to_timestamp_impl {
($array:expr, $builder:expr, $primitive_type:ty, $eval_mode:expr) => {{
let arr = $array.as_primitive::<$primitive_type>();
for i in 0..arr.len() {
if arr.is_null(i) {
$builder.append_null();
} else {
let val = arr.value(i) as f64;
// Path 1: NaN/Infinity check - error says TIMESTAMP
if val.is_nan() || val.is_infinite() {
if $eval_mode == EvalMode::Ansi {
return Err(SparkError::CastInvalidValue {
value: val.to_string(),
from_type: "DOUBLE".to_string(),
to_type: "TIMESTAMP".to_string(),
});
}
$builder.append_null();
} else {
// Path 2: Multiply then check overflow - error says BIGINT
let micros = val * MICROS_PER_SECOND as f64;
if micros.floor() <= i64::MAX as f64 && micros.ceil() >= i64::MIN as f64 {
$builder.append_value(micros as i64);
} else {
if $eval_mode == EvalMode::Ansi {
let value_str = if micros.is_infinite() {
if micros.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
}
} else if micros.is_nan() {
"NaN".to_string()
} else {
format!("{:e}", micros).to_uppercase() + "D"
};
return Err(SparkError::CastOverFlow {
value: value_str,
from_type: "DOUBLE".to_string(),
to_type: "BIGINT".to_string(),
});
}
$builder.append_null();
}
}
}
}
}};
}
macro_rules! cast_float_to_string {
($from:expr, $eval_mode:expr, $type:ty, $output_type:ty, $offset_type:ty, $min_value:expr) => {{
fn cast<OffsetSize>(
from: &dyn Array,
_eval_mode: EvalMode,
) -> SparkResult<ArrayRef>
where
OffsetSize: OffsetSizeTrait, {
use std::fmt::Write;
let array = from.as_any().downcast_ref::<$output_type>().unwrap();
// If the absolute number is less than 10,000,000 and greater or equal than 0.001, the
// result is expressed without scientific notation with at least one digit on either side of
// the decimal point. Otherwise, Spark uses a mantissa followed by E and an
// exponent. The mantissa has an optional leading minus sign followed by one digit to the
// left of the decimal point, and the minimal number of digits greater than zero to the
// right. The exponent has and optional leading minus sign.
// source: https://docs.databricks.com/en/sql/language-manual/functions/cast.html
const LOWER_SCIENTIFIC_BOUND: $type = 0.001;
const UPPER_SCIENTIFIC_BOUND: $type = 10000000.0;
// Values are formatted straight into the builder, so no intermediate String
// is allocated per row. Capacity hint matches arrow-rs's own AVERAGE_STRING_LENGTH
// (16 bytes / value) so typical fractional and scientific outputs like
// "1234.5678" or "-1.4E-45" do not force a mid-loop grow.
let mut builder = GenericStringBuilder::<OffsetSize>::with_capacity(
array.len(),
array.len() * 16,
);
// Reused across rows by the scientific-notation path, which has to inspect
// the formatted text before emitting it.
let mut scratch = String::with_capacity(32);
for value in array.iter() {
let Some(value) = value else {
builder.append_null();
continue;
};
let abs = value.abs();
if (LOWER_SCIENTIFIC_BOUND..UPPER_SCIENTIFIC_BOUND).contains(&abs)
|| abs == 0.0
{
let _ = write!(builder, "{value}");
if value.fract() == 0.0 {
// Spark always renders a fractional digit; Rust omits it.
let _ = builder.write_str(".0");
}
builder.append_value("");
} else if !value.is_finite() {
// NaN and the infinities are excluded by the range check above.
builder.append_value(if value.is_nan() {
"NaN"
} else if value.is_sign_positive() {
"Infinity"
} else {
"-Infinity"
});
} else if abs.to_bits() == 1 {
// Java's Double.toString / Float.toString are not shortest-roundtrip
// and render the smallest subnormals with more digits than Rust does.
builder.append_value(if value.is_sign_negative() {
concat!("-", $min_value)
} else {
$min_value
});
} else {
scratch.clear();
let _ = write!(scratch, "{value:E}");
match scratch.split_once('E') {
Some((coefficient, exponent)) if !coefficient.contains('.') => {
// Spark keeps the fractional digit Rust drops from a whole
// coefficient.
let _ = builder.write_str(coefficient);
let _ = builder.write_str(".0E");
builder.append_value(exponent);
}
_ => builder.append_value(&scratch),
}
}
}
Ok(Arc::new(builder.finish()))
}
cast::<$offset_type>($from, $eval_mode)
}};
}
// eval mode is not needed since all ints can be implemented in binary format
#[macro_export]
macro_rules! cast_whole_num_to_binary {
($array:expr, $primitive_type:ty, $byte_size:expr) => {{
let input_arr = $array
.as_any()
.downcast_ref::<$primitive_type>()
.ok_or_else(|| SparkError::Internal("Expected numeric array".to_string()))?;
let len = input_arr.len();
let mut builder = BinaryBuilder::with_capacity(len, len * $byte_size);
for i in 0..input_arr.len() {
if input_arr.is_null(i) {
builder.append_null();
} else {
builder.append_value(input_arr.value(i).to_be_bytes());
}
}
Ok(Arc::new(builder.finish()) as ArrayRef)
}};
}
macro_rules! cast_int_to_timestamp_impl {
($array:expr, $builder:expr, $primitive_type:ty) => {{
let arr = $array.as_primitive::<$primitive_type>();
for i in 0..arr.len() {
if arr.is_null(i) {
$builder.append_null();
} else {
// saturating_mul limits to i64::MIN/MAX on overflow instead of panicking,
// which could occur when converting extreme values (e.g., Long.MIN_VALUE)
// matching spark behavior (irrespective of EvalMode)
let micros = (arr.value(i) as i64).saturating_mul(MICROS_PER_SECOND);
$builder.append_value(micros);
}
}
}};
}
macro_rules! cast_int_to_int_macro {
(
$array: expr,
$eval_mode:expr,
$from_arrow_primitive_type: ty,
$to_arrow_primitive_type: ty,
$to_native_type: ty,
$spark_from_data_type_name: expr,
$spark_to_data_type_name: expr,
$spark_int_literal_suffix: expr
) => {{
let cast_array = $array
.as_any()
.downcast_ref::<PrimitiveArray<$from_arrow_primitive_type>>()
.unwrap();
let output_array: PrimitiveArray<$to_arrow_primitive_type> = match $eval_mode {
// Legacy narrowing keeps the low-order bits of the source value, which is what a
// wrapping `as` conversion does. Being infallible, it maps the values buffer in a
// single pass and carries the null buffer over untouched.
EvalMode::Legacy => {
cast_array.unary::<_, $to_arrow_primitive_type>(|value| value as $to_native_type)
}
// `try_unary` only applies the conversion to non-null slots, so an out-of-range
// value sitting under a null cannot raise a spurious overflow error.
_ => cast_array.try_unary::<_, $to_arrow_primitive_type, SparkError>(|value| {
<$to_native_type>::try_from(value).map_err(|_| {
cast_overflow(
&(value.to_string() + $spark_int_literal_suffix),
$spark_from_data_type_name,
$spark_to_data_type_name,
)
})
})?,
};
Ok(Arc::new(output_array) as ArrayRef)
}};
}
// When Spark casts to Byte/Short Types, it does not cast directly to Byte/Short.
// It casts to Int first and then to Byte/Short. Because of potential overflows in the Int cast,
// this can cause unexpected Short/Byte cast results. Replicate this behavior.
macro_rules! cast_float_to_int16_down {
(
$array:expr,
$eval_mode:expr,
$src_array_type:ty,
$dest_array_type:ty,
$rust_src_type:ty,
$rust_dest_type:ty,
$src_type_str:expr,
$dest_type_str:expr,
$format_str:expr
) => {{
let cast_array = $array
.as_any()
.downcast_ref::<$src_array_type>()
.expect(concat!("Expected a ", stringify!($src_array_type)));
let output_array = match $eval_mode {
EvalMode::Ansi => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let is_overflow = value.is_nan() || value.abs() as i32 == i32::MAX;
if is_overflow {
return Err(cast_overflow(
&format!($format_str, value).replace("e", "E"),
$src_type_str,
$dest_type_str,
));
}
let i32_value = value as i32;
<$rust_dest_type>::try_from(i32_value)
.map_err(|_| {
cast_overflow(
&format!($format_str, value).replace("e", "E"),
$src_type_str,
$dest_type_str,
)
})
.map(Some)
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
_ => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let i32_value = value as i32;
Ok::<Option<$rust_dest_type>, SparkError>(Some(
i32_value as $rust_dest_type,
))
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
};
Ok(Arc::new(output_array) as ArrayRef)
}};
}
macro_rules! cast_float_to_int32_up {
(
$array:expr,
$eval_mode:expr,
$src_array_type:ty,
$dest_array_type:ty,
$rust_src_type:ty,
$rust_dest_type:ty,
$src_type_str:expr,
$dest_type_str:expr,
$max_dest_val:expr,
$format_str:expr
) => {{
let cast_array = $array
.as_any()
.downcast_ref::<$src_array_type>()
.expect(concat!("Expected a ", stringify!($src_array_type)));
let output_array = match $eval_mode {
EvalMode::Ansi => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let is_overflow =
value.is_nan() || value.abs() as $rust_dest_type == $max_dest_val;
if is_overflow {
return Err(cast_overflow(
&format!($format_str, value).replace("e", "E"),
$src_type_str,
$dest_type_str,
));
}
Ok(Some(value as $rust_dest_type))
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
_ => cast_array
.iter()
.map(|value| match value {
Some(value) => {
Ok::<Option<$rust_dest_type>, SparkError>(Some(value as $rust_dest_type))
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
};
Ok(Arc::new(output_array) as ArrayRef)
}};
}
// When Spark casts to Byte/Short Types, it does not cast directly to Byte/Short.
// It casts to Int first and then to Byte/Short. Because of potential overflows in the Int cast,
// this can cause unexpected Short/Byte cast results. Replicate this behavior.
macro_rules! cast_decimal_to_int16_down {
(
$array:expr,
$eval_mode:expr,
$dest_array_type:ty,
$rust_dest_type:ty,
$dest_type_str:expr,
$precision:expr,
$scale:expr
) => {{
let cast_array = $array
.as_any()
.downcast_ref::<Decimal128Array>()
.expect("Expected a Decimal128ArrayType");
let output_array = match $eval_mode {
EvalMode::Ansi => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let divisor = 10_i128.pow($scale as u32);
let truncated = value / divisor;
let is_overflow = truncated.abs() > i32::MAX.into();
if is_overflow {
return Err(cast_overflow(
&format!(
"{}BD",
format_decimal_str(
&value.to_string(),
$precision as usize,
$scale
)
),
&format!("DECIMAL({},{})", $precision, $scale),
$dest_type_str,
));
}
let i32_value = truncated as i32;
<$rust_dest_type>::try_from(i32_value)
.map_err(|_| {
cast_overflow(
&format!(
"{}BD",
format_decimal_str(
&value.to_string(),
$precision as usize,
$scale
)
),
&format!("DECIMAL({},{})", $precision, $scale),
$dest_type_str,
)
})
.map(Some)
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
_ => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let divisor = 10_i128.pow($scale as u32);
let i32_value = (value / divisor) as i32;
Ok::<Option<$rust_dest_type>, SparkError>(Some(
i32_value as $rust_dest_type,
))
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
};
Ok(Arc::new(output_array) as ArrayRef)
}};
}
macro_rules! cast_decimal_to_int32_up {
(
$array:expr,
$eval_mode:expr,
$dest_array_type:ty,
$rust_dest_type:ty,
$dest_type_str:expr,
$max_dest_val:expr,
$precision:expr,
$scale:expr
) => {{
let cast_array = $array
.as_any()
.downcast_ref::<Decimal128Array>()
.expect("Expected a Decimal128ArrayType");
let output_array = match $eval_mode {
EvalMode::Ansi => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let divisor = 10_i128.pow($scale as u32);
let truncated = value / divisor;
let is_overflow = truncated.abs() > $max_dest_val.into();
if is_overflow {
return Err(cast_overflow(
&format!(
"{}BD",
format_decimal_str(
&value.to_string(),
$precision as usize,
$scale
)
),
&format!("DECIMAL({},{})", $precision, $scale),
$dest_type_str,
));
}
Ok(Some(truncated as $rust_dest_type))
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
_ => cast_array
.iter()
.map(|value| match value {
Some(value) => {
let divisor = 10_i128.pow($scale as u32);
let truncated = value / divisor;
Ok::<Option<$rust_dest_type>, SparkError>(Some(
truncated as $rust_dest_type,
))
}
None => Ok(None),
})
.collect::<Result<$dest_array_type, _>>()?,
};
Ok(Arc::new(output_array) as ArrayRef)
}};
}
// copied from arrow::dataTypes::Decimal128Type since Decimal128Type::format_decimal can't be called directly
pub(crate) fn format_decimal_str(value_str: &str, precision: usize, scale: i8) -> String {
let (sign, rest) = match value_str.strip_prefix('-') {
Some(stripped) => ("-", stripped),
None => ("", value_str),
};
let bound = precision.min(rest.len()) + sign.len();
let value_str = &value_str[0..bound];
if scale == 0 {
value_str.to_string()
} else if scale < 0 {
let padding = value_str.len() + scale.unsigned_abs() as usize;
format!("{value_str:0<padding$}")
} else if rest.len() > scale as usize {
// Decimal separator is in the middle of the string
let (whole, decimal) = value_str.split_at(value_str.len() - scale as usize);
format!("{whole}.{decimal}")
} else {
// String has to be padded
format!("{}0.{:0>width$}", sign, rest, width = scale as usize)
}
}
/// Casts a Decimal128 array to string using Java's BigDecimal.toString() semantics,
/// which is Spark's LEGACY eval mode behavior. Plain notation when scale >= 0 and
/// adjusted_exponent >= -6, otherwise scientific notation (e.g. "0E-18" for zero
/// with scale=18, since adjusted_exponent = -18 + 0 = -18 < -6).
///
/// TRY and ANSI modes produce plain notation via DataFusion's cast instead.
pub(crate) fn cast_decimal128_to_utf8(array: &ArrayRef, scale: i8) -> SparkResult<ArrayRef> {
let decimal_array = array
.as_any()
.downcast_ref::<Decimal128Array>()
.expect("Expected a Decimal128Array");
let mut builder = StringBuilder::with_capacity(decimal_array.len(), decimal_array.len() * 16);
// Reuse the output and digit buffers across rows to keep the loop allocation-free.
let mut buf = String::with_capacity(48);
let mut digits = [0u8; MAX_COEFF_DIGITS];
for opt_val in decimal_array.iter() {
match opt_val {
None => builder.append_null(),
Some(unscaled) => {
buf.clear();
decimal128_to_java_string(unscaled, scale, &mut digits, &mut buf);
builder.append_value(&buf);
}
}
}
Ok(Arc::new(builder.finish()))
}
/// The largest power of ten that fits in a `u64`.
const POW10_19: u128 = 10_000_000_000_000_000_000;
/// Digits in the largest `u128`, which bounds the coefficient of any Decimal128.
const MAX_COEFF_DIGITS: usize = 39;
/// Renders the base-10 digits of `value` into `buf`, returning them without leading zeroes
/// (except for `value == 0`, which renders as `"0"`).
fn render_digits(mut value: u128, buf: &mut [u8; MAX_COEFF_DIGITS]) -> &str {
let mut pos = buf.len();
// Peel off 19 digits at a time so that all but the final group is produced with 64-bit
// arithmetic, which is much cheaper than repeated 128-bit division. Every group taken here
// has a non-zero quotient above it, so emitting its leading zeroes is correct.
while value >= POW10_19 {
let mut group = (value % POW10_19) as u64;
value /= POW10_19;
for _ in 0..19 {
pos -= 1;
buf[pos] = b'0' + (group % 10) as u8;
group /= 10;
}
}
let mut rest = value as u64;
loop {
pos -= 1;
buf[pos] = b'0' + (rest % 10) as u8;
rest /= 10;
if rest == 0 {
break;
}
}
// Only ASCII digits were written above.
std::str::from_utf8(&buf[pos..]).expect("ascii digits")
}
/// Formats a Decimal128 unscaled value into `out` matching Java's BigDecimal.toString():
/// - Plain notation when scale >= 0 and adjusted_exponent >= -6
/// - Scientific notation otherwise
///
/// adjusted_exponent = -scale + (numDigits - 1)
///
/// `digits` is scratch space for the coefficient; the caller reuses one buffer across rows.
fn decimal128_to_java_string(
unscaled: i128,
scale: i8,
digits: &mut [u8; MAX_COEFF_DIGITS],
out: &mut String,
) {
use std::fmt::Write;
let coeff = render_digits(unscaled.unsigned_abs(), digits);
let num_digits = coeff.len();
let adj_exp = -(scale as i64) + (num_digits as i64 - 1);
if unscaled < 0 {
out.push('-');
}
if scale >= 0 && adj_exp >= -6 {
let scale = scale as usize;
if scale == 0 {
out.push_str(coeff);
} else if num_digits > scale {
let (int_part, frac_part) = coeff.split_at(num_digits - scale);
out.push_str(int_part);
out.push('.');
out.push_str(frac_part);
} else {
out.push_str("0.");
for _ in 0..scale - num_digits {
out.push('0');
}
out.push_str(coeff);
}
} else {
if num_digits > 1 {
out.push_str(&coeff[..1]);
out.push('.');
out.push_str(&coeff[1..]);
} else {
out.push_str(coeff);
}
out.push('E');
if adj_exp > 0 {
out.push('+');
}
write!(out, "{adj_exp}").unwrap();
}
}
pub(crate) fn spark_cast_float64_to_utf8<OffsetSize>(
from: &dyn Array,
_eval_mode: EvalMode,
) -> SparkResult<ArrayRef>
where
OffsetSize: OffsetSizeTrait,
{
cast_float_to_string!(from, _eval_mode, f64, Float64Array, OffsetSize, "4.9E-324")
}
pub(crate) fn spark_cast_float32_to_utf8<OffsetSize>(
from: &dyn Array,
_eval_mode: EvalMode,
) -> SparkResult<ArrayRef>
where
OffsetSize: OffsetSizeTrait,
{
cast_float_to_string!(from, _eval_mode, f32, Float32Array, OffsetSize, "1.4E-45")
}
fn cast_int_to_decimal128_internal<T>(
array: &PrimitiveArray<T>,
precision: u8,
scale: i8,
eval_mode: EvalMode,
) -> SparkResult<ArrayRef>
where
T: ArrowPrimitiveType,
T::Native: Into<i128>,
{
let mut builder = Decimal128Builder::with_capacity(array.len());
let multiplier = 10_i128.pow(scale as u32);
for i in 0..array.len() {
if array.is_null(i) {
builder.append_null();
} else {
let v = array.value(i).into();
let scaled = v.checked_mul(multiplier);
match scaled {
Some(scaled) => {
if !is_validate_decimal_precision(scaled, precision) {
match eval_mode {
EvalMode::Ansi => {
return Err(SparkError::NumericValueOutOfRange {
value: v.to_string(),
precision,
scale,
});
}
EvalMode::Try | EvalMode::Legacy => builder.append_null(),
}
} else {
builder.append_value(scaled);
}
}
_ => match eval_mode {
EvalMode::Ansi => {
return Err(SparkError::NumericValueOutOfRange {
value: v.to_string(),
precision,
scale,
})
}
EvalMode::Legacy | EvalMode::Try => builder.append_null(),
},
}
}
}
Ok(Arc::new(
builder.with_precision_and_scale(precision, scale)?.finish(),
))
}
pub(crate) fn cast_int_to_decimal128(
array: &dyn Array,
eval_mode: EvalMode,
from_type: &DataType,
to_type: &DataType,
precision: u8,
scale: i8,
) -> SparkResult<ArrayRef> {
match (from_type, to_type) {
(DataType::Int8, DataType::Decimal128(_p, _s)) => {
cast_int_to_decimal128_internal::<Int8Type>(
array.as_primitive::<Int8Type>(),
precision,
scale,
eval_mode,
)
}
(DataType::Int16, DataType::Decimal128(_p, _s)) => {
cast_int_to_decimal128_internal::<Int16Type>(
array.as_primitive::<Int16Type>(),
precision,
scale,
eval_mode,
)
}
(DataType::Int32, DataType::Decimal128(_p, _s)) => {
cast_int_to_decimal128_internal::<Int32Type>(
array.as_primitive::<Int32Type>(),
precision,
scale,
eval_mode,
)
}
(DataType::Int64, DataType::Decimal128(_p, _s)) => {
cast_int_to_decimal128_internal::<Int64Type>(
array.as_primitive::<Int64Type>(),
precision,
scale,
eval_mode,
)
}
_ => Err(SparkError::Internal(format!(
"Unsupported cast from datatype : {}",
from_type
))),
}
}
pub(crate) fn spark_cast_int_to_int(
array: &dyn Array,
eval_mode: EvalMode,
from_type: &DataType,
to_type: &DataType,
) -> SparkResult<ArrayRef> {
match (from_type, to_type) {
(DataType::Int64, DataType::Int32) => cast_int_to_int_macro!(
array, eval_mode, Int64Type, Int32Type, i32, "BIGINT", "INT", "L"
),
(DataType::Int64, DataType::Int16) => cast_int_to_int_macro!(
array, eval_mode, Int64Type, Int16Type, i16, "BIGINT", "SMALLINT", "L"
),
(DataType::Int64, DataType::Int8) => cast_int_to_int_macro!(
array, eval_mode, Int64Type, Int8Type, i8, "BIGINT", "TINYINT", "L"
),
(DataType::Int32, DataType::Int16) => cast_int_to_int_macro!(
array, eval_mode, Int32Type, Int16Type, i16, "INT", "SMALLINT", ""
),
(DataType::Int32, DataType::Int8) => {
cast_int_to_int_macro!(array, eval_mode, Int32Type, Int8Type, i8, "INT", "TINYINT", "")
}
(DataType::Int16, DataType::Int8) => cast_int_to_int_macro!(
array, eval_mode, Int16Type, Int8Type, i8, "SMALLINT", "TINYINT", "S"
),
_ => unreachable!(
"{}",
format!("invalid integer type {to_type} in cast from {from_type}")
),
}
}
pub(crate) fn spark_cast_decimal_to_boolean(array: &dyn Array) -> SparkResult<ArrayRef> {
let decimal_array = array.as_primitive::<Decimal128Type>();
let mut result = BooleanBuilder::with_capacity(decimal_array.len());
for i in 0..decimal_array.len() {
if decimal_array.is_null(i) {
result.append_null()
} else {
result.append_value(!decimal_array.value(i).is_zero());
}
}
Ok(Arc::new(result.finish()))
}
pub(crate) fn cast_float64_to_decimal128(
array: &dyn Array,
precision: u8,
scale: i8,
eval_mode: EvalMode,
) -> SparkResult<ArrayRef> {
cast_floating_point_to_decimal128::<Float64Type>(array, precision, scale, eval_mode)
}
pub(crate) fn cast_float32_to_decimal128(
array: &dyn Array,
precision: u8,
scale: i8,
eval_mode: EvalMode,
) -> SparkResult<ArrayRef> {
cast_floating_point_to_decimal128::<Float32Type>(array, precision, scale, eval_mode)
}
fn cast_floating_point_to_decimal128<T: ArrowPrimitiveType>(
array: &dyn Array,
precision: u8,
scale: i8,
eval_mode: EvalMode,
) -> SparkResult<ArrayRef>
where
<T as ArrowPrimitiveType>::Native: AsPrimitive<f64>,
{
let input = array.as_any().downcast_ref::<PrimitiveArray<T>>().unwrap();
let mul = 10_f64.powi(scale as i32);
// Single vectorized pass: a value with no in-range integer form (NaN / infinity) or that
// does not fit the output precision maps to null. `unary_opt` only applies the closure to
// non-null slots and carries the input null buffer over, so it replaces the per-element
// builder loop without a second pass.
let result: Decimal128Array = input.unary_opt::<_, Decimal128Type>(|v| {
let f: f64 = v.as_();
(f * mul)
.round()
.to_i128()
.filter(|x| is_validate_decimal_precision(*x, precision))
});
// ANSI must raise on out-of-range values instead of nulling them. `unary_opt` only nulls
// non-null inputs that overflow, so a null count beyond the input's signals an overflow to
// report. This check is O(1); the element-wise rescan runs only on the rare error path and
// reports the first offending value with Spark's exact error.
if eval_mode == EvalMode::Ansi && result.null_count() > input.null_count() {
for i in 0..input.len() {
if !input.is_null(i) {
let input_value: f64 = input.value(i).as_();
let fits = (input_value * mul)
.round()
.to_i128()
.map(|x| is_validate_decimal_precision(x, precision))
.unwrap_or(false);
if !fits {
return Err(SparkError::NumericValueOutOfRange {
value: input_value.to_string(),
precision,
scale,
});
}
}
}
}
Ok(Arc::new(result.with_precision_and_scale(precision, scale)?))
}
pub(crate) fn spark_cast_nonintegral_numeric_to_integral(
array: &dyn Array,
eval_mode: EvalMode,
from_type: &DataType,
to_type: &DataType,
) -> SparkResult<ArrayRef> {
match (from_type, to_type) {
(DataType::Float32, DataType::Int8) => cast_float_to_int16_down!(
array,
eval_mode,
Float32Array,
Int8Array,
f32,
i8,
"FLOAT",
"TINYINT",
"{:e}"
),
(DataType::Float32, DataType::Int16) => cast_float_to_int16_down!(
array,
eval_mode,
Float32Array,
Int16Array,
f32,
i16,
"FLOAT",
"SMALLINT",
"{:e}"
),
(DataType::Float32, DataType::Int32) => cast_float_to_int32_up!(
array,
eval_mode,
Float32Array,
Int32Array,
f32,
i32,
"FLOAT",
"INT",
i32::MAX,
"{:e}"
),
(DataType::Float32, DataType::Int64) => cast_float_to_int32_up!(
array,
eval_mode,
Float32Array,
Int64Array,
f32,
i64,
"FLOAT",
"BIGINT",
i64::MAX,
"{:e}"
),
(DataType::Float64, DataType::Int8) => cast_float_to_int16_down!(
array,
eval_mode,
Float64Array,
Int8Array,
f64,
i8,
"DOUBLE",
"TINYINT",
"{:e}D"
),
(DataType::Float64, DataType::Int16) => cast_float_to_int16_down!(
array,
eval_mode,
Float64Array,
Int16Array,
f64,
i16,
"DOUBLE",
"SMALLINT",
"{:e}D"
),
(DataType::Float64, DataType::Int32) => cast_float_to_int32_up!(
array,
eval_mode,
Float64Array,
Int32Array,
f64,
i32,
"DOUBLE",
"INT",
i32::MAX,
"{:e}D"
),
(DataType::Float64, DataType::Int64) => cast_float_to_int32_up!(
array,
eval_mode,
Float64Array,
Int64Array,
f64,
i64,
"DOUBLE",
"BIGINT",
i64::MAX,
"{:e}D"
),
(DataType::Decimal128(precision, scale), DataType::Int8) => {
cast_decimal_to_int16_down!(
array, eval_mode, Int8Array, i8, "TINYINT", *precision, *scale
)
}
(DataType::Decimal128(precision, scale), DataType::Int16) => {
cast_decimal_to_int16_down!(
array, eval_mode, Int16Array, i16, "SMALLINT", *precision, *scale
)
}
(DataType::Decimal128(precision, scale), DataType::Int32) => {
cast_decimal_to_int32_up!(
array,
eval_mode,
Int32Array,
i32,
"INT",
i32::MAX,
*precision,
*scale
)
}
(DataType::Decimal128(precision, scale), DataType::Int64) => {
cast_decimal_to_int32_up!(
array,
eval_mode,
Int64Array,
i64,
"BIGINT",
i64::MAX,
*precision,
*scale
)
}
_ => unreachable!(
"{}",
format!("invalid cast from non-integral numeric type: {from_type} to integral numeric type: {to_type}")
),
}
}
pub(crate) fn cast_int_to_timestamp(
array_ref: &ArrayRef,
target_tz: &Option<Arc<str>>,
) -> SparkResult<ArrayRef> {
// Input is seconds since epoch, multiply by MICROS_PER_SECOND to get microseconds.
let mut builder = TimestampMicrosecondBuilder::with_capacity(array_ref.len());
match array_ref.data_type() {
DataType::Int8 => cast_int_to_timestamp_impl!(array_ref, builder, Int8Type),
DataType::Int16 => cast_int_to_timestamp_impl!(array_ref, builder, Int16Type),
DataType::Int32 => cast_int_to_timestamp_impl!(array_ref, builder, Int32Type),
DataType::Int64 => cast_int_to_timestamp_impl!(array_ref, builder, Int64Type),
dt => {
return Err(SparkError::Internal(format!(
"Unsupported type for cast_int_to_timestamp: {:?}",
dt
)))
}
}
Ok(Arc::new(builder.finish().with_timezone_opt(target_tz.clone())) as ArrayRef)
}
pub(crate) fn cast_decimal_to_timestamp(
array_ref: &ArrayRef,
target_tz: &Option<Arc<str>>,
scale: i8,
) -> SparkResult<ArrayRef> {
let arr = array_ref.as_primitive::<Decimal128Type>();
let scale_factor = 10_i128.pow(scale as u32);
let mut builder = TimestampMicrosecondBuilder::with_capacity(arr.len());
for i in 0..arr.len() {
if arr.is_null(i) {
builder.append_null();
} else {
let value = arr.value(i);
// Note: spark's big decimal truncates to
// long value and does not throw error (in all leval modes)
let value_256 = i256::from_i128(value);
let micros_256 = value_256 * i256::from_i128(MICROS_PER_SECOND as i128);
let ts = micros_256 / i256::from_i128(scale_factor);
builder.append_value(ts.as_i128() as i64);
}
}
Ok(Arc::new(builder.finish().with_timezone_opt(target_tz.clone())) as ArrayRef)
}
pub(crate) fn cast_float_to_timestamp(
array_ref: &ArrayRef,
target_tz: &Option<Arc<str>>,
eval_mode: EvalMode,
) -> SparkResult<ArrayRef> {
let mut builder = TimestampMicrosecondBuilder::with_capacity(array_ref.len());
match array_ref.data_type() {
DataType::Float32 => {
cast_float_to_timestamp_impl!(array_ref, builder, Float32Type, eval_mode)
}
DataType::Float64 => {
cast_float_to_timestamp_impl!(array_ref, builder, Float64Type, eval_mode)
}
dt => {
return Err(SparkError::Internal(format!(
"Unsupported type for cast_float_to_timestamp: {:?}",
dt
)))
}
}
Ok(Arc::new(builder.finish().with_timezone_opt(target_tz.clone())) as ArrayRef)
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::AsArray;
use arrow::datatypes::TimestampMicrosecondType;
use core::f64;
#[test]
fn test_spark_cast_int_to_int_overflow() {
// Test Int64 -> Int32 overflow
let array: ArrayRef = Arc::new(Int64Array::from(vec![
Some(i64::MAX),
Some(i64::MIN),
Some(100),
]));
// Legacy mode should wrap around
let result =
spark_cast_int_to_int(&array, EvalMode::Legacy, &DataType::Int64, &DataType::Int32)
.unwrap();
let int32_array = result.as_primitive::<Int32Type>();
assert_eq!(int32_array.value(2), 100);
// Ansi mode should error on overflow
let result =
spark_cast_int_to_int(&array, EvalMode::Ansi, &DataType::Int64, &DataType::Int32);
assert!(result.is_err());
}
#[test]
fn test_spark_cast_float_min_value_to_string() {
let float_array: ArrayRef = Arc::new(Float32Array::from(vec![
Some(f32::from_bits(1)),
Some(-f32::from_bits(1)),
]));
let result = spark_cast_float32_to_utf8::<i32>(&float_array, EvalMode::Legacy).unwrap();
let strings = result.as_string::<i32>();
assert_eq!(strings.value(0), "1.4E-45");
assert_eq!(strings.value(1), "-1.4E-45");
let double_array: ArrayRef = Arc::new(Float64Array::from(vec![
Some(f64::from_bits(1)),
Some(-f64::from_bits(1)),
]));
let result = spark_cast_float64_to_utf8::<i32>(&double_array, EvalMode::Legacy).unwrap();
let strings = result.as_string::<i32>();
assert_eq!(strings.value(0), "4.9E-324");
assert_eq!(strings.value(1), "-4.9E-324");
}
#[test]
fn test_spark_cast_decimal_to_boolean() {
let array: ArrayRef = Arc::new(
Decimal128Array::from(vec![Some(0), Some(100), Some(-100), None])
.with_precision_and_scale(10, 2)
.unwrap(),
);
let result = spark_cast_decimal_to_boolean(&array).unwrap();
let bool_array = result.as_boolean();
assert!(!bool_array.value(0)); // 0 -> false
assert!(bool_array.value(1)); // 100 -> true
assert!(bool_array.value(2)); // -100 -> true
assert!(bool_array.is_null(3)); // null -> null
}
#[test]
fn test_cast_int_to_decimal128() {
let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(100), Some(-100), None]));
let result = cast_int_to_decimal128(
&array,
EvalMode::Legacy,
&DataType::Int32,
&DataType::Decimal128(10, 2),
10,
2,
)
.unwrap();
let decimal_array = result.as_primitive::<Decimal128Type>();
assert_eq!(decimal_array.value(0), 10000); // 100 * 10^2
assert_eq!(decimal_array.value(1), -10000); // -100 * 10^2
assert!(decimal_array.is_null(2));
}
#[test]
fn test_cast_int_to_timestamp() {
let timezones: [Option<Arc<str>>; 6] = [
Some(Arc::from("UTC")),
Some(Arc::from("America/New_York")),
Some(Arc::from("America/Los_Angeles")),
Some(Arc::from("Europe/London")),
Some(Arc::from("Asia/Tokyo")),
Some(Arc::from("Australia/Sydney")),
];
for tz in &timezones {
let int8_array: ArrayRef = Arc::new(Int8Array::from(vec![
Some(0),
Some(1),
Some(-1),
Some(127),
Some(-128),
None,
]));
let result = cast_int_to_timestamp(&int8_array, tz).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000);
assert_eq!(ts_array.value(2), -1_000_000);
assert_eq!(ts_array.value(3), 127_000_000);
assert_eq!(ts_array.value(4), -128_000_000);
assert!(ts_array.is_null(5));
assert_eq!(ts_array.timezone(), tz.as_ref().map(|s| s.as_ref()));
let int16_array: ArrayRef = Arc::new(Int16Array::from(vec![
Some(0),
Some(1),
Some(-1),
Some(32767),
Some(-32768),
None,
]));
let result = cast_int_to_timestamp(&int16_array, tz).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000);
assert_eq!(ts_array.value(2), -1_000_000);
assert_eq!(ts_array.value(3), 32_767_000_000_i64);
assert_eq!(ts_array.value(4), -32_768_000_000_i64);
assert!(ts_array.is_null(5));
assert_eq!(ts_array.timezone(), tz.as_ref().map(|s| s.as_ref()));
let int32_array: ArrayRef = Arc::new(Int32Array::from(vec![
Some(0),
Some(1),
Some(-1),
Some(1704067200),
None,
]));
let result = cast_int_to_timestamp(&int32_array, tz).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000);
assert_eq!(ts_array.value(2), -1_000_000);
assert_eq!(ts_array.value(3), 1_704_067_200_000_000_i64);
assert!(ts_array.is_null(4));
assert_eq!(ts_array.timezone(), tz.as_ref().map(|s| s.as_ref()));
let int64_array: ArrayRef = Arc::new(Int64Array::from(vec![
Some(0),
Some(1),
Some(-1),
Some(i64::MAX),
Some(i64::MIN),
]));
let result = cast_int_to_timestamp(&int64_array, tz).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000_i64);
assert_eq!(ts_array.value(2), -1_000_000_i64);
assert_eq!(ts_array.value(3), i64::MAX);
assert_eq!(ts_array.value(4), i64::MIN);
assert_eq!(ts_array.timezone(), tz.as_ref().map(|s| s.as_ref()));
}
}
#[test]
// Currently the cast function depending on `f64::powi`, which has unspecified precision according to the doc
// https://doc.rust-lang.org/std/primitive.f64.html#unspecified-precision.
// Miri deliberately apply random floating-point errors to these operations to expose bugs
// https://github.com/rust-lang/miri/issues/4395.
// The random errors may interfere with test cases at rounding edge, so we ignore it on miri for now.
// Once https://github.com/apache/datafusion-comet/issues/1371 is fixed, this should no longer be an issue.
#[cfg_attr(miri, ignore)]
fn test_cast_float_to_decimal() {
let a: ArrayRef = Arc::new(Float64Array::from(vec![
Some(42.),
Some(0.5153125),
Some(-42.4242415),
Some(42e-314),
Some(0.),
Some(-4242.424242),
Some(f64::INFINITY),
Some(f64::NEG_INFINITY),
Some(f64::NAN),
None,
]));
let b =
cast_floating_point_to_decimal128::<Float64Type>(&a, 8, 6, EvalMode::Legacy).unwrap();
assert_eq!(b.len(), a.len());
let casted = b.as_primitive::<Decimal128Type>();
assert_eq!(casted.value(0), 42000000);
// https://github.com/apache/datafusion-comet/issues/1371
// assert_eq!(casted.value(1), 515313);
assert_eq!(casted.value(2), -42424242);
assert_eq!(casted.value(3), 0);
assert_eq!(casted.value(4), 0);
assert!(casted.is_null(5));
assert!(casted.is_null(6));
assert!(casted.is_null(7));
assert!(casted.is_null(8));
assert!(casted.is_null(9));
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_cast_float_to_decimal_no_overflow_fast_path() {
// All values fit precision 10, scale 2, so the vectorized fast path is taken and the
// input null is preserved.
let a: ArrayRef = Arc::new(Float64Array::from(vec![
Some(42.0),
Some(-1.5),
None,
Some(0.0),
]));
let b =
cast_floating_point_to_decimal128::<Float64Type>(&a, 10, 2, EvalMode::Legacy).unwrap();
let d = b.as_primitive::<Decimal128Type>();
assert_eq!(d.value(0), 4200); // 42.00
assert_eq!(d.value(1), -150); // -1.50
assert!(d.is_null(2));
assert_eq!(d.value(3), 0);
assert_eq!(d.data_type(), &DataType::Decimal128(10, 2));
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_cast_float_to_decimal_ansi_no_overflow() {
let a: ArrayRef = Arc::new(Float64Array::from(vec![Some(42.0), None, Some(-1.5)]));
let b =
cast_floating_point_to_decimal128::<Float64Type>(&a, 10, 2, EvalMode::Ansi).unwrap();
let d = b.as_primitive::<Decimal128Type>();
assert_eq!(d.value(0), 4200);
assert!(d.is_null(1));
assert_eq!(d.value(2), -150);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_cast_float_to_decimal_ansi_overflow_errors() {
// Precision overflow: 4242.42 * 10^2 = 424242 does not fit precision 4. Two overflow
// rows with distinct string forms pin that the rescan reports the *first* offender.
let a: ArrayRef = Arc::new(Float64Array::from(vec![
Some(1.0),
Some(4242.42),
Some(9999.99),
]));
match cast_floating_point_to_decimal128::<Float64Type>(&a, 4, 2, EvalMode::Ansi) {
Err(SparkError::NumericValueOutOfRange {
value,
precision,
scale,
}) => {
assert_eq!(value, "4242.42");
assert_eq!(precision, 4);
assert_eq!(scale, 2);
}
other => panic!("expected NumericValueOutOfRange, got {other:?}"),
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_cast_float_to_decimal_ansi_nan_errors() {
// NaN / infinity have no in-range integer form: `to_i128()` returns `None`, so the
// closure nulls the slot and the rescan must recompute `fits == false` and raise.
// This is a distinct path from finite precision overflow — assert both here to lock
// the reported value string.
let a: ArrayRef = Arc::new(Float64Array::from(vec![Some(1.0), Some(f64::NAN)]));
match cast_floating_point_to_decimal128::<Float64Type>(&a, 10, 2, EvalMode::Ansi) {
Err(SparkError::NumericValueOutOfRange {
value,
precision,
scale,
}) => {
assert_eq!(value, "NaN");
assert_eq!(precision, 10);
assert_eq!(scale, 2);
}
other => panic!("expected NumericValueOutOfRange, got {other:?}"),
}
let a: ArrayRef = Arc::new(Float64Array::from(vec![Some(f64::INFINITY)]));
match cast_floating_point_to_decimal128::<Float64Type>(&a, 10, 2, EvalMode::Ansi) {
Err(SparkError::NumericValueOutOfRange { value, .. }) => {
assert_eq!(value, "inf");
}
other => panic!("expected NumericValueOutOfRange, got {other:?}"),
}
}
#[test]
fn test_cast_decimal_to_timestamp() {
let timezones: [Option<Arc<str>>; 3] = [
Some(Arc::from("UTC")),
Some(Arc::from("America/Los_Angeles")),
None,
];
for tz in &timezones {
// Decimal128 with scale 6
let decimal_array: ArrayRef = Arc::new(
Decimal128Array::from(vec![
Some(0_i128),
Some(1_000_000_i128),
Some(-1_000_000_i128),
Some(1_500_000_i128),
Some(123_456_789_i128),
None,
])
.with_precision_and_scale(18, 6)
.unwrap(),
);
let result = cast_decimal_to_timestamp(&decimal_array, tz, 6).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000);
assert_eq!(ts_array.value(2), -1_000_000);
assert_eq!(ts_array.value(3), 1_500_000);
assert_eq!(ts_array.value(4), 123_456_789);
assert!(ts_array.is_null(5));
assert_eq!(ts_array.timezone(), tz.as_ref().map(|s| s.as_ref()));
// Test with scale 2
let decimal_array: ArrayRef = Arc::new(
Decimal128Array::from(vec![Some(100_i128), Some(150_i128), Some(-250_i128)])
.with_precision_and_scale(10, 2)
.unwrap(),
);
let result = cast_decimal_to_timestamp(&decimal_array, tz, 2).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 1_000_000);
assert_eq!(ts_array.value(1), 1_500_000);
assert_eq!(ts_array.value(2), -2_500_000);
}
}
#[test]
fn test_cast_float_to_timestamp() {
let timezones: [Option<Arc<str>>; 3] = [
Some(Arc::from("UTC")),
Some(Arc::from("America/Los_Angeles")),
None,
];
let eval_modes = [EvalMode::Legacy, EvalMode::Ansi, EvalMode::Try];
for tz in &timezones {
for eval_mode in &eval_modes {
// Float64 tests
let f64_array: ArrayRef = Arc::new(Float64Array::from(vec![
Some(0.0),
Some(1.0),
Some(-1.0),
Some(1.5),
Some(0.000001),
None,
]));
let result = cast_float_to_timestamp(&f64_array, tz, *eval_mode).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000);
assert_eq!(ts_array.value(2), -1_000_000);
assert_eq!(ts_array.value(3), 1_500_000);
assert_eq!(ts_array.value(4), 1);
assert!(ts_array.is_null(5));
assert_eq!(ts_array.timezone(), tz.as_ref().map(|s| s.as_ref()));
// Float32 tests
let f32_array: ArrayRef = Arc::new(Float32Array::from(vec![
Some(0.0_f32),
Some(1.0_f32),
Some(-1.0_f32),
None,
]));
let result = cast_float_to_timestamp(&f32_array, tz, *eval_mode).unwrap();
let ts_array = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(ts_array.value(0), 0);
assert_eq!(ts_array.value(1), 1_000_000);
assert_eq!(ts_array.value(2), -1_000_000);
assert!(ts_array.is_null(3));
}
}
// ANSI mode errors on NaN/Infinity
let tz = &Some(Arc::from("UTC"));
let f64_nan: ArrayRef = Arc::new(Float64Array::from(vec![Some(f64::NAN)]));
assert!(cast_float_to_timestamp(&f64_nan, tz, EvalMode::Ansi).is_err());
let f64_inf: ArrayRef = Arc::new(Float64Array::from(vec![Some(f64::INFINITY)]));
assert!(cast_float_to_timestamp(&f64_inf, tz, EvalMode::Ansi).is_err());
}
#[test]
fn test_decimal128_to_java_string() {
fn fmt(unscaled: i128, scale: i8) -> String {
let mut buf = String::new();
decimal128_to_java_string(unscaled, scale, &mut [0u8; MAX_COEFF_DIGITS], &mut buf);
buf
}
// scale >= 0, adj_exp >= -6 → plain notation
assert_eq!(fmt(0, 0), "0");
assert_eq!(fmt(0, 2), "0.00");
assert_eq!(fmt(12345, 2), "123.45");
assert_eq!(fmt(-12345, 2), "-123.45");
assert_eq!(fmt(1, 2), "0.01");
assert_eq!(fmt(42, 0), "42");
assert_eq!(fmt(-42, 0), "-42");
assert_eq!(fmt(1, 6), "0.000001"); // adj_exp = -6 (boundary)
// scale >= 0, adj_exp < -6 → scientific notation (Spark LEGACY mode)
assert_eq!(fmt(0, 18), "0E-18"); // adj_exp = -18
assert_eq!(fmt(0, 7), "0E-7"); // adj_exp = -7
assert_eq!(fmt(1, 7), "1E-7");
assert_eq!(fmt(1, 18), "1E-18");
// scale < 0 → scientific notation
assert_eq!(fmt(0, -2), "0E+2");
assert_eq!(fmt(1, -2), "1E+2");
assert_eq!(fmt(123, -2), "1.23E+4");
assert_eq!(fmt(-123, -2), "-1.23E+4");
// values needing more than one 64-bit digit group
assert_eq!(fmt(10_000_000_000_000_000_000, 0), "10000000000000000000");
// 10^19 + 5: the lower group is 0000000000000000005 and its leading zeros are load-bearing
// (the render_digits comment "emitting its leading zeroes is correct" applies here).
assert_eq!(fmt(10_000_000_000_000_000_005, 0), "10000000000000000005");
assert_eq!(fmt(i128::MAX, 0), i128::MAX.to_string());
assert_eq!(fmt(i128::MIN, 0), i128::MIN.to_string());
assert_eq!(
fmt(99_999_999_999_999_999_999_999_999_999_999_999_999, 10),
"9999999999999999999999999999.9999999999"
);
// multi-group coefficient in scientific notation, both signs, so the sign push and
// the &coeff[..1] / &coeff[1..] split are covered on a value that spans two groups.
assert_eq!(
fmt(i128::MAX, 45),
"1.70141183460469231731687303715884105727E-7"
);
assert_eq!(
fmt(-i128::MAX, 45),
"-1.70141183460469231731687303715884105727E-7"
);
}
}