Skip to main content

vortex_array/arrays/decimal/compute/
cast.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use num_traits::AsPrimitive;
5use num_traits::CheckedMul;
6use num_traits::ToPrimitive as NumToPrimitive;
7use vortex_buffer::Buffer;
8use vortex_buffer::BufferMut;
9use vortex_compute::lane_kernels::IndexedSourceExt;
10use vortex_error::VortexError;
11use vortex_error::VortexExpect;
12use vortex_error::VortexResult;
13use vortex_error::vortex_bail;
14use vortex_error::vortex_err;
15use vortex_error::vortex_panic;
16use vortex_mask::Mask;
17
18use crate::ArrayRef;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::array::ArrayView;
22use crate::arrays::ConstantArray;
23use crate::arrays::Decimal;
24use crate::arrays::DecimalArray;
25use crate::arrays::PrimitiveArray;
26use crate::arrays::decimal::DecimalArrayExt;
27use crate::dtype::BigCast;
28use crate::dtype::DType;
29use crate::dtype::DecimalDType;
30use crate::dtype::DecimalType;
31use crate::dtype::IntegerPType;
32use crate::dtype::NativeDecimalType;
33use crate::dtype::Nullability;
34use crate::dtype::PType;
35use crate::dtype::ToI256;
36use crate::dtype::i256;
37use crate::match_each_decimal_value_type;
38use crate::match_each_integer_ptype;
39use crate::scalar::DecimalToIntegerCast;
40use crate::scalar::DecimalValue;
41use crate::scalar::Scalar;
42use crate::scalar_fn::fns::cast::CastKernel;
43use crate::scalar_fn::fns::cast::CastReduce;
44use crate::validity::Validity;
45
46impl CastReduce for Decimal {
47    fn cast(array: ArrayView<'_, Decimal>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
48        // Only nullability changes within the same decimal dtype are reducible without execution.
49        // Precision/scale changes need the kernel.
50        let DType::Decimal(to_decimal_dtype, to_nullability) = dtype else {
51            return Ok(None);
52        };
53        let DType::Decimal(from_decimal_dtype, _) = array.dtype() else {
54            vortex_panic!(
55                "DecimalArray must have decimal dtype, got {:?}",
56                array.dtype()
57            );
58        };
59
60        if from_decimal_dtype != to_decimal_dtype {
61            return Ok(None);
62        }
63
64        let Some(new_validity) = array
65            .validity()?
66            .trivially_cast_nullability(*to_nullability, array.len())?
67        else {
68            return Ok(None);
69        };
70
71        // SAFETY: validity has the same length, only its nullability tag changes.
72        unsafe {
73            Ok(Some(
74                DecimalArray::new_unchecked_handle(
75                    array.buffer_handle().clone(),
76                    array.values_type(),
77                    *to_decimal_dtype,
78                    new_validity,
79                )
80                .into_array(),
81            ))
82        }
83    }
84}
85
86impl CastKernel for Decimal {
87    fn cast(
88        array: ArrayView<'_, Decimal>,
89        dtype: &DType,
90        ctx: &mut ExecutionCtx,
91    ) -> VortexResult<Option<ArrayRef>> {
92        let DType::Decimal(from_decimal_dtype, _) = array.dtype() else {
93            vortex_panic!(
94                "DecimalArray must have decimal dtype, got {:?}",
95                array.dtype()
96            );
97        };
98        if let DType::Primitive(ptype, nullability) = dtype
99            && ptype.is_int()
100        {
101            return match_each_integer_ptype!(*ptype, |T| {
102                cast_to_integer::<T>(array, *nullability, ctx).map(Some)
103            });
104        }
105        if let DType::Primitive(PType::F64, nullability) = dtype {
106            let scale = from_decimal_dtype.scale();
107            return cast_to_f64(array, scale, *nullability, ctx).map(Some);
108        }
109        let DType::Decimal(to_decimal_dtype, to_nullability) = dtype else {
110            return Ok(None);
111        };
112
113        // If the dtype is exactly the same, return self
114        if array.dtype() == dtype {
115            return Ok(Some(array.array().clone()));
116        }
117
118        let validity = array.validity()?;
119
120        // Cast the validity to the new nullability
121        let new_validity = validity
122            .clone()
123            .cast_nullability(*to_nullability, array.len(), ctx)?;
124
125        // Reuse the values buffer untouched when no rescale is required, the target precision
126        // only widens (so every value still fits), and the current physical type is already wide
127        // enough to hold the target precision. This keeps the common precision-widening cast
128        // (and pure nullability changes) zero-copy instead of allocating and re-scanning.
129        if from_decimal_dtype.scale() == to_decimal_dtype.scale()
130            && to_decimal_dtype.precision() >= from_decimal_dtype.precision()
131            && array
132                .values_type()
133                .is_compatible_decimal_value_type(*to_decimal_dtype)
134        {
135            // SAFETY: the source values are bit-identical and remain in range for the wider
136            // precision, and new_validity has the same length, only its nullability tag changes.
137            unsafe {
138                return Ok(Some(
139                    DecimalArray::new_unchecked_handle(
140                        array.buffer_handle().clone(),
141                        array.values_type(),
142                        *to_decimal_dtype,
143                        new_validity,
144                    )
145                    .into_array(),
146                ));
147            }
148        }
149
150        let valid_values = validity.execute_mask(array.len(), ctx)?;
151        if !array.is_empty() && matches!(valid_values, Mask::AllFalse(_)) {
152            return Ok(Some(
153                ConstantArray::new(Scalar::null(dtype.clone()), array.len()).into_array(),
154            ));
155        }
156        let target_values_type = DecimalType::smallest_decimal_value_type(to_decimal_dtype);
157
158        match_each_decimal_value_type!(array.values_type(), |F| {
159            match_each_decimal_value_type!(target_values_type, |T| {
160                cast_decimal_values::<F, T>(
161                    array,
162                    *from_decimal_dtype,
163                    *to_decimal_dtype,
164                    new_validity,
165                    &valid_values,
166                )
167                .map(Some)
168            })
169        })
170    }
171}
172
173fn cast_to_integer<T: IntegerPType + BigCast>(
174    array: ArrayView<'_, Decimal>,
175    nullability: Nullability,
176    ctx: &mut ExecutionCtx,
177) -> VortexResult<ArrayRef> {
178    let source_validity = array.validity()?;
179    let mask = source_validity.execute_mask(array.len(), ctx)?;
180    let validity = source_validity.cast_nullability(nullability, array.len(), ctx)?;
181    if !array.is_empty() && matches!(mask, Mask::AllFalse(_)) {
182        return Ok(ConstantArray::new(
183            Scalar::null(DType::Primitive(T::PTYPE, nullability)),
184            array.len(),
185        )
186        .into_array());
187    }
188    let cast = DecimalToIntegerCast::<T>::new(array.decimal_dtype().scale());
189    let buffer = match_each_decimal_value_type!(array.values_type(), |F| {
190        let values = array.buffer::<F>();
191        cast_decimal_buffer(values.as_slice(), &mask, |value: F| {
192            cast.cast(value.to_i256()?)
193        })
194        .map_err(|index| cast.error(DecimalValue::from(values[index]).as_i256()))?
195    });
196    Ok(PrimitiveArray::new(buffer, validity).into_array())
197}
198
199fn cast_to_f64(
200    array: ArrayView<'_, Decimal>,
201    scale: i8,
202    nullability: Nullability,
203    ctx: &mut ExecutionCtx,
204) -> VortexResult<ArrayRef> {
205    let source_validity = array.validity()?;
206    let n = array.len();
207    let mask = source_validity.execute_mask(n, ctx)?;
208    let validity = source_validity.cast_nullability(nullability, n, ctx)?;
209    if n > 0 && matches!(mask, Mask::AllFalse(_)) {
210        return Ok(
211            ConstantArray::new(Scalar::null(DType::Primitive(PType::F64, nullability)), n)
212                .into_array(),
213        );
214    }
215
216    let scale: i32 = scale.as_();
217    let inv_factor: f64 = 10f64.powi(-scale);
218
219    let buffer = match &mask {
220        Mask::AllFalse(_) => BufferMut::<f64>::zeroed(n),
221        Mask::AllTrue(_) => match_each_decimal_value_type!(array.values_type(), |F| {
222            let values = array.buffer::<F>();
223            let values = values.as_slice();
224            let mut out = BufferMut::<f64>::with_capacity(n);
225            values.map_into(&mut out.spare_capacity_mut()[..n], |v: F| {
226                to_f64_lossy::<F>(v) * inv_factor
227            });
228            // SAFETY: map_into wrote every lane before returning.
229            unsafe { out.set_len(n) };
230            out
231        }),
232        Mask::Values(mask_values) => match_each_decimal_value_type!(array.values_type(), |F| {
233            let values = array.buffer::<F>();
234            let values = values.as_slice();
235            let mut out = BufferMut::<f64>::with_capacity(n);
236            let write_result = values.try_map_masked_into(
237                mask_values.bit_buffer(),
238                &mut out.spare_capacity_mut()[..n],
239                |v: F| Some(to_f64_lossy::<F>(v) * inv_factor),
240            );
241            debug_assert!(write_result.is_ok());
242            // SAFETY: try_map_masked_into wrote every lane before returning Ok.
243            unsafe { out.set_len(n) };
244            out
245        }),
246    };
247
248    Ok(PrimitiveArray::new(buffer, validity).into_array())
249}
250
251#[inline]
252fn to_f64_lossy<F: NativeDecimalType>(value: F) -> f64 {
253    NumToPrimitive::to_f64(&value).unwrap_or(f64::NAN)
254}
255
256fn cast_decimal_values<F, T>(
257    array: ArrayView<'_, Decimal>,
258    from_decimal_dtype: DecimalDType,
259    to_decimal_dtype: DecimalDType,
260    validity: Validity,
261    valid_values: &Mask,
262) -> VortexResult<ArrayRef>
263where
264    F: NativeDecimalType,
265    T: NativeDecimalType + CheckedMul,
266    DecimalValue: From<F>,
267{
268    let values = array.buffer::<F>();
269    let values = values.as_slice();
270    let cast_result = match DecimalCastPlan::<T>::new(from_decimal_dtype, to_decimal_dtype) {
271        DecimalCastPlan::SameScale { min, max } => {
272            cast_decimal_buffer(values, valid_values, |value| {
273                let value = <T as BigCast>::from(value)?;
274                (value >= min && value <= max).then_some(value)
275            })
276        }
277        cast_plan => cast_decimal_buffer(values, valid_values, |value| cast_plan.cast(value)),
278    };
279    let buffer = cast_result.map_err(|idx| {
280        decimal_cast_error::<F, T>(values[idx], from_decimal_dtype, to_decimal_dtype)
281    })?;
282
283    Ok(DecimalArray::new(buffer, to_decimal_dtype, validity).into_array())
284}
285
286fn cast_decimal_buffer<F, T>(
287    values: &[F],
288    valid_values: &Mask,
289    cast: impl Fn(F) -> Option<T>,
290) -> Result<Buffer<T>, usize>
291where
292    F: NativeDecimalType,
293    T: Copy + Default,
294{
295    let mut buffer = BufferMut::<T>::with_capacity(values.len());
296    match valid_values {
297        Mask::AllTrue(_) => {
298            values.try_map_into(&mut buffer.spare_capacity_mut()[..values.len()], &cast)?;
299        }
300        Mask::AllFalse(_) => return Ok(BufferMut::<T>::zeroed(values.len()).freeze()),
301        Mask::Values(mask) => {
302            values.try_map_masked_into(
303                mask.bit_buffer(),
304                &mut buffer.spare_capacity_mut()[..values.len()],
305                &cast,
306            )?;
307        }
308    }
309    // SAFETY: the selected map kernel initialized every lane before returning Ok.
310    unsafe { buffer.set_len(values.len()) };
311    Ok(buffer.freeze())
312}
313
314#[cold]
315fn decimal_cast_error<F, T>(
316    value: F,
317    from_decimal_dtype: DecimalDType,
318    to_decimal_dtype: DecimalDType,
319) -> VortexError
320where
321    F: NativeDecimalType,
322    T: NativeDecimalType,
323    DecimalValue: From<F>,
324{
325    match DecimalValue::from(value)
326        .cast_decimal(from_decimal_dtype, to_decimal_dtype)
327        .and_then(|value| {
328            value.cast::<T>().ok_or_else(|| {
329                vortex_err!(
330                    "decimal value cannot be represented as {} after casting to {}",
331                    T::DECIMAL_TYPE,
332                    to_decimal_dtype
333                )
334            })
335        }) {
336        Ok(_) => {
337            // The fast path only returns `None` for values the slow path also rejects, so this
338            // arm should be unreachable. If it is hit, the fast and slow paths have drifted and
339            // we are erroring on a value that is actually representable.
340            debug_assert!(
341                false,
342                "decimal fast-path cast rejected value {value} that the slow path accepts \
343                 (from {from_decimal_dtype} to {to_decimal_dtype})"
344            );
345            vortex_err!(
346                "decimal value cannot be represented as {} after casting from {} to {}",
347                T::DECIMAL_TYPE,
348                from_decimal_dtype,
349                to_decimal_dtype
350            )
351        }
352        Err(error) => error,
353    }
354}
355
356#[derive(Debug, Clone, Copy)]
357enum DecimalCastPlan<T> {
358    SameScale { min: T, max: T },
359    ScaleUp { factor: T, min: T, max: T },
360    ScaleUpOverflow,
361    ScaleDown { factor: i256, min: i256, max: i256 },
362    ScaleDownOverflow,
363}
364
365impl<T> DecimalCastPlan<T>
366where
367    T: NativeDecimalType + CheckedMul,
368{
369    fn new(from_decimal_dtype: DecimalDType, to_decimal_dtype: DecimalDType) -> Self {
370        let scale_delta = to_decimal_dtype.scale() as i16 - from_decimal_dtype.scale() as i16;
371        if scale_delta == 0 {
372            let (min, max) = decimal_precision_range::<T>(to_decimal_dtype);
373            return Self::SameScale { min, max };
374        }
375
376        if scale_delta > 0 {
377            let Some(factor) = decimal_scale_factor::<T>(scale_delta as u32) else {
378                return Self::ScaleUpOverflow;
379            };
380            let (min, max) = decimal_precision_range::<T>(to_decimal_dtype);
381            return Self::ScaleUp { factor, min, max };
382        }
383
384        let Some(factor) = decimal_scale_factor::<i256>((-scale_delta) as u32) else {
385            return Self::ScaleDownOverflow;
386        };
387        let (min, max) = decimal_precision_range::<i256>(to_decimal_dtype);
388        Self::ScaleDown { factor, min, max }
389    }
390
391    #[inline]
392    fn cast<F>(&self, value: F) -> Option<T>
393    where
394        F: NativeDecimalType,
395    {
396        match *self {
397            DecimalCastPlan::SameScale { min, max } => {
398                let value = <T as BigCast>::from(value)?;
399                (value >= min && value <= max).then_some(value)
400            }
401            DecimalCastPlan::ScaleUp { factor, min, max } => {
402                let value = <T as BigCast>::from(value)?;
403                let value = value.checked_mul(&factor)?;
404                (value >= min && value <= max).then_some(value)
405            }
406            DecimalCastPlan::ScaleUpOverflow | DecimalCastPlan::ScaleDownOverflow => {
407                (value == F::default()).then_some(T::default())
408            }
409            DecimalCastPlan::ScaleDown { factor, min, max } => {
410                let value = <i256 as BigCast>::from(value)?;
411                if value == i256::ZERO {
412                    return Some(T::default());
413                }
414                if value % factor != i256::ZERO {
415                    return None;
416                }
417
418                let value = value / factor;
419                if value < min || value > max {
420                    return None;
421                }
422                <T as BigCast>::from(value)
423            }
424        }
425    }
426}
427
428fn decimal_precision_range<T: NativeDecimalType>(decimal_dtype: DecimalDType) -> (T, T) {
429    let precision = usize::from(decimal_dtype.precision());
430    (
431        T::MIN_BY_PRECISION[precision],
432        T::MAX_BY_PRECISION[precision],
433    )
434}
435
436fn decimal_scale_factor<T>(exp: u32) -> Option<T>
437where
438    T: NativeDecimalType + CheckedMul,
439{
440    let ten = <T as BigCast>::from(10_i8)?;
441    let mut factor = <T as BigCast>::from(1_i8)?;
442    for _ in 0..exp {
443        factor = factor.checked_mul(&ten)?;
444    }
445    Some(factor)
446}
447
448/// Upcast a DecimalArray to a wider physical representation (e.g., i32 -> i64) while keeping
449/// the same precision and scale.
450///
451/// This is useful when you need to widen the underlying storage type to accommodate operations
452/// that might overflow the current representation, or to match the physical type expected by
453/// downstream consumers.
454///
455/// # Errors
456///
457/// Returns an error if `to_values_type` is narrower than the array's current values type.
458/// Only upcasting (widening) is supported.
459pub fn upcast_decimal_values(
460    array: ArrayView<'_, Decimal>,
461    to_values_type: DecimalType,
462) -> VortexResult<DecimalArray> {
463    let from_values_type = array.values_type();
464
465    // If already the target type, just clone
466    if from_values_type == to_values_type {
467        return Ok(array.array().as_::<Decimal>().into_owned());
468    }
469
470    // Only allow upcasting (widening)
471    if to_values_type < from_values_type {
472        vortex_bail!(
473            "Cannot downcast decimal values from {:?} to {:?}. Only upcasting is supported.",
474            from_values_type,
475            to_values_type
476        );
477    }
478
479    let decimal_dtype = array.decimal_dtype();
480    let validity = array.validity()?;
481
482    // Use match_each_decimal_value_type to dispatch based on source and target types
483    match_each_decimal_value_type!(from_values_type, |F| {
484        let from_buffer = array.buffer::<F>();
485        match_each_decimal_value_type!(to_values_type, |T| {
486            let to_buffer = upcast_decimal_buffer::<F, T>(from_buffer);
487            Ok(DecimalArray::new(to_buffer, decimal_dtype, validity))
488        })
489    })
490}
491
492/// Upcast a buffer of decimal values from type F to type T.
493/// Since T is wider than F, this conversion never fails.
494fn upcast_decimal_buffer<F: NativeDecimalType, T: NativeDecimalType>(from: Buffer<F>) -> Buffer<T> {
495    from.iter()
496        .map(|&v| T::from(v).vortex_expect("upcast should never fail"))
497        .collect()
498}
499
500#[cfg(test)]
501mod tests {
502    use rstest::rstest;
503    use vortex_buffer::buffer;
504    use vortex_error::VortexResult;
505
506    use super::upcast_decimal_values;
507    use crate::Canonical;
508    use crate::IntoArray;
509    use crate::VortexSessionExecute;
510    use crate::array_session;
511    use crate::arrays::DecimalArray;
512    use crate::arrays::PrimitiveArray;
513    use crate::assert_arrays_eq;
514    use crate::builtins::ArrayBuiltins;
515    use crate::compute::conformance::cast::test_cast_conformance;
516    use crate::dtype::BigCast;
517    use crate::dtype::DType;
518    use crate::dtype::DecimalDType;
519    use crate::dtype::DecimalType;
520    use crate::dtype::Nullability;
521    use crate::dtype::PType;
522    use crate::dtype::i256;
523    use crate::match_each_decimal_value_type;
524    use crate::match_each_integer_ptype;
525    use crate::scalar::Scalar;
526    use crate::validity::Validity;
527
528    #[rstest]
529    #[case::workflow(0, vec![0, 1], vec![0, 1])]
530    #[case::fractional(1, vec![-199, -1, 0, 1, 199], vec![-19, 0, 0, 0, 19])]
531    #[case::negative_scale(-2, vec![-12, 0, 12], vec![-1200, 0, 1200])]
532    #[case::large_exact(
533        0,
534        vec![1_786_639_777_684_000_001],
535        vec![1_786_639_777_684_000_001]
536    )]
537    #[case::large_scale(76, vec![-1, 1], vec![0, 0])]
538    #[case::zero_extreme_scale(-128, vec![0, 0], vec![0, 0])]
539    fn cast_decimal_to_integer_policy(
540        #[case] scale: i8,
541        #[case] values: Vec<i128>,
542        #[case] expected: Vec<i64>,
543    ) -> VortexResult<()> {
544        let mut ctx = array_session().create_execution_ctx();
545        let decimal_dtype = DecimalDType::new(76, scale);
546        let array = DecimalArray::new(
547            values.iter().copied().map(i256::from_i128).collect(),
548            decimal_dtype,
549            Validity::NonNullable,
550        );
551        let target = DType::Primitive(PType::I64, Nullability::NonNullable);
552        let casted = array
553            .into_array()
554            .cast(target.clone())?
555            .execute::<PrimitiveArray>(&mut ctx)?;
556        assert_arrays_eq!(
557            casted,
558            PrimitiveArray::from_iter(expected.iter().copied()),
559            &mut ctx
560        );
561        for (value, expected) in values.into_iter().zip(expected) {
562            let scalar = Scalar::decimal(value.into(), decimal_dtype, Nullability::NonNullable);
563            assert_eq!(scalar.cast(&target)?, Scalar::from(expected));
564        }
565        Ok(())
566    }
567
568    #[rstest]
569    fn cast_decimal_to_integer_storage_and_nulls(
570        #[values(
571            DecimalType::I8,
572            DecimalType::I16,
573            DecimalType::I32,
574            DecimalType::I64,
575            DecimalType::I128,
576            DecimalType::I256
577        )]
578        storage: DecimalType,
579        #[values(
580            PType::I8, PType::I16, PType::I32, PType::I64, PType::U8, PType::U16, PType::U32,
581            PType::U64
582        )]
583        target: PType,
584    ) -> VortexResult<()> {
585        let mut ctx = array_session().create_execution_ctx();
586        let decimal_dtype = DecimalDType::new(3, 1);
587        let array = match_each_decimal_value_type!(storage, |F| {
588            DecimalArray::from_option_iter(
589                [Some(19i8), None, Some(123)].map(|value| value.and_then(<F as BigCast>::from)),
590                decimal_dtype,
591            )
592        });
593        let dtype = DType::Primitive(target, Nullability::Nullable);
594        let casted = array
595            .into_array()
596            .cast(dtype.clone())?
597            .execute::<PrimitiveArray>(&mut ctx)?;
598        match_each_integer_ptype!(target, |T| {
599            assert_arrays_eq!(
600                casted,
601                PrimitiveArray::from_option_iter([Some(1 as T), None, Some(12 as T)]),
602                &mut ctx
603            );
604            for (value, expected) in [(19i8, 1 as T), (123, 12 as T)] {
605                let scalar = Scalar::decimal(value.into(), decimal_dtype, Nullability::Nullable);
606                assert_eq!(
607                    scalar.cast(&dtype)?,
608                    Scalar::primitive(expected, Nullability::Nullable)
609                );
610            }
611        });
612        Ok(())
613    }
614
615    #[rstest]
616    fn cast_decimal_to_integer_bounds(
617        #[values(
618            PType::I8, PType::I16, PType::I32, PType::I64, PType::U8, PType::U16, PType::U32,
619            PType::U64
620        )]
621        target: PType,
622    ) -> VortexResult<()> {
623        let mut ctx = array_session().create_execution_ctx();
624        let decimal_dtype = DecimalDType::new(76, 1);
625        let dtype = DType::Primitive(target, Nullability::NonNullable);
626        match_each_integer_ptype!(target, |T| {
627            let ten = i256::from_i128(10);
628            let minimum = i256::from_i128(T::MIN as i128) * ten;
629            let maximum = i256::from_i128(T::MAX as i128) * ten;
630            let array = DecimalArray::new(
631                buffer![minimum, maximum],
632                decimal_dtype,
633                Validity::NonNullable,
634            );
635            let casted = array
636                .into_array()
637                .cast(dtype.clone())?
638                .execute::<PrimitiveArray>(&mut ctx)?;
639            assert_arrays_eq!(
640                casted,
641                PrimitiveArray::from_iter([T::MIN, T::MAX]),
642                &mut ctx
643            );
644            for (value, expected) in [(minimum, T::MIN), (maximum, T::MAX)] {
645                let scalar = Scalar::decimal(value.into(), decimal_dtype, Nullability::NonNullable);
646                assert_eq!(
647                    scalar.cast(&dtype)?,
648                    Scalar::primitive(expected, Nullability::NonNullable)
649                );
650            }
651            // The scalar policy rejects values outside the range even when truncation would fit.
652            for value in [
653                minimum - i256::ONE,
654                maximum + i256::ONE,
655                minimum - ten,
656                maximum + ten,
657            ] {
658                let array = DecimalArray::new(
659                    buffer![i256::ZERO, value],
660                    decimal_dtype,
661                    Validity::NonNullable,
662                );
663                let error = array
664                    .into_array()
665                    .cast(dtype.clone())?
666                    .execute::<PrimitiveArray>(&mut ctx)
667                    .unwrap_err();
668                assert!(error.to_string().contains("out of range"));
669                let scalar = Scalar::decimal(value.into(), decimal_dtype, Nullability::NonNullable);
670                assert!(scalar.cast(&dtype).is_err());
671            }
672        });
673        Ok(())
674    }
675
676    #[test]
677    fn cast_decimal_to_integer_masks_and_empty() -> VortexResult<()> {
678        let mut ctx = array_session().create_execution_ctx();
679        let decimal_dtype = DecimalDType::new(21, 0);
680        let target = DType::Primitive(PType::I8, Nullability::Nullable);
681        for validity in [Validity::from_iter([false, true]), Validity::AllInvalid] {
682            let expected = PrimitiveArray::new(buffer![0i8, 12], validity.clone());
683            let array = DecimalArray::new(buffer![999i128, 12], decimal_dtype, validity);
684            let casted = array
685                .into_array()
686                .cast(target.clone())?
687                .execute::<PrimitiveArray>(&mut ctx)?;
688            assert_arrays_eq!(casted, expected, &mut ctx);
689        }
690        let array = DecimalArray::new(
691            buffer![999i128, 12],
692            decimal_dtype,
693            Validity::from_iter([false, true]),
694        );
695        assert!(
696            array
697                .into_array()
698                .cast(target.as_nonnullable())?
699                .execute::<PrimitiveArray>(&mut ctx)
700                .is_err()
701        );
702        let empty = DecimalArray::from_option_iter([] as [Option<i128>; 0], decimal_dtype);
703        let casted = empty
704            .into_array()
705            .cast(target.as_nonnullable())?
706            .execute::<PrimitiveArray>(&mut ctx)?;
707        assert_arrays_eq!(casted, PrimitiveArray::from_iter([] as [i8; 0]), &mut ctx);
708        Ok(())
709    }
710
711    #[test]
712    fn cast_decimal_to_integer_wide_storage() -> VortexResult<()> {
713        let mut ctx = array_session().create_execution_ctx();
714        let factor = i256::from_i128(10).checked_pow(40).unwrap();
715        let value = i256::from_i128(123) * factor + i256::ONE;
716        let decimal_dtype = DecimalDType::new(76, 40);
717        let target = DType::Primitive(PType::I64, Nullability::NonNullable);
718        let array = DecimalArray::new(buffer![value, -value], decimal_dtype, Validity::NonNullable);
719        let casted = array
720            .into_array()
721            .cast(target.clone())?
722            .execute::<PrimitiveArray>(&mut ctx)?;
723        assert_arrays_eq!(casted, PrimitiveArray::from_iter([123i64, -123]), &mut ctx);
724        for (value, expected) in [(value, 123i64), (-value, -123)] {
725            let scalar = Scalar::decimal(value.into(), decimal_dtype, Nullability::NonNullable);
726            assert_eq!(scalar.cast(&target)?, Scalar::from(expected));
727        }
728        let huge = DecimalArray::new(
729            buffer![i256::ONE],
730            DecimalDType::new(1, -128),
731            Validity::NonNullable,
732        );
733        assert!(
734            huge.into_array()
735                .cast(target)?
736                .execute::<PrimitiveArray>(&mut ctx)
737                .is_err()
738        );
739        Ok(())
740    }
741
742    #[test]
743    fn cast_decimal_to_nullable() {
744        let mut ctx = array_session().create_execution_ctx();
745        let decimal_dtype = DecimalDType::new(10, 2);
746        let array = DecimalArray::new(
747            buffer![100i32, 200, 300],
748            decimal_dtype,
749            Validity::NonNullable,
750        );
751
752        // Cast to nullable
753        let nullable_dtype = DType::Decimal(decimal_dtype, Nullability::Nullable);
754        let casted = array
755            .into_array()
756            .cast(nullable_dtype.clone())
757            .unwrap()
758            .execute::<DecimalArray>(&mut ctx)
759            .unwrap();
760
761        assert_eq!(casted.dtype(), &nullable_dtype);
762        assert!(matches!(casted.validity(), Ok(Validity::AllValid)));
763        assert_eq!(casted.len(), 3);
764    }
765
766    #[test]
767    fn cast_nullable_to_non_nullable() {
768        let mut ctx = array_session().create_execution_ctx();
769        let decimal_dtype = DecimalDType::new(10, 2);
770
771        // Create nullable array with no nulls
772        let array = DecimalArray::new(buffer![100i32, 200, 300], decimal_dtype, Validity::AllValid);
773
774        // Cast to non-nullable
775        let non_nullable_dtype = DType::Decimal(decimal_dtype, Nullability::NonNullable);
776        let casted = array
777            .into_array()
778            .cast(non_nullable_dtype.clone())
779            .unwrap()
780            .execute::<DecimalArray>(&mut ctx)
781            .unwrap();
782
783        assert_eq!(casted.dtype(), &non_nullable_dtype);
784        assert!(matches!(casted.validity(), Ok(Validity::NonNullable)));
785    }
786
787    #[test]
788    #[should_panic(expected = "Cannot cast array with invalid values to non-nullable type")]
789    fn cast_nullable_with_nulls_to_non_nullable_fails() {
790        let mut ctx = array_session().create_execution_ctx();
791        let decimal_dtype = DecimalDType::new(10, 2);
792
793        // Create nullable array with nulls
794        let array = DecimalArray::from_option_iter([Some(100i32), None, Some(300)], decimal_dtype);
795
796        // Attempt to cast to non-nullable should fail
797        let non_nullable_dtype = DType::Decimal(decimal_dtype, Nullability::NonNullable);
798        array
799            .into_array()
800            .cast(non_nullable_dtype)
801            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()))
802            .unwrap();
803    }
804
805    #[test]
806    fn cast_different_scale_rescales() {
807        let mut ctx = array_session().create_execution_ctx();
808        let array = DecimalArray::new(
809            buffer![100i32],
810            DecimalDType::new(10, 2),
811            Validity::NonNullable,
812        );
813
814        // Cast 1.00 to scale 3, where it is stored as 1000.
815        let different_dtype = DType::Decimal(DecimalDType::new(15, 3), Nullability::NonNullable);
816        let casted = array
817            .into_array()
818            .cast(different_dtype)
819            .unwrap()
820            .execute::<DecimalArray>(&mut ctx)
821            .unwrap();
822
823        assert_eq!(casted.precision(), 15);
824        assert_eq!(casted.scale(), 3);
825        assert_eq!(casted.values_type(), DecimalType::I64);
826        assert_eq!(casted.buffer::<i64>().as_ref(), &[1000]);
827    }
828
829    #[test]
830    fn cast_downcast_precision_succeeds_when_values_fit() {
831        let mut ctx = array_session().create_execution_ctx();
832        let array = DecimalArray::new(
833            buffer![100i64],
834            DecimalDType::new(18, 2),
835            Validity::NonNullable,
836        );
837
838        // Downcasting precision is allowed when every value fits.
839        let smaller_dtype = DType::Decimal(DecimalDType::new(10, 2), Nullability::NonNullable);
840        let casted = array
841            .into_array()
842            .cast(smaller_dtype)
843            .unwrap()
844            .execute::<DecimalArray>(&mut ctx)
845            .unwrap();
846
847        assert_eq!(casted.precision(), 10);
848        assert_eq!(casted.scale(), 2);
849        assert_eq!(casted.buffer::<i64>().as_ref(), &[100]);
850    }
851
852    #[test]
853    fn cast_downcast_precision_checks_values() {
854        let mut ctx = array_session().create_execution_ctx();
855        let array = DecimalArray::new(
856            buffer![1000i64],
857            DecimalDType::new(18, 0),
858            Validity::NonNullable,
859        );
860
861        let smaller_dtype = DType::Decimal(DecimalDType::new(3, 0), Nullability::NonNullable);
862        let result = array
863            .into_array()
864            .cast(smaller_dtype)
865            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()));
866
867        assert!(result.is_err());
868        assert!(
869            result
870                .unwrap_err()
871                .to_string()
872                .contains("does not fit in precision")
873        );
874    }
875
876    #[test]
877    fn cast_lower_scale_requires_exact_rescale() {
878        let mut ctx = array_session().create_execution_ctx();
879        let array = DecimalArray::new(
880            buffer![123456i64],
881            DecimalDType::new(10, 4),
882            Validity::NonNullable,
883        );
884
885        let lower_scale_dtype = DType::Decimal(DecimalDType::new(10, 2), Nullability::NonNullable);
886        let result = array
887            .into_array()
888            .cast(lower_scale_dtype)
889            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()));
890
891        assert!(result.is_err());
892        assert!(
893            result
894                .unwrap_err()
895                .to_string()
896                .contains("would lose precision")
897        );
898    }
899
900    #[test]
901    fn cast_lower_scale_ignores_null_lane_failures() {
902        let mut ctx = array_session().create_execution_ctx();
903        let array = DecimalArray::new(
904            buffer![100i64, 123456],
905            DecimalDType::new(10, 4),
906            Validity::from_iter([true, false]),
907        );
908
909        let lower_scale_dtype = DType::Decimal(DecimalDType::new(3, 2), Nullability::Nullable);
910        let casted = array
911            .into_array()
912            .cast(lower_scale_dtype)
913            .unwrap()
914            .execute::<DecimalArray>(&mut ctx)
915            .unwrap();
916
917        let mask = casted
918            .as_ref()
919            .validity()
920            .unwrap()
921            .execute_mask(
922                casted.as_ref().len(),
923                &mut array_session().create_execution_ctx(),
924            )
925            .unwrap();
926        assert!(mask.value(0));
927        assert!(!mask.value(1));
928        assert_eq!(casted.buffer::<i16>().as_ref()[0], 1);
929    }
930
931    #[test]
932    fn cast_upcast_precision_succeeds() {
933        let mut ctx = array_session().create_execution_ctx();
934        let array = DecimalArray::new(
935            buffer![100i32, 200, 300],
936            DecimalDType::new(10, 2),
937            Validity::NonNullable,
938        );
939
940        // Cast to higher precision with same scale - should succeed
941        let wider_dtype = DType::Decimal(DecimalDType::new(38, 2), Nullability::NonNullable);
942        let casted = array
943            .into_array()
944            .cast(wider_dtype)
945            .unwrap()
946            .execute::<DecimalArray>(&mut ctx)
947            .unwrap();
948
949        assert_eq!(casted.precision(), 38);
950        assert_eq!(casted.scale(), 2);
951        assert_eq!(casted.len(), 3);
952        // Should be stored in i128 now (precision 38 requires i128)
953        assert_eq!(casted.values_type(), DecimalType::I128);
954    }
955
956    #[test]
957    fn cast_widening_same_physical_type_is_zero_copy() {
958        let mut ctx = array_session().create_execution_ctx();
959        // Decimal(10,2) and Decimal(18,2) are both physically i64 with the same scale, so widening
960        // the precision must reuse the values buffer rather than allocate and re-scan it.
961        let array = DecimalArray::new(
962            buffer![100i64, 200, 300],
963            DecimalDType::new(10, 2),
964            Validity::NonNullable,
965        );
966        let src_ptr = array.buffer::<i64>().as_ptr();
967
968        let wider_dtype = DType::Decimal(DecimalDType::new(18, 2), Nullability::NonNullable);
969        let casted = array
970            .into_array()
971            .cast(wider_dtype)
972            .unwrap()
973            .execute::<DecimalArray>(&mut ctx)
974            .unwrap();
975
976        assert_eq!(casted.precision(), 18);
977        assert_eq!(casted.scale(), 2);
978        assert_eq!(casted.values_type(), DecimalType::I64);
979        assert_eq!(casted.buffer::<i64>().as_ref(), &[100, 200, 300]);
980        // The values buffer must be shared with the source (zero-copy), not reallocated.
981        assert_eq!(
982            casted.buffer::<i64>().as_ptr(),
983            src_ptr,
984            "precision-widening cast must reuse the source values buffer"
985        );
986    }
987
988    #[test]
989    fn cast_to_non_decimal_returns_err() {
990        let mut ctx = array_session().create_execution_ctx();
991        let array = DecimalArray::new(
992            buffer![100i32],
993            DecimalDType::new(10, 2),
994            Validity::NonNullable,
995        );
996
997        // Try to cast to non-decimal type - should fail since no kernel can handle it
998        let result = array
999            .into_array()
1000            .cast(DType::Utf8(Nullability::NonNullable))
1001            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()));
1002
1003        assert!(result.is_err());
1004        assert!(
1005            result
1006                .unwrap_err()
1007                .to_string()
1008                .contains("No CastKernel to cast canonical array")
1009        );
1010    }
1011
1012    #[rstest]
1013    #[case(DecimalArray::new(buffer![100i32, 200, 300], DecimalDType::new(10, 2), Validity::NonNullable))]
1014    #[case(DecimalArray::new(buffer![10000i64, 20000, 30000], DecimalDType::new(18, 4), Validity::NonNullable))]
1015    #[case(DecimalArray::from_option_iter([Some(100i32), None, Some(300)], DecimalDType::new(10, 2)))]
1016    #[case(DecimalArray::new(buffer![42i32], DecimalDType::new(5, 1), Validity::NonNullable))]
1017    fn test_cast_decimal_conformance(#[case] array: DecimalArray) {
1018        test_cast_conformance(
1019            &array.into_array(),
1020            &mut array_session().create_execution_ctx(),
1021        );
1022    }
1023
1024    #[test]
1025    fn upcast_decimal_values_i32_to_i64() {
1026        let decimal_dtype = DecimalDType::new(10, 2);
1027        let array = DecimalArray::new(
1028            buffer![100i32, 200, 300],
1029            decimal_dtype,
1030            Validity::NonNullable,
1031        );
1032
1033        assert_eq!(array.values_type(), DecimalType::I32);
1034
1035        let array = array.as_view();
1036        let casted = upcast_decimal_values(array, DecimalType::I64).unwrap();
1037
1038        assert_eq!(casted.values_type(), DecimalType::I64);
1039        assert_eq!(casted.decimal_dtype(), decimal_dtype);
1040        assert_eq!(casted.len(), 3);
1041
1042        // Verify values are preserved
1043        let buffer = casted.buffer::<i64>();
1044        assert_eq!(buffer.as_ref(), &[100i64, 200, 300]);
1045    }
1046
1047    #[test]
1048    fn upcast_decimal_values_i64_to_i128() {
1049        let decimal_dtype = DecimalDType::new(18, 4);
1050        let array = DecimalArray::new(
1051            buffer![10000i64, 20000, 30000],
1052            decimal_dtype,
1053            Validity::NonNullable,
1054        );
1055
1056        let array = array.as_view();
1057        let casted = upcast_decimal_values(array, DecimalType::I128).unwrap();
1058
1059        assert_eq!(casted.values_type(), DecimalType::I128);
1060        assert_eq!(casted.decimal_dtype(), decimal_dtype);
1061
1062        let buffer = casted.buffer::<i128>();
1063        assert_eq!(buffer.as_ref(), &[10000i128, 20000, 30000]);
1064    }
1065
1066    #[test]
1067    fn upcast_decimal_values_same_type_returns_clone() {
1068        let decimal_dtype = DecimalDType::new(10, 2);
1069        let array = DecimalArray::new(
1070            buffer![100i32, 200, 300],
1071            decimal_dtype,
1072            Validity::NonNullable,
1073        );
1074
1075        let array = array.as_view();
1076        let casted = upcast_decimal_values(array, DecimalType::I32).unwrap();
1077
1078        assert_eq!(casted.values_type(), DecimalType::I32);
1079        assert_eq!(casted.decimal_dtype(), decimal_dtype);
1080    }
1081
1082    #[test]
1083    fn upcast_decimal_values_with_nulls() {
1084        let decimal_dtype = DecimalDType::new(10, 2);
1085        let array = DecimalArray::from_option_iter([Some(100i32), None, Some(300)], decimal_dtype);
1086
1087        let array = array.as_view();
1088        let casted = upcast_decimal_values(array, DecimalType::I64).unwrap();
1089
1090        assert_eq!(casted.values_type(), DecimalType::I64);
1091        assert_eq!(casted.len(), 3);
1092
1093        // Check validity is preserved
1094        let mask = casted
1095            .as_ref()
1096            .validity()
1097            .unwrap()
1098            .execute_mask(
1099                casted.as_ref().len(),
1100                &mut array_session().create_execution_ctx(),
1101            )
1102            .unwrap();
1103        assert!(mask.value(0));
1104        assert!(!mask.value(1));
1105        assert!(mask.value(2));
1106
1107        // Check non-null values
1108        let buffer = casted.buffer::<i64>();
1109        assert_eq!(buffer[0], 100);
1110        assert_eq!(buffer[2], 300);
1111    }
1112
1113    #[test]
1114    fn upcast_decimal_values_downcast_fails() {
1115        let decimal_dtype = DecimalDType::new(18, 4);
1116        let array = DecimalArray::new(
1117            buffer![10000i64, 20000, 30000],
1118            decimal_dtype,
1119            Validity::NonNullable,
1120        );
1121
1122        // Attempt to downcast from i64 to i32 should fail
1123        let array = array.as_view();
1124        let result = upcast_decimal_values(array, DecimalType::I32);
1125        assert!(result.is_err());
1126        assert!(
1127            result
1128                .unwrap_err()
1129                .to_string()
1130                .contains("Cannot downcast decimal values")
1131        );
1132    }
1133
1134    #[test]
1135    fn cast_decimal_f64() {
1136        let dtype = DecimalDType::new(6, 2);
1137        let array = DecimalArray::new(buffer![125i32, 250, -375], dtype, Validity::NonNullable);
1138        let target = DType::Primitive(PType::F64, Nullability::NonNullable);
1139        let casted = array.into_array().cast(target.clone()).unwrap();
1140        assert_eq!(casted.dtype(), &target);
1141
1142        let mut ctx = array_session().create_execution_ctx();
1143        let primitive = casted
1144            .execute::<Canonical>(&mut ctx)
1145            .unwrap()
1146            .into_primitive();
1147        assert_eq!(primitive.to_buffer::<f64>().as_ref(), &[1.25, 2.5, -3.75]);
1148    }
1149
1150    #[test]
1151    fn cast_decimal_f64_null() {
1152        let values = [Some(100i32), None, Some(-200)];
1153        let array = DecimalArray::from_option_iter(values, DecimalDType::new(6, 2));
1154        let target = DType::Primitive(PType::F64, Nullability::Nullable);
1155        let casted = array.into_array().cast(target.clone()).unwrap();
1156        assert_eq!(casted.dtype(), &target);
1157
1158        let mut ctx = array_session().create_execution_ctx();
1159        let primitive = casted
1160            .execute::<Canonical>(&mut ctx)
1161            .unwrap()
1162            .into_primitive();
1163
1164        assert!(primitive.is_valid(0, &mut ctx).unwrap());
1165        assert!(!primitive.is_valid(1, &mut ctx).unwrap());
1166        assert!(primitive.is_valid(2, &mut ctx).unwrap());
1167
1168        assert_eq!(
1169            primitive.execute_scalar(0, &mut ctx).unwrap(),
1170            Scalar::from(1f64)
1171        );
1172        assert_eq!(
1173            primitive.execute_scalar(2, &mut ctx).unwrap(),
1174            Scalar::from(-2f64)
1175        );
1176    }
1177
1178    #[test]
1179    fn cast_decimal_f64_all_null() {
1180        let dtype = DecimalDType::new(6, 2);
1181        let buf = buffer![i32::MAX, i32::MIN, 12345];
1182        let array = DecimalArray::new(buf, dtype, Validity::AllInvalid);
1183        let target = DType::Primitive(PType::F64, Nullability::Nullable);
1184        let casted = array.into_array().cast(target.clone()).unwrap();
1185        assert_eq!(casted.dtype(), &target);
1186
1187        let primitive = casted
1188            .execute::<Canonical>(&mut array_session().create_execution_ctx())
1189            .unwrap()
1190            .into_primitive();
1191        let mut ctx = array_session().create_execution_ctx();
1192        for i in 0..2 {
1193            assert!(!primitive.is_valid(i, &mut ctx).unwrap());
1194        }
1195        assert_eq!(primitive.to_buffer::<f64>().as_ref(), &[0.0, 0.0, 0.0]);
1196    }
1197}