| // 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::string::{digits_to_i128, div_round_half_up_i128, pow10_i128}; | |
| 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, BooleanArray, Decimal128Array, Float32Array, Float64Array, | |
| GenericStringBuilder, Int16Array, Int32Array, Int64Array, Int8Array, OffsetSizeTrait, | |
| PrimitiveArray, Scalar, StringBuilder, TimestampMicrosecondBuilder, | |
| }; | |
| use arrow::buffer::BooleanBuffer; | |
| use arrow::compute::kernels::cmp::neq; | |
| use arrow::datatypes::{ | |
| format_decimal_str, i256, is_validate_decimal_precision, ArrowPrimitiveType, DataType, | |
| Decimal128Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type, | |
| }; | |
| use num::{cast::AsPrimitive, Float, ToPrimitive}; | |
| use std::fmt::{self, Write}; | |
| 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 | |
| // Float32 / Float64 are intentionally absent: DataFusion computes | |
| // `(unscaled as f64) / 10^scale`, which rounds twice (three times for f32, via the | |
| // f64 intermediate) and can differ from Spark's correctly rounded | |
| // BigDecimal.doubleValue() / floatValue() by one ulp once |unscaled| > 2^53. | |
| // cast.rs routes Decimal128 sources to cast_decimal128_to_float64 / _float32. | |
| | 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 that compares against a zero decimal of | |
| // the same precision/scale, 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(); | |
| } | |
| } | |
| } | |
| } | |
| }}; | |
| } | |
| /// A float width that Java renders through `Float.toString` / `Double.toString`. | |
| /// | |
| /// `num::Float` supplies the arithmetic predicates; the two widths differ only in the plain-notation | |
| /// window's endpoints and in the literal text of the smallest subnormal, which Java's algorithm | |
| /// spells with more digits than a shortest-round-trip formatter produces. | |
| /// | |
| /// Sealed: the crate root re-exports this module, and `f32` and `f64` are the only widths Java has. | |
| pub trait JavaFloatString: sealed::Sealed + Float + fmt::Display + fmt::UpperExp { | |
| /// `Float.MIN_VALUE` / `Double.MIN_VALUE`: the value one ULP above zero, the one Java does not | |
| /// render shortest. `Float::min_positive_value` is the smallest *normal*, so this has no `num` | |
| /// equivalent. | |
| const MIN_SUBNORMAL: Self; | |
| /// `MIN_SUBNORMAL` as Java spells it. | |
| const MIN_SUBNORMAL_TEXT: &'static str; | |
| /// Plain notation covers `[0.001, 10^7)`; anything outside it is scientific. | |
| const PLAIN_LOWER: Self; | |
| const PLAIN_UPPER: Self; | |
| } | |
| mod sealed { | |
| pub trait Sealed {} | |
| impl Sealed for f32 {} | |
| impl Sealed for f64 {} | |
| } | |
| impl JavaFloatString for f32 { | |
| const MIN_SUBNORMAL: Self = f32::from_bits(1); | |
| const MIN_SUBNORMAL_TEXT: &'static str = "1.4E-45"; | |
| const PLAIN_LOWER: Self = 0.001; | |
| const PLAIN_UPPER: Self = 10000000.0; | |
| } | |
| impl JavaFloatString for f64 { | |
| const MIN_SUBNORMAL: Self = f64::from_bits(1); | |
| const MIN_SUBNORMAL_TEXT: &'static str = "4.9E-324"; | |
| const PLAIN_LOWER: Self = 0.001; | |
| const PLAIN_UPPER: Self = 10000000.0; | |
| } | |
| /// Writes `value` as Java's `Float.toString` / `Double.toString` renders it. | |
| /// | |
| /// If the absolute value 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 the value is a mantissa followed by `E` and an exponent, the mantissa having | |
| /// 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. | |
| /// Source: <https://docs.databricks.com/en/sql/language-manual/functions/cast.html> | |
| /// | |
| /// Rust's own `Display` and `UpperExp` give the same digits but drop a whole coefficient's | |
| /// fractional zero (`1` for `1.0`) and never switch to an exponent, so `Double.MAX_VALUE` would | |
| /// render as 309 digits. Both matter beyond cosmetics: Spark spells a `cast(double as string)` | |
| /// this way, and iceberg-java spells a float or double partition directory this way, where the | |
| /// unabbreviated form overruns the filesystem's limit on one path component. | |
| /// | |
| /// Rust's digits are shortest-round-trip, which is what JDK 19+ `Double.toString` produces. Earlier | |
| /// JDKs sometimes emit a longer string (JDK-4511638: `2.0E23` came out as `1.9999999999999998E23`), | |
| /// and those values are not corrected for here. The smallest subnormal, which every JDK spells as | |
| /// `4.9E-324` rather than the shortest `5E-324`, is. | |
| /// | |
| /// Errors only if `out` does. Writing into a `String` or an arrow string builder cannot fail, which | |
| /// is what lets the callers of this function discard the result. | |
| pub fn write_java_float_string<T: JavaFloatString, W: fmt::Write>( | |
| value: T, | |
| out: &mut W, | |
| ) -> fmt::Result { | |
| let abs = value.abs(); | |
| if (T::PLAIN_LOWER..T::PLAIN_UPPER).contains(&abs) || abs.is_zero() { | |
| write!(out, "{value}")?; | |
| if value.fract().is_zero() { | |
| // Java always renders a fractional digit; Rust omits it. | |
| out.write_str(".0")?; | |
| } | |
| Ok(()) | |
| } else if !value.is_finite() { | |
| // NaN and the infinities are excluded by the range check above. | |
| out.write_str(if value.is_nan() { | |
| "NaN" | |
| } else if value.is_sign_negative() { | |
| "-Infinity" | |
| } else { | |
| "Infinity" | |
| }) | |
| } else if abs == T::MIN_SUBNORMAL { | |
| if value.is_sign_negative() { | |
| out.write_str("-")?; | |
| } | |
| out.write_str(T::MIN_SUBNORMAL_TEXT) | |
| } else { | |
| // The coefficient has to be inspected before any of it is emitted, so it is formatted | |
| // into a stack buffer rather than into `out`, which may not be rewindable. | |
| let mut scratch = ExponentBuf::default(); | |
| write!(scratch, "{value:E}")?; | |
| let text = scratch.as_str(); | |
| match text.split_once('E') { | |
| Some((coefficient, exponent)) if !coefficient.contains('.') => { | |
| // Java keeps the fractional digit Rust drops from a whole coefficient. | |
| out.write_str(coefficient)?; | |
| out.write_str(".0E")?; | |
| out.write_str(exponent) | |
| } | |
| _ => out.write_str(text), | |
| } | |
| } | |
| } | |
| /// `Float.toString` / `Double.toString` of `value` as an owned `String`, for callers that need one | |
| /// rendering rather than a column of them. | |
| pub fn java_float_string<T: JavaFloatString>(value: T) -> String { | |
| let mut out = String::new(); | |
| // Cannot fail; see `write_java_float_string`. | |
| let _ = write_java_float_string(value, &mut out); | |
| out | |
| } | |
| /// Scratch space for one `{:E}` rendering. `{:E}` emits at most 17 significant digits for an | |
| /// `f64`, so the longest output is sign + digit + point + 16 digits + `E` + sign + 3 exponent | |
| /// digits, 24 bytes. | |
| #[derive(Default)] | |
| struct ExponentBuf { | |
| bytes: [u8; 32], | |
| len: usize, | |
| } | |
| impl ExponentBuf { | |
| fn as_str(&self) -> &str { | |
| // Only `{:E}` output, which is ASCII, is ever written. | |
| std::str::from_utf8(&self.bytes[..self.len]).expect("ascii float text") | |
| } | |
| } | |
| impl fmt::Write for ExponentBuf { | |
| fn write_str(&mut self, s: &str) -> fmt::Result { | |
| let end = self.len + s.len(); | |
| // Unreachable for `{:E}` of an f32 or f64 (see the size bound above); returning an error | |
| // rather than panicking keeps a future caller's mistake out of the JNI boundary. | |
| let target = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?; | |
| target.copy_from_slice(s.as_bytes()); | |
| self.len = end; | |
| Ok(()) | |
| } | |
| } | |
| /// Casts a float array to strings the way Spark's `cast(float as string)` does, which is Java's | |
| /// `Float.toString` / `Double.toString`. | |
| fn spark_cast_float_to_utf8<T, OffsetSize>(from: &dyn Array) -> SparkResult<ArrayRef> | |
| where | |
| T: ArrowPrimitiveType, | |
| T::Native: JavaFloatString, | |
| OffsetSize: OffsetSizeTrait, | |
| { | |
| let array = from.as_primitive::<T>(); | |
| // 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); | |
| for value in array.iter() { | |
| match value { | |
| None => builder.append_null(), | |
| Some(value) => { | |
| // Cannot fail; see `write_java_float_string`. | |
| let _ = write_java_float_string(value, &mut builder); | |
| builder.append_value(""); | |
| } | |
| } | |
| } | |
| Ok(Arc::new(builder.finish())) | |
| } | |
| // 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) | |
| }}; | |
| } | |
| /// Spark's ANSI range check for casting a floating point value to a 32-bit or 64-bit integer, | |
| /// from `FloatExactNumeric`/`DoubleExactNumeric.toInt`/`toLong` in Spark's `numerics.scala`: | |
| /// `Math.floor(x) <= MaxValue && Math.ceil(x) >= MinValue`. The JVM widens a float source to | |
| /// double for `Math.floor`/`Math.ceil` and converts the integer bounds to double for the | |
| /// comparison, so the check is evaluated in `f64` here as well. A value that passes it is then | |
| /// truncated towards zero with saturation by `as`, exactly like the JVM's `d2i`/`d2l`, so the | |
| /// exactly representable bounds are valid inputs; note that `i64::MAX as f64 == 2^63`, so `±2^63` | |
| /// passes the check and saturates to `i64::MAX`/`i64::MIN`, as it does in Spark. NaN fails both | |
| /// comparisons and the infinities fail one of them. | |
| fn spark_float_fits_integral<F: Into<f64>>(value: F, min: f64, max: f64) -> bool { | |
| let value: f64 = value.into(); | |
| value.floor() <= max && value.ceil() >= min | |
| } | |
| // 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) => { | |
| // Spark's ANSI cast converts to Int with the exact 32-bit range check | |
| // first and then requires the truncated Int to round-trip through the | |
| // narrower type. | |
| if !spark_float_fits_integral(value, i32::MIN as f64, i32::MAX as f64) { | |
| 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, | |
| $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) => { | |
| if !spark_float_fits_integral( | |
| value, | |
| <$rust_dest_type>::MIN as f64, | |
| <$rust_dest_type>::MAX as f64, | |
| ) { | |
| return Err(cast_overflow( | |
| &format!($format_str, value).replace("e", "E"), | |
| $src_type_str, | |
| $dest_type_str, | |
| )); | |
| } | |
| // `as` truncates towards zero and saturates like the JVM's d2i/d2l | |
| 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) | |
| }}; | |
| } | |
| /// 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, | |
| { | |
| spark_cast_float_to_utf8::<Float64Type, OffsetSize>(from) | |
| } | |
| pub(crate) fn spark_cast_float32_to_utf8<OffsetSize>( | |
| from: &dyn Array, | |
| _eval_mode: EvalMode, | |
| ) -> SparkResult<ArrayRef> | |
| where | |
| OffsetSize: OffsetSizeTrait, | |
| { | |
| spark_cast_float_to_utf8::<Float32Type, OffsetSize>(from) | |
| } | |
| 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 multiplier = 10_i128.pow(scale as u32); | |
| // Single spelling of the "does this value fit at the target precision after scaling" check, | |
| // shared between the vectorized pass and the ANSI rescan below. | |
| let fits = |v: i128| -> Option<i128> { | |
| v.checked_mul(multiplier) | |
| .filter(|scaled| is_validate_decimal_precision(*scaled, precision)) | |
| }; | |
| // Single vectorized pass: a value that overflows the multiply or 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, replacing the per-element builder loop without a second pass. | |
| let result: Decimal128Array = array.unary_opt::<_, Decimal128Type>(|v| fits(v.into())); | |
| // 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() > array.null_count() { | |
| for i in 0..array.len() { | |
| if !array.is_null(i) { | |
| let v: i128 = array.value(i).into(); | |
| if fits(v).is_none() { | |
| return Err(SparkError::NumericValueOutOfRange { | |
| value: v.to_string(), | |
| precision, | |
| scale, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| Ok(Arc::new(result.with_precision_and_scale(precision, scale)?)) | |
| } | |
| 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>(); | |
| // All-null fast path: skips the zero-scalar construction for a batch whose output | |
| // is trivially all-null. Also covers all-null `Decimal128(0, 0)`, which the default | |
| // path cannot handle (see the precision-zero fast path below). | |
| if decimal_array.null_count() == decimal_array.len() { | |
| return Ok(Arc::new(BooleanArray::new_null(decimal_array.len()))); | |
| } | |
| // Precision-zero fast path. Arrow rejects `precision == 0` in `with_precision_and_scale`, | |
| // so the default `neq`-against-zero path cannot round-trip a `Decimal128(0, 0)` batch | |
| // even when it has valid slots. Spark reaches this shape via JVM-side writers that do | |
| // not validate precision (e.g. a UDF returning `BigInteger.ZERO` written through | |
| // `DecimalVector.setSafe(long)`). The type contract says only 0 is representable, but | |
| // we still cast the raw i128 payload (`v != 0`) rather than hard-coding `false`, so an | |
| // out-of-contract non-zero value in a valid slot still round-trips correctly. | |
| if decimal_array.precision() == 0 { | |
| let values: BooleanBuffer = decimal_array.values().iter().map(|&v| v != 0).collect(); | |
| return Ok(Arc::new(BooleanArray::new( | |
| values, | |
| decimal_array.nulls().cloned(), | |
| ))); | |
| } | |
| // Arrow has no Decimal-to-Boolean cast. `neq` against a zero of the same | |
| // precision/scale is exactly `!value.is_zero()`, including null handling. | |
| let zero = Scalar::new( | |
| Decimal128Array::from(vec![0i128]) | |
| .with_precision_and_scale(decimal_array.precision(), decimal_array.scale())?, | |
| ); | |
| Ok(Arc::new(neq(decimal_array, &zero)?)) | |
| } | |
| /// Powers of ten that are exactly representable as `f64` (`10^n = 2^n * 5^n` and `5^22 < 2^53`); | |
| /// the same table as Java's `BigDecimal.DOUBLE_10_POW`. | |
| const F64_EXACT_POW10: [f64; 23] = [ | |
| 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, | |
| 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, | |
| ]; | |
| /// Powers of ten that are exactly representable as `f32` (`5^10 < 2^24`); the same table as | |
| /// Java's `BigDecimal.FLOAT_10_POW`. | |
| const F32_EXACT_POW10: [f32; 11] = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10]; | |
| /// Largest integer magnitude that converts to `f64` without rounding. | |
| const F64_EXACT_INT_LIMIT: u128 = 1 << 53; | |
| /// Largest integer magnitude that converts to `f32` without rounding. | |
| const F32_EXACT_INT_LIMIT: u128 = 1 << 24; | |
| /// Converts a Decimal128 value to the `f64` nearest to its exact decimal value (ties to even), | |
| /// matching Spark's `Decimal.toDouble`, i.e. `java.math.BigDecimal.doubleValue()`. | |
| /// | |
| /// DataFusion's kernel computes `(unscaled as f64) / 10^scale`, which rounds the unscaled value | |
| /// first and the quotient second; once `|unscaled| > 2^53` (any `DECIMAL(38,18)` value of | |
| /// magnitude `>= 0.01`) the two roundings can land one ulp away from the correctly rounded | |
| /// result, e.g. `12345.6789` becomes `12345.678899999999`. | |
| pub(crate) fn decimal128_to_f64(unscaled: i128, scale: i8) -> f64 { | |
| if scale == 0 { | |
| return unscaled as f64; | |
| } | |
| // Fast path (also Java's): when the unscaled value and the power of ten are both exact | |
| // doubles, a single IEEE division or multiplication is correctly rounded. | |
| if unscaled.unsigned_abs() <= F64_EXACT_INT_LIMIT { | |
| let m = unscaled as f64; | |
| match scale { | |
| 1..=22 => return m / F64_EXACT_POW10[scale as usize], | |
| -22..=-1 => return m * F64_EXACT_POW10[scale.unsigned_abs() as usize], | |
| _ => {} | |
| } | |
| } | |
| parse_exact_decimal(unscaled, scale) | |
| } | |
| /// Converts a Decimal128 value to the `f32` nearest to its exact decimal value (ties to even), | |
| /// matching Spark's `Decimal.toFloat`, i.e. `java.math.BigDecimal.floatValue()`. | |
| /// | |
| /// The exact value is rounded straight to `f32`, never through an `f64` intermediate, because | |
| /// rounding twice is observable: `16777217.0000000001` is `16777218` as a float, but narrowing | |
| /// the nearest `f64` (`16777217.0`, a tie) gives `16777216`. | |
| pub(crate) fn decimal128_to_f32(unscaled: i128, scale: i8) -> f32 { | |
| if scale == 0 { | |
| return unscaled as f32; | |
| } | |
| if unscaled.unsigned_abs() <= F32_EXACT_INT_LIMIT { | |
| let m = unscaled as f32; | |
| match scale { | |
| 1..=10 => return m / F32_EXACT_POW10[scale as usize], | |
| -10..=-1 => return m * F32_EXACT_POW10[scale.unsigned_abs() as usize], | |
| _ => {} | |
| } | |
| } | |
| parse_exact_decimal(unscaled, scale) | |
| } | |
| /// Rounds the exact value `unscaled * 10^-scale` once, by formatting it as `<unscaled>e<-scale>` | |
| /// into a stack buffer and parsing it with Rust's correctly rounded float parser. This mirrors | |
| /// Java's fallback of `Double.parseDouble(BigDecimal.toString())` / | |
| /// `Float.parseFloat(BigDecimal.toString())`, which likewise round the exact decimal directly to | |
| /// the target width; a value beyond the target's range parses to an infinity, as in Java. | |
| fn parse_exact_decimal<F>(unscaled: i128, scale: i8) -> F | |
| where | |
| F: std::str::FromStr, | |
| F::Err: std::fmt::Debug, | |
| { | |
| use std::io::Write; | |
| // Sign and up to 39 digits, 'e', and an exponent of up to 4 characters (`-127`). | |
| let mut buf = [0u8; 48]; | |
| let mut cursor = &mut buf[..]; | |
| write!(cursor, "{unscaled}e{}", -i32::from(scale)).expect("buffer holds any i128 and i8"); | |
| let remaining = cursor.len(); | |
| let text = std::str::from_utf8(&buf[..buf.len() - remaining]).expect("ascii"); | |
| text.parse::<F>().expect("well-formed float literal") | |
| } | |
| /// Casts a Decimal128 array to Float64 with `BigDecimal.doubleValue()` semantics; see | |
| /// [`decimal128_to_f64`]. The conversion is total and cannot overflow, so every eval mode | |
| /// behaves alike and the input null buffer carries over unchanged. | |
| pub(crate) fn cast_decimal128_to_float64(array: &dyn Array, scale: i8) -> SparkResult<ArrayRef> { | |
| let input = array.as_primitive::<Decimal128Type>(); | |
| let result: Float64Array = input.unary(|v| decimal128_to_f64(v, scale)); | |
| Ok(Arc::new(result)) | |
| } | |
| /// Casts a Decimal128 array to Float32 with `BigDecimal.floatValue()` semantics; see | |
| /// [`decimal128_to_f32`]. Every Decimal128 magnitude (`< 2^127`) is below `f32::MAX`, so for | |
| /// the non-negative scales Spark produces the conversion cannot overflow and every eval mode | |
| /// behaves alike. | |
| pub(crate) fn cast_decimal128_to_float32(array: &dyn Array, scale: i8) -> SparkResult<ArrayRef> { | |
| let input = array.as_primitive::<Decimal128Type>(); | |
| let result: Float32Array = input.unary(|v| decimal128_to_f32(v, scale)); | |
| Ok(Arc::new(result)) | |
| } | |
| 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); | |
| // Perf-only early out: both paths below already map overflow to null, but this skips | |
| // the exact-path string formatting for values (including ±infinity) whose scaled | |
| // magnitude clearly exceeds the precision limit no matter what their exact decimal | |
| // digits are. The slack generously covers the same scaling error that TIE_GUARD below | |
| // bounds, so borderline values still take the exact path. | |
| let overflow_bound = 10_f64.powi(precision as i32) * (1.0 + 1e-12); | |
| // A value with no decimal 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. Floats are widened to double first because Spark | |
| // casts through `x.toDouble` before converting to a string. | |
| let result: Decimal128Array = input.unary_opt::<_, Decimal128Type>(|v| { | |
| let f: f64 = v.as_(); | |
| let x = f * mul; | |
| if x.abs() > overflow_bound { | |
| // also catches ±infinity inputs, which Spark nulls | |
| return None; | |
| } | |
| // Rounding the binary product agrees with Spark's string-based result whenever | |
| // every value within the combined error of the shortest-string representation, | |
| // the multiplication, and `powi` (a few ulp, at most |x| * 2^-50) rounds to the | |
| // same integer: |x - round(x)| + |x| * TIE_GUARD < 0.5, with TIE_GUARD at 4x the | |
| // error bound. For |x| > 2^47 the guard exceeds 0.5, so large values, whose | |
| // integer neighborhoods are wider than 1, always take the exact path; for | |
| // high-scale targets (e.g. Decimal(38,18)) that makes the exact path the common | |
| // path, not a rare fallback. NaN fails the comparison and takes the exact path, | |
| // which nulls it. | |
| const TIE_GUARD: f64 = 3.552713678800501e-15; // 2^-48 | |
| let r = x.round(); | |
| if (x - r).abs() + x.abs() * TIE_GUARD < 0.5 { | |
| return r | |
| .to_i128() | |
| .filter(|u| is_validate_decimal_precision(*u, precision)); | |
| } | |
| float_to_decimal128(f, precision, scale) | |
| }); | |
| // ANSI must raise on out-of-range values instead of nulling them, but NaN / infinity | |
| // stay null even in ANSI mode: Spark catches the NumberFormatException thrown by | |
| // `Decimal(double)` and returns null before the ANSI overflow check runs. `unary_opt` | |
| // only nulls non-null inputs, so a null count beyond the input's signals a potential | |
| // overflow; the element-wise rescan runs only on that rare path and reports the first | |
| // finite 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) && result.is_null(i) { | |
| let input_value: f64 = input.value(i).as_(); | |
| if input_value.is_finite() { | |
| return Err(SparkError::NumericValueOutOfRange { | |
| value: input_value.to_string(), | |
| precision, | |
| scale, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| Ok(Arc::new(result.with_precision_and_scale(precision, scale)?)) | |
| } | |
| /// Convert a double to a decimal unscaled value with Spark semantics. | |
| /// | |
| /// Spark converts through `BigDecimal(Double.toString(d)).setScale(scale, HALF_UP)`: it | |
| /// rounds the shortest decimal string form of the value, not its exact binary expansion. | |
| /// The two disagree for values like 0.5153125 whose binary value (0.51531249999...) sits | |
| /// just below the rounding tie that the string form lands on, so a plain | |
| /// `(f * 10^scale).round()` produces results that differ from Spark. | |
| /// | |
| /// Returns `None` for NaN / infinity and for results that do not fit `precision`. | |
| fn float_to_decimal128(f: f64, precision: u8, scale: i8) -> Option<i128> { | |
| if !f.is_finite() { | |
| return None; | |
| } | |
| // Shortest round-trip decimal form, same digits as Java's Double.toString | |
| let mut buf = ryu::Buffer::new(); | |
| let (mantissa, exp10) = parse_decimal_notation(buf.format_finite(f))?; | |
| // value = mantissa * 10^exp10, so unscaled = round(mantissa * 10^(exp10 + scale)) | |
| let shift = exp10 + scale as i32; | |
| let unscaled = if shift >= 0 { | |
| // Overflowing i128 here means the result cannot fit any decimal precision | |
| mantissa.checked_mul(pow10_i128(shift as u32)?)? | |
| } else { | |
| match pow10_i128(-shift as u32) { | |
| // The mantissa has at most 17 significant digits, so dividing by a power of | |
| // ten too large for i128 always rounds to zero | |
| None => 0, | |
| // Divide with HALF_UP rounding (away from zero on a tie, matching BigDecimal) | |
| Some(div) => div_round_half_up_i128(mantissa, div), | |
| } | |
| }; | |
| is_validate_decimal_precision(unscaled, precision).then_some(unscaled) | |
| } | |
| /// Parse ryu's `[-]digits[.digits][e[-]digits]` output into an integer mantissa and a | |
| /// base-10 exponent such that the value equals `mantissa * 10^exp10`. | |
| /// | |
| /// Returns `None` if the mantissa does not fit an `i128`, which cannot happen for ryu | |
| /// output: a shortest-form double carries at most 17 significant digits. | |
| fn parse_decimal_notation(s: &str) -> Option<(i128, i32)> { | |
| let (digits, exp10) = match s.split_once('e') { | |
| Some((digits, exp)) => (digits, exp.parse::<i32>().expect("exponent from ryu")), | |
| None => (s, 0), | |
| }; | |
| let (digits, negative) = match digits.strip_prefix('-') { | |
| Some(unsigned) => (unsigned, true), | |
| None => (digits, false), | |
| }; | |
| let (integral, fractional) = match digits.split_once('.') { | |
| Some((integral, fractional)) => (integral, fractional), | |
| None => (digits, ""), | |
| }; | |
| // value = (integral * 10^frac_digits + fractional) * 10^(exp10 - frac_digits) | |
| let frac_digits = fractional.len() as i32; | |
| let mantissa = pow10_i128(frac_digits as u32) | |
| .and_then(|p| digits_to_i128(integral.as_bytes())?.checked_mul(p)) | |
| .and_then(|v| v.checked_add(digits_to_i128(fractional.as_bytes())?))?; | |
| let mantissa = if negative { -mantissa } else { mantissa }; | |
| Some((mantissa, exp10 - frac_digits)) | |
| } | |
| 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", | |
| "{:e}" | |
| ), | |
| (DataType::Float32, DataType::Int64) => cast_float_to_int32_up!( | |
| array, | |
| eval_mode, | |
| Float32Array, | |
| Int64Array, | |
| f32, | |
| i64, | |
| "FLOAT", | |
| "BIGINT", | |
| "{: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", | |
| "{:e}D" | |
| ), | |
| (DataType::Float64, DataType::Int64) => cast_float_to_int32_up!( | |
| array, | |
| eval_mode, | |
| Float64Array, | |
| Int64Array, | |
| f64, | |
| i64, | |
| "DOUBLE", | |
| "BIGINT", | |
| "{: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; | |
| #[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()); | |
| } | |
| /// Casts `values` from the floating point type `S` to the integral type `D` with | |
| /// `spark_cast_nonintegral_numeric_to_integral` and returns the resulting values. | |
| fn cast_float_to_integral<S: ArrowPrimitiveType, D: ArrowPrimitiveType>( | |
| values: Vec<S::Native>, | |
| eval_mode: EvalMode, | |
| ) -> SparkResult<Vec<D::Native>> { | |
| let array = PrimitiveArray::<S>::from_iter_values(values); | |
| let result = spark_cast_nonintegral_numeric_to_integral( | |
| &array, | |
| eval_mode, | |
| &S::DATA_TYPE, | |
| &D::DATA_TYPE, | |
| )?; | |
| Ok(result.as_primitive::<D>().values().to_vec()) | |
| } | |
| /// Asserts that each value overflows on its own in ANSI mode with Spark's `CAST_OVERFLOW` | |
| /// error naming the source and target types. | |
| fn assert_ansi_cast_overflow<S: ArrowPrimitiveType, D: ArrowPrimitiveType>( | |
| values: &[S::Native], | |
| from_type: &str, | |
| to_type: &str, | |
| ) { | |
| for value in values { | |
| match cast_float_to_integral::<S, D>(vec![*value], EvalMode::Ansi) { | |
| Err(SparkError::CastOverFlow { | |
| from_type: from, | |
| to_type: to, | |
| .. | |
| }) => assert_eq!( | |
| (from.as_str(), to.as_str()), | |
| (from_type, to_type), | |
| "unexpected types in the overflow error for {value:?}" | |
| ), | |
| other => panic!("expected CAST_OVERFLOW for {value:?}, got {other:?}"), | |
| } | |
| } | |
| } | |
| #[test] | |
| fn test_cast_double_to_int_ansi_boundaries() { | |
| // Spark accepts x when floor(x) <= i32::MAX && ceil(x) >= i32::MIN and truncates it | |
| // towards zero, so the exactly representable bounds and the fractional values inside | |
| // them are valid inputs | |
| assert_eq!( | |
| cast_float_to_integral::<Float64Type, Int32Type>( | |
| vec![ | |
| 2147483647.0, | |
| -2147483648.0, | |
| 2147483647.5, | |
| -2147483648.5, | |
| 2147483648.0f64.next_down(), | |
| (-2147483649.0f64).next_up(), | |
| ], | |
| EvalMode::Ansi, | |
| ) | |
| .unwrap(), | |
| vec![i32::MAX, i32::MIN, i32::MAX, i32::MIN, i32::MAX, i32::MIN] | |
| ); | |
| assert_ansi_cast_overflow::<Float64Type, Int32Type>( | |
| &[ | |
| 2147483648.0, | |
| -2147483649.0, | |
| 2147483648.5, | |
| -2147483649.5, | |
| 1e10, | |
| f64::MAX, | |
| f64::MIN, | |
| f64::NAN, | |
| f64::INFINITY, | |
| f64::NEG_INFINITY, | |
| ], | |
| "DOUBLE", | |
| "INT", | |
| ); | |
| // the offending value is rendered like Spark's Double.toString plus the D suffix | |
| match cast_float_to_integral::<Float64Type, Int32Type>(vec![2147483648.0], EvalMode::Ansi) { | |
| Err(SparkError::CastOverFlow { value, .. }) => assert_eq!(value, "2.147483648E9D"), | |
| other => panic!("expected CAST_OVERFLOW, got {other:?}"), | |
| } | |
| } | |
| #[test] | |
| fn test_cast_double_to_long_ansi_boundaries() { | |
| // i64::MAX converted to f64 rounds up to 2^63, which Spark therefore accepts and | |
| // saturates to i64::MAX, exactly like the JVM's d2l | |
| let two_pow_63 = 9223372036854775808.0f64; | |
| assert_eq!(i64::MAX as f64, two_pow_63); | |
| assert_eq!( | |
| cast_float_to_integral::<Float64Type, Int64Type>( | |
| vec![ | |
| two_pow_63, | |
| -two_pow_63, | |
| two_pow_63.next_down(), | |
| (-two_pow_63).next_up(), | |
| ], | |
| EvalMode::Ansi, | |
| ) | |
| .unwrap(), | |
| vec![ | |
| i64::MAX, | |
| i64::MIN, | |
| 9223372036854774784, | |
| -9223372036854774784 | |
| ] | |
| ); | |
| assert_ansi_cast_overflow::<Float64Type, Int64Type>( | |
| &[ | |
| two_pow_63.next_up(), | |
| (-two_pow_63).next_down(), | |
| 1e19, | |
| -1e19, | |
| f64::MAX, | |
| f64::NAN, | |
| f64::INFINITY, | |
| f64::NEG_INFINITY, | |
| ], | |
| "DOUBLE", | |
| "BIGINT", | |
| ); | |
| } | |
| #[test] | |
| fn test_cast_float_to_int_ansi_boundaries() { | |
| // The bounds are compared in double precision, as in Spark: i32::MAX is not | |
| // representable as f32 and rounds up to 2^31, which must overflow, while the largest | |
| // f32 below 2^31 (2147483520) and -2^31 itself are accepted | |
| let two_pow_31 = 2147483648.0f32; | |
| assert_eq!(i32::MAX as f32, two_pow_31); | |
| assert_eq!( | |
| cast_float_to_integral::<Float32Type, Int32Type>( | |
| vec![two_pow_31.next_down(), -two_pow_31, (-two_pow_31).next_up()], | |
| EvalMode::Ansi, | |
| ) | |
| .unwrap(), | |
| vec![2147483520, i32::MIN, -2147483520] | |
| ); | |
| assert_ansi_cast_overflow::<Float32Type, Int32Type>( | |
| &[ | |
| two_pow_31, | |
| (-two_pow_31).next_down(), | |
| 1e10, | |
| f32::MAX, | |
| f32::MIN, | |
| f32::NAN, | |
| f32::INFINITY, | |
| f32::NEG_INFINITY, | |
| ], | |
| "FLOAT", | |
| "INT", | |
| ); | |
| } | |
| #[test] | |
| fn test_cast_float_to_long_ansi_boundaries() { | |
| let two_pow_63 = 9223372036854775808.0f32; | |
| assert_eq!(i64::MAX as f32, two_pow_63); | |
| assert_eq!( | |
| cast_float_to_integral::<Float32Type, Int64Type>( | |
| vec![ | |
| two_pow_63, | |
| -two_pow_63, | |
| two_pow_63.next_down(), | |
| (-two_pow_63).next_up(), | |
| ], | |
| EvalMode::Ansi, | |
| ) | |
| .unwrap(), | |
| vec![ | |
| i64::MAX, | |
| i64::MIN, | |
| 9223371487098961920, | |
| -9223371487098961920 | |
| ] | |
| ); | |
| assert_ansi_cast_overflow::<Float32Type, Int64Type>( | |
| &[ | |
| two_pow_63.next_up(), | |
| (-two_pow_63).next_down(), | |
| 1e19, | |
| f32::MAX, | |
| f32::NAN, | |
| f32::INFINITY, | |
| f32::NEG_INFINITY, | |
| ], | |
| "FLOAT", | |
| "BIGINT", | |
| ); | |
| } | |
| #[test] | |
| fn test_cast_float_to_short_and_byte_ansi_boundaries() { | |
| // Spark converts to INT first and then requires the truncated INT to round-trip through | |
| // the narrower type, so 127.9 is a valid TINYINT while 128.0 is not, and a value that | |
| // passes the INT range check can still overflow the narrower type | |
| assert_eq!( | |
| cast_float_to_integral::<Float64Type, Int8Type>( | |
| vec![127.0, -128.0, 127.9, -128.9], | |
| EvalMode::Ansi | |
| ) | |
| .unwrap(), | |
| vec![i8::MAX, i8::MIN, i8::MAX, i8::MIN] | |
| ); | |
| assert_ansi_cast_overflow::<Float64Type, Int8Type>( | |
| &[ | |
| 128.0, | |
| -129.0, | |
| 2147483647.5, | |
| 2147483648.0, | |
| f64::NAN, | |
| f64::INFINITY, | |
| f64::NEG_INFINITY, | |
| ], | |
| "DOUBLE", | |
| "TINYINT", | |
| ); | |
| assert_eq!( | |
| cast_float_to_integral::<Float64Type, Int16Type>( | |
| vec![32767.0, -32768.0, 32767.9, -32768.9], | |
| EvalMode::Ansi | |
| ) | |
| .unwrap(), | |
| vec![i16::MAX, i16::MIN, i16::MAX, i16::MIN] | |
| ); | |
| assert_ansi_cast_overflow::<Float64Type, Int16Type>( | |
| &[ | |
| 32768.0, | |
| -32769.0, | |
| 2147483647.5, | |
| 2147483648.0, | |
| f64::NAN, | |
| f64::INFINITY, | |
| f64::NEG_INFINITY, | |
| ], | |
| "DOUBLE", | |
| "SMALLINT", | |
| ); | |
| assert_eq!( | |
| cast_float_to_integral::<Float32Type, Int8Type>( | |
| vec![127.0, -128.0, 127.9, -128.9], | |
| EvalMode::Ansi | |
| ) | |
| .unwrap(), | |
| vec![i8::MAX, i8::MIN, i8::MAX, i8::MIN] | |
| ); | |
| assert_ansi_cast_overflow::<Float32Type, Int8Type>( | |
| &[ | |
| 128.0, | |
| -129.0, | |
| 2147483520.0, | |
| 2147483648.0, | |
| f32::NAN, | |
| f32::INFINITY, | |
| f32::NEG_INFINITY, | |
| ], | |
| "FLOAT", | |
| "TINYINT", | |
| ); | |
| assert_eq!( | |
| cast_float_to_integral::<Float32Type, Int16Type>( | |
| vec![32767.0, -32768.0, 32767.9, -32768.9], | |
| EvalMode::Ansi | |
| ) | |
| .unwrap(), | |
| vec![i16::MAX, i16::MIN, i16::MAX, i16::MIN] | |
| ); | |
| assert_ansi_cast_overflow::<Float32Type, Int16Type>( | |
| &[ | |
| 32768.0, | |
| -32769.0, | |
| 2147483520.0, | |
| 2147483648.0, | |
| f32::NAN, | |
| f32::INFINITY, | |
| f32::NEG_INFINITY, | |
| ], | |
| "FLOAT", | |
| "SMALLINT", | |
| ); | |
| } | |
| #[test] | |
| fn test_cast_float_to_integral_legacy_saturates() { | |
| // LEGACY mode keeps truncating towards zero with saturation and mapping NaN to 0 | |
| assert_eq!( | |
| cast_float_to_integral::<Float64Type, Int32Type>( | |
| vec![ | |
| 2147483648.0, | |
| -2147483649.0, | |
| 1e19, | |
| -1e19, | |
| f64::NAN, | |
| f64::INFINITY, | |
| f64::NEG_INFINITY, | |
| ], | |
| EvalMode::Legacy, | |
| ) | |
| .unwrap(), | |
| vec![ | |
| i32::MAX, | |
| i32::MIN, | |
| i32::MAX, | |
| i32::MIN, | |
| 0, | |
| i32::MAX, | |
| i32::MIN | |
| ] | |
| ); | |
| assert_eq!( | |
| cast_float_to_integral::<Float32Type, Int64Type>( | |
| vec![f32::MAX, f32::MIN, f32::NAN], | |
| EvalMode::Legacy | |
| ) | |
| .unwrap(), | |
| vec![i64::MAX, i64::MIN, 0] | |
| ); | |
| // narrowing casts go through INT first, so the INT result wraps like Spark's toShort | |
| assert_eq!( | |
| cast_float_to_integral::<Float64Type, Int16Type>( | |
| vec![32768.0, 2147483648.0], | |
| EvalMode::Legacy | |
| ) | |
| .unwrap(), | |
| vec![i16::MIN, -1] | |
| ); | |
| } | |
| #[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 | |
| // A different precision/scale must still compare against a matching zero scalar. | |
| let array: ArrayRef = Arc::new( | |
| Decimal128Array::from(vec![Some(0), Some(1)]) | |
| .with_precision_and_scale(38, 0) | |
| .unwrap(), | |
| ); | |
| let result = spark_cast_decimal_to_boolean(&array).unwrap(); | |
| let bool_array = result.as_boolean(); | |
| assert!(!bool_array.value(0)); | |
| assert!(bool_array.value(1)); | |
| // All-null Decimal128(0, 0) is reachable via Spark's RDD row-to-Arrow path; | |
| // `with_precision_and_scale(0, 0)` rejects precision 0, so the all-null fast path | |
| // must yield an all-null boolean array without constructing the zero scalar. | |
| // SAFETY: builds a well-formed Decimal128 ArrayData: | |
| // - len = 2 slots | |
| // - values buffer = 32 bytes = 2 * 16 (Decimal128 slot width), zero-initialized | |
| // - null buffer = 1 byte, all bits clear, covering >= len bits as required | |
| // Precision 0 skips ArrowError validation but is otherwise a legal DataType tag; | |
| // no non-null slot is ever read, so precision-range invariants are vacuous. | |
| let array_data = unsafe { | |
| arrow::array::ArrayData::builder(DataType::Decimal128(0, 0)) | |
| .len(2) | |
| .null_bit_buffer(Some(arrow::buffer::Buffer::from(&[0u8]))) | |
| .add_buffer(arrow::buffer::Buffer::from(&[0u8; 32])) | |
| .build_unchecked() | |
| }; | |
| let array: ArrayRef = Arc::new(Decimal128Array::from(array_data)); | |
| assert_eq!(array.data_type(), &DataType::Decimal128(0, 0)); | |
| let result = spark_cast_decimal_to_boolean(&array).unwrap(); | |
| let bool_array = result.as_boolean(); | |
| assert_eq!(bool_array.len(), 2); | |
| assert!(bool_array.is_null(0)); | |
| assert!(bool_array.is_null(1)); | |
| // Empty Decimal128(0, 0) input: `null_count() == len()` (0 == 0) must still take | |
| // the fast path. Precision 0 makes this discriminating — without the fast path, | |
| // building the zero scalar would fail regardless of the empty length. | |
| // SAFETY: len = 0, so both the (empty) values buffer and the absent null buffer | |
| // trivially cover every slot; the precision-range invariant is vacuous. | |
| let array_data = unsafe { | |
| arrow::array::ArrayData::builder(DataType::Decimal128(0, 0)) | |
| .len(0) | |
| .add_buffer(arrow::buffer::Buffer::from(&[] as &[u8])) | |
| .build_unchecked() | |
| }; | |
| let array: ArrayRef = Arc::new(Decimal128Array::from(array_data)); | |
| // The load-bearing check is that `spark_cast_decimal_to_boolean` returns Ok — without | |
| // the fast path, precision 0 would fail during zero-scalar construction. Length is | |
| // asserted for completeness; null_count is trivially 0 on any empty array. | |
| let result = spark_cast_decimal_to_boolean(&array).unwrap(); | |
| assert_eq!(result.as_boolean().len(), 0); | |
| // Mixed valid + null Decimal128(0, 0). Reachable via JVM-side writers that do not | |
| // validate precision (e.g. a Java UDF returning `BigInteger.ZERO` declared as | |
| // `DecimalType(0, 0)`, written through `DecimalVector.setSafe(long)`). The all-null | |
| // fast path does not cover this: `null_count() < len()` yet the batch still cannot | |
| // round-trip through a zero scalar. We build a 3-row batch where slot 0 is a valid | |
| // zero, slot 1 is null, and slot 2 is an out-of-contract non-zero i128; the path | |
| // must read the raw value (not hard-code `false`) so slot 2 comes back as `true`. | |
| // Expected output: `[false, null, true]`. | |
| // SAFETY: len = 3; values buffer = 48 bytes = 3 * 16, with i128(0), i128(0), | |
| // i128(7) laid out little-endian; null bit buffer 0b0000_0101 = slots 0 and 2 | |
| // valid, slot 1 null. | |
| let mut vals = [0u8; 48]; | |
| vals[32..48].copy_from_slice(&7i128.to_le_bytes()); // slot 2 = 7 | |
| let array_data = unsafe { | |
| arrow::array::ArrayData::builder(DataType::Decimal128(0, 0)) | |
| .len(3) | |
| .null_bit_buffer(Some(arrow::buffer::Buffer::from(&[0b0000_0101u8]))) | |
| .add_buffer(arrow::buffer::Buffer::from(&vals)) | |
| .build_unchecked() | |
| }; | |
| let array: ArrayRef = Arc::new(Decimal128Array::from(array_data)); | |
| assert_eq!(array.data_type(), &DataType::Decimal128(0, 0)); | |
| assert_eq!(array.null_count(), 1); | |
| let result = spark_cast_decimal_to_boolean(&array).unwrap(); | |
| let bool_array = result.as_boolean(); | |
| assert_eq!(bool_array.len(), 3); | |
| assert!(!bool_array.is_null(0)); | |
| assert!(!bool_array.value(0)); // valid zero -> false | |
| assert!(bool_array.is_null(1)); // null -> null | |
| assert!(!bool_array.is_null(2)); | |
| assert!(bool_array.value(2)); // valid non-zero i128 -> true (raw-value cast) | |
| } | |
| #[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_decimal128_overflow_legacy_nulls() { | |
| // 1000 * 10^2 = 100000 does not fit precision 3 -> null (legacy). Valid values and the | |
| // input null are preserved, exercising the vectorized null-on-overflow path. | |
| let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(9), Some(1000), None, Some(-9)])); | |
| let result = cast_int_to_decimal128( | |
| &array, | |
| EvalMode::Legacy, | |
| &DataType::Int32, | |
| &DataType::Decimal128(3, 2), | |
| 3, | |
| 2, | |
| ) | |
| .unwrap(); | |
| let d = result.as_primitive::<Decimal128Type>(); | |
| assert_eq!(d.value(0), 900); // 9.00 | |
| assert!(d.is_null(1)); // overflow -> null | |
| assert!(d.is_null(2)); // input null preserved | |
| assert_eq!(d.value(3), -900); | |
| assert_eq!(d.data_type(), &DataType::Decimal128(3, 2)); | |
| } | |
| #[test] | |
| fn test_cast_int_to_decimal128_overflow_try_nulls() { | |
| // Try shares the Legacy null-on-overflow branch but is a distinct enum arm; assert it | |
| // explicitly so a future refactor cannot regress it silently. | |
| let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(9), Some(1000), None, Some(-9)])); | |
| let result = cast_int_to_decimal128( | |
| &array, | |
| EvalMode::Try, | |
| &DataType::Int32, | |
| &DataType::Decimal128(3, 2), | |
| 3, | |
| 2, | |
| ) | |
| .unwrap(); | |
| let d = result.as_primitive::<Decimal128Type>(); | |
| assert_eq!(d.value(0), 900); | |
| assert!(d.is_null(1)); | |
| assert!(d.is_null(2)); | |
| assert_eq!(d.value(3), -900); | |
| } | |
| #[test] | |
| fn test_cast_int_to_decimal128_no_overflow_ansi() { | |
| let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(9), None, Some(-9)])); | |
| let result = cast_int_to_decimal128( | |
| &array, | |
| EvalMode::Ansi, | |
| &DataType::Int32, | |
| &DataType::Decimal128(3, 2), | |
| 3, | |
| 2, | |
| ) | |
| .unwrap(); | |
| let d = result.as_primitive::<Decimal128Type>(); | |
| assert_eq!(d.value(0), 900); | |
| assert!(d.is_null(1)); | |
| assert_eq!(d.value(2), -900); | |
| } | |
| #[test] | |
| fn test_cast_int_to_decimal128_overflow_ansi_errors() { | |
| // Two overflowing values: the rescan is documented to report the first one, so asserting | |
| // on 1000 rather than 2000 pins the scan order as well as the error variant and payload. | |
| let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(9), Some(1000), Some(2000)])); | |
| let result = cast_int_to_decimal128( | |
| &array, | |
| EvalMode::Ansi, | |
| &DataType::Int32, | |
| &DataType::Decimal128(3, 2), | |
| 3, | |
| 2, | |
| ); | |
| let err = result.unwrap_err(); | |
| assert!( | |
| matches!( | |
| err, | |
| SparkError::NumericValueOutOfRange { | |
| ref value, | |
| precision: 3, | |
| scale: 2 | |
| } if value == "1000" | |
| ), | |
| "unexpected error: {err:?}" | |
| ); | |
| } | |
| #[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] | |
| fn test_decimal128_to_f64_matches_bigdecimal_double_value() { | |
| // Expected values were checked against Java's | |
| // `new BigDecimal(BigInteger(unscaled), scale).doubleValue()`. | |
| let cases: Vec<(i128, i8, f64)> = vec![ | |
| // https://github.com/apache/datafusion-comet/issues/5670 | |
| (12345678900000000000000, 18, 12345.6789), | |
| (123456789012000000000000, 18, 123456.789012), | |
| (76543210000000000000000, 18, 76543.21), | |
| (577312285583388355022308, 18, 577312.2855833884), | |
| (-12345678900000000000000, 18, -12345.6789), | |
| (-577312285583388355022308, 18, -577312.2855833884), | |
| // zero and the smallest magnitudes | |
| (0, 18, 0.0), | |
| (0, 38, 0.0), | |
| (1, 38, 1e-38), | |
| (-1, 38, -1e-38), | |
| // 38-digit extremes | |
| (99999999999999999999999999999999999999, 0, 1e38), | |
| (-99999999999999999999999999999999999999, 0, -1e38), | |
| (99999999999999999999999999999999999999, 38, 1.0), | |
| (-99999999999999999999999999999999999999, 38, -1.0), | |
| (i128::MAX, 0, 2f64.powi(127)), | |
| (i128::MIN, 0, -(2f64.powi(127))), | |
| // 2^53 + 1 is halfway between two doubles and rounds to even | |
| (9007199254740993, 0, 9007199254740992.0), | |
| // exact-operand fast path | |
| (1, 18, 1e-18), | |
| (-15, 1, -1.5), | |
| (123456789, 3, 123456.789), | |
| (9007199254740992, 22, 9007199254740992e-22), | |
| // negative scales, which arrow permits but Spark never produces | |
| (123456789, -3, 123456789e3), | |
| (9007199254740992, -22, 9007199254740992e22), | |
| (9007199254740993, -22, 9007199254740993e22), | |
| ]; | |
| for (unscaled, scale, expected) in cases { | |
| let actual = decimal128_to_f64(unscaled, scale); | |
| assert_eq!( | |
| actual.to_bits(), | |
| expected.to_bits(), | |
| "{unscaled}e{}: got {actual:?}, expected {expected:?}", | |
| -i32::from(scale) | |
| ); | |
| } | |
| // The double-rounding formula the kernel replaces gets the issue's example wrong. | |
| assert_ne!((12345678900000000000000_i128 as f64) / 1e18, 12345.6789); | |
| } | |
| #[test] | |
| fn test_decimal128_to_f32_matches_bigdecimal_float_value() { | |
| // Expected values were checked against Java's | |
| // `new BigDecimal(BigInteger(unscaled), scale).floatValue()`. | |
| let cases: Vec<(i128, i8, f32)> = vec![ | |
| // https://github.com/apache/datafusion-comet/issues/5670: 16777217.0000000001 lies | |
| // above the midpoint of the floats 16777216 and 16777218, but its nearest double | |
| // (16777217.0) is exactly that midpoint and would narrow to 16777216. | |
| (167772170000000001, 10, 16777218.0), | |
| (-167772170000000001, 10, -16777218.0), | |
| (12345678900000000000000, 18, 12345.679), | |
| (76543210000000000000000, 18, 76543.21), | |
| (-12345678900000000000000, 18, -12345.679), | |
| // zero and the smallest magnitudes (1e-38 is subnormal in f32) | |
| (0, 18, 0.0), | |
| (0, 38, 0.0), | |
| (1, 38, 1e-38), | |
| (-1, 38, -1e-38), | |
| // 38-digit extremes | |
| (99999999999999999999999999999999999999, 0, 1e38), | |
| (-99999999999999999999999999999999999999, 0, -1e38), | |
| (99999999999999999999999999999999999999, 38, 1.0), | |
| (-99999999999999999999999999999999999999, 38, -1.0), | |
| (i128::MAX, 0, 2f32.powi(127)), | |
| (i128::MIN, 0, -(2f32.powi(127))), | |
| // 2^24 + 1 is halfway between two floats and rounds to even | |
| (16777217, 0, 16777216.0), | |
| // exact-operand fast path | |
| (1, 10, 1e-10), | |
| (-15, 1, -1.5), | |
| (123456789, 3, 123456.79), | |
| (16777216, 10, 16777216e-10), | |
| // negative scales, which arrow permits but Spark never produces; beyond the f32 | |
| // range the result is an infinity, like Float.parseFloat | |
| (123456789, -3, 1.2345679e11), | |
| (16777216, -10, 16777216e10), | |
| (1, -39, f32::INFINITY), | |
| (-1, -39, f32::NEG_INFINITY), | |
| ]; | |
| for (unscaled, scale, expected) in cases { | |
| let actual = decimal128_to_f32(unscaled, scale); | |
| assert_eq!( | |
| actual.to_bits(), | |
| expected.to_bits(), | |
| "{unscaled}e{}: got {actual:?}, expected {expected:?}", | |
| -i32::from(scale) | |
| ); | |
| } | |
| // Narrowing the (already correctly rounded) double gets the issue's example wrong. | |
| assert_ne!( | |
| decimal128_to_f64(167772170000000001, 10) as f32, | |
| 16777218.0_f32 | |
| ); | |
| } | |
| #[test] | |
| fn test_decimal128_to_float_fast_path_matches_exact_path() { | |
| use rand::rngs::StdRng; | |
| use rand::{RngExt, SeedableRng}; | |
| let mut rng = StdRng::seed_from_u64(5670); | |
| let mut cases: Vec<(i128, i8)> = Vec::new(); | |
| // Magnitudes spread over the whole i128 range, so both the exact-operand fast paths | |
| // (|unscaled| <= 2^24 for f32, <= 2^53 for f64) and the parse fallback are exercised. | |
| for _ in 0..10_000 { | |
| let bits = rng.random_range(0_u32..=127); | |
| let magnitude = if bits == 0 { | |
| 0 | |
| } else { | |
| rng.random::<u128>() >> (128 - bits) | |
| }; | |
| let unscaled = if rng.random::<bool>() { | |
| magnitude as i128 | |
| } else { | |
| -(magnitude as i128) | |
| }; | |
| cases.push((unscaled, rng.random_range(-30_i8..=38))); | |
| } | |
| // Values around the fast-path limits, where a wrong bound would show up. | |
| for _ in 0..10_000 { | |
| let limit = if rng.random::<bool>() { | |
| 1_i128 << 53 | |
| } else { | |
| 1 << 24 | |
| }; | |
| let unscaled = limit + rng.random_range(-1000_i128..=1000); | |
| let unscaled = if rng.random::<bool>() { | |
| unscaled | |
| } else { | |
| -unscaled | |
| }; | |
| cases.push((unscaled, rng.random_range(-23_i8..=23))); | |
| } | |
| // Decimal-looking values with few digits, all on the fast paths. | |
| for _ in 0..10_000 { | |
| let unscaled = rng.random_range(-10_000_000_i128..=10_000_000); | |
| cases.push((unscaled, rng.random_range(0_i8..=10))); | |
| } | |
| for (unscaled, scale) in cases { | |
| // Rust's float parser rounds the exact decimal value once, like BigDecimal does. | |
| let text = format!("{unscaled}e{}", -i32::from(scale)); | |
| let expected_f64: f64 = text.parse().unwrap(); | |
| let expected_f32: f32 = text.parse().unwrap(); | |
| let actual_f64 = decimal128_to_f64(unscaled, scale); | |
| let actual_f32 = decimal128_to_f32(unscaled, scale); | |
| assert_eq!( | |
| actual_f64.to_bits(), | |
| expected_f64.to_bits(), | |
| "{text}: got {actual_f64:?}, expected {expected_f64:?}" | |
| ); | |
| assert_eq!( | |
| actual_f32.to_bits(), | |
| expected_f32.to_bits(), | |
| "{text}: got {actual_f32:?}, expected {expected_f32:?}" | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn test_cast_decimal128_to_float_arrays_keep_nulls_in_every_eval_mode() { | |
| use crate::conversion_funcs::cast::cast_array; | |
| use crate::SparkCastOptions; | |
| let decimals_38_18: ArrayRef = Arc::new( | |
| Decimal128Array::from(vec![ | |
| Some(12345678900000000000000_i128), | |
| None, | |
| Some(-76543210000000000000000), | |
| Some(0), | |
| Some(577312285583388355022308), | |
| None, | |
| ]) | |
| .with_precision_and_scale(38, 18) | |
| .unwrap(), | |
| ); | |
| let decimals_38_10: ArrayRef = Arc::new( | |
| Decimal128Array::from(vec![Some(167772170000000001_i128), None, Some(-123456789)]) | |
| .with_precision_and_scale(38, 10) | |
| .unwrap(), | |
| ); | |
| for eval_mode in [EvalMode::Legacy, EvalMode::Ansi, EvalMode::Try] { | |
| let options = SparkCastOptions::new(eval_mode, "UTC", false); | |
| let doubles = | |
| cast_array(Arc::clone(&decimals_38_18), &DataType::Float64, &options).unwrap(); | |
| let doubles = doubles.as_primitive::<Float64Type>(); | |
| assert_eq!(doubles.len(), 6); | |
| assert_eq!(doubles.null_count(), 2); | |
| assert_eq!(doubles.value(0), 12345.6789); | |
| assert!(doubles.is_null(1)); | |
| assert_eq!(doubles.value(2), -76543.21); | |
| assert_eq!(doubles.value(3).to_bits(), 0.0_f64.to_bits()); | |
| assert_eq!(doubles.value(4), 577312.2855833884); | |
| assert!(doubles.is_null(5)); | |
| let floats = | |
| cast_array(Arc::clone(&decimals_38_18), &DataType::Float32, &options).unwrap(); | |
| let floats = floats.as_primitive::<Float32Type>(); | |
| assert_eq!(floats.len(), 6); | |
| assert_eq!(floats.null_count(), 2); | |
| assert_eq!(floats.value(0), 12345.679_f32); | |
| assert!(floats.is_null(1)); | |
| assert_eq!(floats.value(2), -76543.21_f32); | |
| assert_eq!(floats.value(3).to_bits(), 0.0_f32.to_bits()); | |
| assert_eq!(floats.value(4), 577312.3_f32); | |
| assert!(floats.is_null(5)); | |
| let floats = | |
| cast_array(Arc::clone(&decimals_38_10), &DataType::Float32, &options).unwrap(); | |
| let floats = floats.as_primitive::<Float32Type>(); | |
| assert_eq!(floats.value(0), 16777218.0_f32); | |
| assert!(floats.is_null(1)); | |
| assert_eq!(floats.value(2), -0.012345679_f32); | |
| let doubles = | |
| cast_array(Arc::clone(&decimals_38_10), &DataType::Float64, &options).unwrap(); | |
| let doubles = doubles.as_primitive::<Float64Type>(); | |
| // 16777217.0000000001 rounds to the double 16777217.0 | |
| assert_eq!(doubles.value(0), 16777217.0); | |
| assert!(doubles.is_null(1)); | |
| assert_eq!(doubles.value(2), -0.0123456789); | |
| } | |
| } | |
| #[test] | |
| 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); | |
| 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] | |
| 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] | |
| 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] | |
| 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:?}"), | |
| } | |
| } | |
| /// Cast an f64 array to Decimal128(precision, scale), panicking on error | |
| fn cast_f64_to_decimal128( | |
| values: Vec<Option<f64>>, | |
| precision: u8, | |
| scale: i8, | |
| eval_mode: EvalMode, | |
| ) -> Decimal128Array { | |
| let a: ArrayRef = Arc::new(Float64Array::from(values)); | |
| cast_floating_point_to_decimal128::<Float64Type>(&a, precision, scale, eval_mode) | |
| .unwrap() | |
| .as_primitive::<Decimal128Type>() | |
| .clone() | |
| } | |
| #[test] | |
| fn test_cast_float_to_decimal_ansi_nan_and_infinity_are_null() { | |
| // Spark converts through Decimal(double) which throws NumberFormatException for | |
| // NaN/infinity; the cast catches it and produces null before the ANSI overflow | |
| // check runs, so these are null even in ANSI mode. | |
| let d = cast_f64_to_decimal128( | |
| vec![ | |
| Some(1.0), | |
| Some(f64::NAN), | |
| Some(f64::INFINITY), | |
| Some(f64::NEG_INFINITY), | |
| ], | |
| 10, | |
| 2, | |
| EvalMode::Ansi, | |
| ); | |
| assert_eq!(d.value(0), 100); | |
| assert!(d.is_null(1)); | |
| assert!(d.is_null(2)); | |
| assert!(d.is_null(3)); | |
| } | |
| #[test] | |
| fn test_parse_decimal_notation_round_trips_ryu_output() { | |
| // Every shape ryu can emit: plain integral ("1.0"), plain fractional ("12.34"), | |
| // leading-zero fractional ("0.001234", up to 21 fractional digits), single-digit | |
| // exponent form ("1e30") and full exponent form ("1.234e33"). The extremes pin | |
| // down that the mantissa always fits an i128, so the `None` arm stays unreachable. | |
| let mut buf = ryu::Buffer::new(); | |
| for v in [ | |
| 0.0, | |
| 1.0, | |
| -1.0, | |
| 12.34, | |
| -0.001234, | |
| 0.5153125, | |
| 1e16, | |
| 1e17, | |
| 1.5e10, | |
| 1e30, | |
| 1.2345678901234567e-5, | |
| 1.2345678901234567e300, | |
| f64::MAX, | |
| f64::MIN, | |
| f64::MIN_POSITIVE, | |
| f64::from_bits(1), // smallest subnormal | |
| ] { | |
| let formatted = buf.format_finite(v); | |
| let (mantissa, exp10) = | |
| parse_decimal_notation(formatted).expect("ryu mantissa fits i128"); | |
| let reconstructed: f64 = format!("{mantissa}e{exp10}").parse().unwrap(); | |
| assert_eq!(reconstructed, v, "{formatted} -> {mantissa}e{exp10}"); | |
| } | |
| } | |
| #[test] | |
| fn test_cast_float_to_decimal_spark_rounding_parity() { | |
| // Spark rounds the shortest decimal string form of the double (Double.toString -> | |
| // BigDecimal.setScale(HALF_UP)), not the binary value. These values sit exactly on | |
| // a rounding tie in string form while the binary value is just below it, so binary | |
| // rounding gives a different (wrong) result. | |
| let d = cast_f64_to_decimal128( | |
| vec![Some(0.5153125), Some(-0.5153125), Some(0.5203125)], | |
| 8, | |
| 6, | |
| EvalMode::Legacy, | |
| ); | |
| assert_eq!(d.value(0), 515313); | |
| assert_eq!(d.value(1), -515313); | |
| assert_eq!(d.value(2), 520313); | |
| // HALF_UP rounds away from zero for negative values too | |
| let d = cast_f64_to_decimal128(vec![Some(1.005), Some(-1.005)], 10, 2, EvalMode::Legacy); | |
| assert_eq!(d.value(0), 101); | |
| assert_eq!(d.value(1), -101); | |
| // values whose shortest form uses exponent notation | |
| let d = cast_f64_to_decimal128(vec![Some(1.5e10)], 20, 2, EvalMode::Legacy); | |
| assert_eq!(d.value(0), 1_500_000_000_000); | |
| let d = cast_f64_to_decimal128(vec![Some(1e30)], 10, 2, EvalMode::Legacy); | |
| assert!(d.is_null(0)); | |
| } | |
| #[test] | |
| fn test_cast_float32_to_decimal_widens_to_double_first() { | |
| // Spark widens float to double before converting to string, so 1.005f32 becomes | |
| // 1.0049999952316284 and rounds down; using the shortest f32 form ("1.005") would | |
| // incorrectly round up. | |
| let a: ArrayRef = Arc::new(Float32Array::from(vec![Some(1.005f32)])); | |
| let b = | |
| cast_floating_point_to_decimal128::<Float32Type>(&a, 10, 2, EvalMode::Legacy).unwrap(); | |
| let d = b.as_primitive::<Decimal128Type>(); | |
| assert_eq!(d.value(0), 100); | |
| // 0.5153125f32 widens to 0.5153124928474426 which rounds down, unlike the f64 case | |
| let a: ArrayRef = Arc::new(Float32Array::from(vec![Some(0.5153125f32)])); | |
| let b = | |
| cast_floating_point_to_decimal128::<Float32Type>(&a, 8, 6, EvalMode::Legacy).unwrap(); | |
| let d = b.as_primitive::<Decimal128Type>(); | |
| assert_eq!(d.value(0), 515312); | |
| } | |
| #[test] | |
| // Not a correctness concern under miri, just far too slow: 240k values through the | |
| // interpreter. The fixed-value tests above cover both paths under miri. | |
| #[cfg_attr(miri, ignore)] | |
| fn test_cast_float_to_decimal_fast_path_matches_exact_path() { | |
| use rand::rngs::StdRng; | |
| use rand::{RngExt, SeedableRng}; | |
| let mut rng = StdRng::seed_from_u64(42); | |
| let mut values: Vec<f64> = Vec::new(); | |
| // random bit patterns cover the full range including subnormals, NaN and infinity | |
| for _ in 0..20_000 { | |
| values.push(f64::from_bits(rng.random::<u64>())); | |
| } | |
| // decimal-looking values, which land on or near rounding ties far more often | |
| for _ in 0..20_000 { | |
| let unscaled = rng.random_range(-10_000_000_i64..10_000_000); | |
| let scale = rng.random_range(0_i32..8); | |
| values.push(unscaled as f64 / 10_f64.powi(scale)); | |
| } | |
| for (precision, scale) in [(38_u8, 18_i8), (18, 6), (10, 2), (8, 6), (4, 2), (38, 0)] { | |
| let d = cast_f64_to_decimal128( | |
| values.iter().copied().map(Some).collect(), | |
| precision, | |
| scale, | |
| EvalMode::Legacy, | |
| ); | |
| for (i, &v) in values.iter().enumerate() { | |
| let expected = float_to_decimal128(v, precision, scale); | |
| let actual = (!d.is_null(i)).then(|| d.value(i)); | |
| assert_eq!(actual, expected, "value {v:?} at ({precision},{scale})"); | |
| } | |
| } | |
| } | |
| #[test] | |
| fn test_cast_float_to_decimal_round_up_overflow() { | |
| // 99.995 rounds up to 100.00 in string form, which no longer fits precision 4. | |
| // Binary rounding of 99.99499999999999886... would incorrectly keep 99.99. | |
| let d = cast_f64_to_decimal128(vec![Some(99.995)], 4, 2, EvalMode::Legacy); | |
| assert!(d.is_null(0)); | |
| let a: ArrayRef = Arc::new(Float64Array::from(vec![Some(99.995)])); | |
| match cast_floating_point_to_decimal128::<Float64Type>(&a, 4, 2, EvalMode::Ansi) { | |
| Err(SparkError::NumericValueOutOfRange { | |
| value, | |
| precision, | |
| scale, | |
| }) => { | |
| assert_eq!(value, "99.995"); | |
| assert_eq!(precision, 4); | |
| assert_eq!(scale, 2); | |
| } | |
| 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" | |
| ); | |
| } | |
| } |