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