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::Decimal;
23use crate::arrays::DecimalArray;
24use crate::arrays::PrimitiveArray;
25use crate::dtype::BigCast;
26use crate::dtype::DType;
27use crate::dtype::DecimalDType;
28use crate::dtype::DecimalType;
29use crate::dtype::NativeDecimalType;
30use crate::dtype::Nullability;
31use crate::dtype::PType;
32use crate::dtype::i256;
33use crate::match_each_decimal_value_type;
34use crate::scalar::DecimalValue;
35use crate::scalar_fn::fns::cast::CastKernel;
36use crate::scalar_fn::fns::cast::CastReduce;
37use crate::validity::Validity;
38
39impl CastReduce for Decimal {
40    fn cast(array: ArrayView<'_, Decimal>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
41        // Only nullability changes within the same decimal dtype are reducible without execution.
42        // Precision/scale changes need the kernel.
43        let DType::Decimal(to_decimal_dtype, to_nullability) = dtype else {
44            return Ok(None);
45        };
46        let DType::Decimal(from_decimal_dtype, _) = array.dtype() else {
47            vortex_panic!(
48                "DecimalArray must have decimal dtype, got {:?}",
49                array.dtype()
50            );
51        };
52
53        if from_decimal_dtype != to_decimal_dtype {
54            return Ok(None);
55        }
56
57        let Some(new_validity) = array
58            .validity()?
59            .trivially_cast_nullability(*to_nullability, array.len())?
60        else {
61            return Ok(None);
62        };
63
64        // SAFETY: validity has the same length, only its nullability tag changes.
65        unsafe {
66            Ok(Some(
67                DecimalArray::new_unchecked_handle(
68                    array.buffer_handle().clone(),
69                    array.values_type(),
70                    *to_decimal_dtype,
71                    new_validity,
72                )
73                .into_array(),
74            ))
75        }
76    }
77}
78
79impl CastKernel for Decimal {
80    fn cast(
81        array: ArrayView<'_, Decimal>,
82        dtype: &DType,
83        ctx: &mut ExecutionCtx,
84    ) -> VortexResult<Option<ArrayRef>> {
85        let DType::Decimal(from_decimal_dtype, _) = array.dtype() else {
86            vortex_panic!(
87                "DecimalArray must have decimal dtype, got {:?}",
88                array.dtype()
89            );
90        };
91        if let DType::Primitive(PType::F64, nullability) = dtype {
92            let scale = from_decimal_dtype.scale();
93            return cast_to_f64(array, scale, *nullability, ctx).map(Some);
94        }
95        let DType::Decimal(to_decimal_dtype, to_nullability) = dtype else {
96            return Ok(None);
97        };
98
99        // If the dtype is exactly the same, return self
100        if array.dtype() == dtype {
101            return Ok(Some(array.array().clone()));
102        }
103
104        let validity = array.validity()?;
105
106        // Cast the validity to the new nullability
107        let new_validity = validity
108            .clone()
109            .cast_nullability(*to_nullability, array.len(), ctx)?;
110
111        // Reuse the values buffer untouched when no rescale is required, the target precision
112        // only widens (so every value still fits), and the current physical type is already wide
113        // enough to hold the target precision. This keeps the common precision-widening cast
114        // (and pure nullability changes) zero-copy instead of allocating and re-scanning.
115        if from_decimal_dtype.scale() == to_decimal_dtype.scale()
116            && to_decimal_dtype.precision() >= from_decimal_dtype.precision()
117            && array
118                .values_type()
119                .is_compatible_decimal_value_type(*to_decimal_dtype)
120        {
121            // SAFETY: the source values are bit-identical and remain in range for the wider
122            // precision, and new_validity has the same length, only its nullability tag changes.
123            unsafe {
124                return Ok(Some(
125                    DecimalArray::new_unchecked_handle(
126                        array.buffer_handle().clone(),
127                        array.values_type(),
128                        *to_decimal_dtype,
129                        new_validity,
130                    )
131                    .into_array(),
132                ));
133            }
134        }
135
136        let valid_values = validity.execute_mask(array.len(), ctx)?;
137        let target_values_type = DecimalType::smallest_decimal_value_type(to_decimal_dtype);
138
139        match_each_decimal_value_type!(array.values_type(), |F| {
140            match_each_decimal_value_type!(target_values_type, |T| {
141                cast_decimal_values::<F, T>(
142                    array,
143                    *from_decimal_dtype,
144                    *to_decimal_dtype,
145                    new_validity,
146                    &valid_values,
147                )
148                .map(Some)
149            })
150        })
151    }
152}
153
154fn cast_to_f64(
155    array: ArrayView<'_, Decimal>,
156    scale: i8,
157    nullability: Nullability,
158    ctx: &mut ExecutionCtx,
159) -> VortexResult<ArrayRef> {
160    let source_validity = array.validity()?;
161    let n = array.len();
162    let mask = source_validity.execute_mask(n, ctx)?;
163    let validity = source_validity.cast_nullability(nullability, n, ctx)?;
164
165    let scale: i32 = scale.as_();
166    let inv_factor: f64 = 10f64.powi(-scale);
167
168    let buffer = match &mask {
169        Mask::AllFalse(_) => BufferMut::<f64>::zeroed(n),
170        Mask::AllTrue(_) => match_each_decimal_value_type!(array.values_type(), |F| {
171            let values = array.buffer::<F>();
172            let values = values.as_slice();
173            let mut out = BufferMut::<f64>::with_capacity(n);
174            values.map_into(&mut out.spare_capacity_mut()[..n], |v: F| {
175                to_f64_lossy::<F>(v) * inv_factor
176            });
177            // SAFETY: map_into wrote every lane before returning.
178            unsafe { out.set_len(n) };
179            out
180        }),
181        Mask::Values(mask_values) => match_each_decimal_value_type!(array.values_type(), |F| {
182            let values = array.buffer::<F>();
183            let values = values.as_slice();
184            let mut out = BufferMut::<f64>::with_capacity(n);
185            let write_result = values.try_map_masked_into(
186                mask_values.bit_buffer(),
187                &mut out.spare_capacity_mut()[..n],
188                |v: F| Some(to_f64_lossy::<F>(v) * inv_factor),
189            );
190            debug_assert!(write_result.is_ok());
191            // SAFETY: try_map_masked_into wrote every lane before returning Ok.
192            unsafe { out.set_len(n) };
193            out
194        }),
195    };
196
197    Ok(PrimitiveArray::new(buffer, validity).into_array())
198}
199
200#[inline]
201fn to_f64_lossy<F: NativeDecimalType>(value: F) -> f64 {
202    NumToPrimitive::to_f64(&value).unwrap_or(f64::NAN)
203}
204
205fn cast_decimal_values<F, T>(
206    array: ArrayView<'_, Decimal>,
207    from_decimal_dtype: DecimalDType,
208    to_decimal_dtype: DecimalDType,
209    validity: Validity,
210    valid_values: &Mask,
211) -> VortexResult<ArrayRef>
212where
213    F: NativeDecimalType,
214    T: NativeDecimalType + CheckedMul,
215    DecimalValue: From<F>,
216{
217    let values = array.buffer::<F>();
218    let values = values.as_slice();
219    let cast_plan = DecimalCastPlan::<T>::new(from_decimal_dtype, to_decimal_dtype);
220
221    let buffer = match valid_values {
222        Mask::AllTrue(_) => {
223            let mut buffer = BufferMut::<T>::with_capacity(values.len());
224            values
225                .try_map_into(&mut buffer.spare_capacity_mut()[..values.len()], |value| {
226                    cast_plan.cast(value)
227                })
228                .map_err(|idx| {
229                    decimal_cast_error::<F, T>(values[idx], from_decimal_dtype, to_decimal_dtype)
230                })?;
231            // SAFETY: try_map_into initializes every lane before returning Ok.
232            unsafe { buffer.set_len(values.len()) };
233            buffer.freeze()
234        }
235        Mask::AllFalse(_) => BufferMut::<T>::zeroed(values.len()).freeze(),
236        Mask::Values(mask) => {
237            let mut buffer = BufferMut::<T>::with_capacity(values.len());
238            values
239                .try_map_masked_into(
240                    mask.bit_buffer(),
241                    &mut buffer.spare_capacity_mut()[..values.len()],
242                    |value| cast_plan.cast(value),
243                )
244                .map_err(|idx| {
245                    decimal_cast_error::<F, T>(values[idx], from_decimal_dtype, to_decimal_dtype)
246                })?;
247            // SAFETY: try_map_masked_into initializes every lane before returning Ok.
248            unsafe { buffer.set_len(values.len()) };
249            buffer.freeze()
250        }
251    };
252
253    Ok(DecimalArray::new(buffer, to_decimal_dtype, validity).into_array())
254}
255
256#[cold]
257fn decimal_cast_error<F, T>(
258    value: F,
259    from_decimal_dtype: DecimalDType,
260    to_decimal_dtype: DecimalDType,
261) -> VortexError
262where
263    F: NativeDecimalType,
264    T: NativeDecimalType,
265    DecimalValue: From<F>,
266{
267    match DecimalValue::from(value)
268        .cast_decimal(from_decimal_dtype, to_decimal_dtype)
269        .and_then(|value| {
270            value.cast::<T>().ok_or_else(|| {
271                vortex_err!(
272                    "decimal value cannot be represented as {} after casting to {}",
273                    T::DECIMAL_TYPE,
274                    to_decimal_dtype
275                )
276            })
277        }) {
278        Ok(_) => {
279            // The fast path only returns `None` for values the slow path also rejects, so this
280            // arm should be unreachable. If it is hit, the fast and slow paths have drifted and
281            // we are erroring on a value that is actually representable.
282            debug_assert!(
283                false,
284                "decimal fast-path cast rejected value {value} that the slow path accepts \
285                 (from {from_decimal_dtype} to {to_decimal_dtype})"
286            );
287            vortex_err!(
288                "decimal value cannot be represented as {} after casting from {} to {}",
289                T::DECIMAL_TYPE,
290                from_decimal_dtype,
291                to_decimal_dtype
292            )
293        }
294        Err(error) => error,
295    }
296}
297
298#[derive(Debug, Clone, Copy)]
299enum DecimalCastPlan<T> {
300    SameScale { min: T, max: T },
301    ScaleUp { factor: T, min: T, max: T },
302    ScaleUpOverflow,
303    ScaleDown { factor: i256, min: i256, max: i256 },
304    ScaleDownOverflow,
305}
306
307impl<T> DecimalCastPlan<T>
308where
309    T: NativeDecimalType + CheckedMul,
310{
311    fn new(from_decimal_dtype: DecimalDType, to_decimal_dtype: DecimalDType) -> Self {
312        let scale_delta = to_decimal_dtype.scale() as i16 - from_decimal_dtype.scale() as i16;
313        if scale_delta == 0 {
314            let (min, max) = decimal_precision_range::<T>(to_decimal_dtype);
315            return Self::SameScale { min, max };
316        }
317
318        if scale_delta > 0 {
319            let Some(factor) = decimal_scale_factor::<T>(scale_delta as u32) else {
320                return Self::ScaleUpOverflow;
321            };
322            let (min, max) = decimal_precision_range::<T>(to_decimal_dtype);
323            return Self::ScaleUp { factor, min, max };
324        }
325
326        let Some(factor) = decimal_scale_factor::<i256>((-scale_delta) as u32) else {
327            return Self::ScaleDownOverflow;
328        };
329        let (min, max) = decimal_precision_range::<i256>(to_decimal_dtype);
330        Self::ScaleDown { factor, min, max }
331    }
332
333    #[inline]
334    fn cast<F>(&self, value: F) -> Option<T>
335    where
336        F: NativeDecimalType,
337    {
338        match *self {
339            DecimalCastPlan::SameScale { min, max } => {
340                let value = <T as BigCast>::from(value)?;
341                (value >= min && value <= max).then_some(value)
342            }
343            DecimalCastPlan::ScaleUp { factor, min, max } => {
344                let value = <T as BigCast>::from(value)?;
345                let value = value.checked_mul(&factor)?;
346                (value >= min && value <= max).then_some(value)
347            }
348            DecimalCastPlan::ScaleUpOverflow | DecimalCastPlan::ScaleDownOverflow => {
349                (value == F::default()).then_some(T::default())
350            }
351            DecimalCastPlan::ScaleDown { factor, min, max } => {
352                let value = <i256 as BigCast>::from(value)?;
353                if value == i256::ZERO {
354                    return Some(T::default());
355                }
356                if value % factor != i256::ZERO {
357                    return None;
358                }
359
360                let value = value / factor;
361                if value < min || value > max {
362                    return None;
363                }
364                <T as BigCast>::from(value)
365            }
366        }
367    }
368}
369
370fn decimal_precision_range<T: NativeDecimalType>(decimal_dtype: DecimalDType) -> (T, T) {
371    let precision = usize::from(decimal_dtype.precision());
372    (
373        T::MIN_BY_PRECISION[precision],
374        T::MAX_BY_PRECISION[precision],
375    )
376}
377
378fn decimal_scale_factor<T>(exp: u32) -> Option<T>
379where
380    T: NativeDecimalType + CheckedMul,
381{
382    let ten = <T as BigCast>::from(10_i8)?;
383    let mut factor = <T as BigCast>::from(1_i8)?;
384    for _ in 0..exp {
385        factor = factor.checked_mul(&ten)?;
386    }
387    Some(factor)
388}
389
390/// Upcast a DecimalArray to a wider physical representation (e.g., i32 -> i64) while keeping
391/// the same precision and scale.
392///
393/// This is useful when you need to widen the underlying storage type to accommodate operations
394/// that might overflow the current representation, or to match the physical type expected by
395/// downstream consumers.
396///
397/// # Errors
398///
399/// Returns an error if `to_values_type` is narrower than the array's current values type.
400/// Only upcasting (widening) is supported.
401pub fn upcast_decimal_values(
402    array: ArrayView<'_, Decimal>,
403    to_values_type: DecimalType,
404) -> VortexResult<DecimalArray> {
405    let from_values_type = array.values_type();
406
407    // If already the target type, just clone
408    if from_values_type == to_values_type {
409        return Ok(array.array().as_::<Decimal>().into_owned());
410    }
411
412    // Only allow upcasting (widening)
413    if to_values_type < from_values_type {
414        vortex_bail!(
415            "Cannot downcast decimal values from {:?} to {:?}. Only upcasting is supported.",
416            from_values_type,
417            to_values_type
418        );
419    }
420
421    let decimal_dtype = array.decimal_dtype();
422    let validity = array.validity()?;
423
424    // Use match_each_decimal_value_type to dispatch based on source and target types
425    match_each_decimal_value_type!(from_values_type, |F| {
426        let from_buffer = array.buffer::<F>();
427        match_each_decimal_value_type!(to_values_type, |T| {
428            let to_buffer = upcast_decimal_buffer::<F, T>(from_buffer);
429            Ok(DecimalArray::new(to_buffer, decimal_dtype, validity))
430        })
431    })
432}
433
434/// Upcast a buffer of decimal values from type F to type T.
435/// Since T is wider than F, this conversion never fails.
436fn upcast_decimal_buffer<F: NativeDecimalType, T: NativeDecimalType>(from: Buffer<F>) -> Buffer<T> {
437    from.iter()
438        .map(|&v| T::from(v).vortex_expect("upcast should never fail"))
439        .collect()
440}
441
442#[cfg(test)]
443mod tests {
444    use rstest::rstest;
445    use vortex_buffer::buffer;
446
447    use super::upcast_decimal_values;
448    use crate::Canonical;
449    use crate::IntoArray;
450    use crate::VortexSessionExecute;
451    use crate::array_session;
452    use crate::arrays::DecimalArray;
453    use crate::builtins::ArrayBuiltins;
454    use crate::compute::conformance::cast::test_cast_conformance;
455    use crate::dtype::DType;
456    use crate::dtype::DecimalDType;
457    use crate::dtype::DecimalType;
458    use crate::dtype::Nullability;
459    use crate::dtype::PType;
460    use crate::scalar::Scalar;
461    use crate::validity::Validity;
462
463    #[test]
464    fn cast_decimal_to_nullable() {
465        let mut ctx = array_session().create_execution_ctx();
466        let decimal_dtype = DecimalDType::new(10, 2);
467        let array = DecimalArray::new(
468            buffer![100i32, 200, 300],
469            decimal_dtype,
470            Validity::NonNullable,
471        );
472
473        // Cast to nullable
474        let nullable_dtype = DType::Decimal(decimal_dtype, Nullability::Nullable);
475        let casted = array
476            .into_array()
477            .cast(nullable_dtype.clone())
478            .unwrap()
479            .execute::<DecimalArray>(&mut ctx)
480            .unwrap();
481
482        assert_eq!(casted.dtype(), &nullable_dtype);
483        assert!(matches!(casted.validity(), Ok(Validity::AllValid)));
484        assert_eq!(casted.len(), 3);
485    }
486
487    #[test]
488    fn cast_nullable_to_non_nullable() {
489        let mut ctx = array_session().create_execution_ctx();
490        let decimal_dtype = DecimalDType::new(10, 2);
491
492        // Create nullable array with no nulls
493        let array = DecimalArray::new(buffer![100i32, 200, 300], decimal_dtype, Validity::AllValid);
494
495        // Cast to non-nullable
496        let non_nullable_dtype = DType::Decimal(decimal_dtype, Nullability::NonNullable);
497        let casted = array
498            .into_array()
499            .cast(non_nullable_dtype.clone())
500            .unwrap()
501            .execute::<DecimalArray>(&mut ctx)
502            .unwrap();
503
504        assert_eq!(casted.dtype(), &non_nullable_dtype);
505        assert!(matches!(casted.validity(), Ok(Validity::NonNullable)));
506    }
507
508    #[test]
509    #[should_panic(expected = "Cannot cast array with invalid values to non-nullable type")]
510    fn cast_nullable_with_nulls_to_non_nullable_fails() {
511        let mut ctx = array_session().create_execution_ctx();
512        let decimal_dtype = DecimalDType::new(10, 2);
513
514        // Create nullable array with nulls
515        let array = DecimalArray::from_option_iter([Some(100i32), None, Some(300)], decimal_dtype);
516
517        // Attempt to cast to non-nullable should fail
518        let non_nullable_dtype = DType::Decimal(decimal_dtype, Nullability::NonNullable);
519        array
520            .into_array()
521            .cast(non_nullable_dtype)
522            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()))
523            .unwrap();
524    }
525
526    #[test]
527    fn cast_different_scale_rescales() {
528        let mut ctx = array_session().create_execution_ctx();
529        let array = DecimalArray::new(
530            buffer![100i32],
531            DecimalDType::new(10, 2),
532            Validity::NonNullable,
533        );
534
535        // Cast 1.00 to scale 3, where it is stored as 1000.
536        let different_dtype = DType::Decimal(DecimalDType::new(15, 3), Nullability::NonNullable);
537        let casted = array
538            .into_array()
539            .cast(different_dtype)
540            .unwrap()
541            .execute::<DecimalArray>(&mut ctx)
542            .unwrap();
543
544        assert_eq!(casted.precision(), 15);
545        assert_eq!(casted.scale(), 3);
546        assert_eq!(casted.values_type(), DecimalType::I64);
547        assert_eq!(casted.buffer::<i64>().as_ref(), &[1000]);
548    }
549
550    #[test]
551    fn cast_downcast_precision_succeeds_when_values_fit() {
552        let mut ctx = array_session().create_execution_ctx();
553        let array = DecimalArray::new(
554            buffer![100i64],
555            DecimalDType::new(18, 2),
556            Validity::NonNullable,
557        );
558
559        // Downcasting precision is allowed when every value fits.
560        let smaller_dtype = DType::Decimal(DecimalDType::new(10, 2), Nullability::NonNullable);
561        let casted = array
562            .into_array()
563            .cast(smaller_dtype)
564            .unwrap()
565            .execute::<DecimalArray>(&mut ctx)
566            .unwrap();
567
568        assert_eq!(casted.precision(), 10);
569        assert_eq!(casted.scale(), 2);
570        assert_eq!(casted.buffer::<i64>().as_ref(), &[100]);
571    }
572
573    #[test]
574    fn cast_downcast_precision_checks_values() {
575        let mut ctx = array_session().create_execution_ctx();
576        let array = DecimalArray::new(
577            buffer![1000i64],
578            DecimalDType::new(18, 0),
579            Validity::NonNullable,
580        );
581
582        let smaller_dtype = DType::Decimal(DecimalDType::new(3, 0), Nullability::NonNullable);
583        let result = array
584            .into_array()
585            .cast(smaller_dtype)
586            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()));
587
588        assert!(result.is_err());
589        assert!(
590            result
591                .unwrap_err()
592                .to_string()
593                .contains("does not fit in precision")
594        );
595    }
596
597    #[test]
598    fn cast_lower_scale_requires_exact_rescale() {
599        let mut ctx = array_session().create_execution_ctx();
600        let array = DecimalArray::new(
601            buffer![123456i64],
602            DecimalDType::new(10, 4),
603            Validity::NonNullable,
604        );
605
606        let lower_scale_dtype = DType::Decimal(DecimalDType::new(10, 2), Nullability::NonNullable);
607        let result = array
608            .into_array()
609            .cast(lower_scale_dtype)
610            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()));
611
612        assert!(result.is_err());
613        assert!(
614            result
615                .unwrap_err()
616                .to_string()
617                .contains("would lose precision")
618        );
619    }
620
621    #[test]
622    fn cast_lower_scale_ignores_null_lane_failures() {
623        let mut ctx = array_session().create_execution_ctx();
624        let array = DecimalArray::new(
625            buffer![100i64, 123456],
626            DecimalDType::new(10, 4),
627            Validity::from_iter([true, false]),
628        );
629
630        let lower_scale_dtype = DType::Decimal(DecimalDType::new(3, 2), Nullability::Nullable);
631        let casted = array
632            .into_array()
633            .cast(lower_scale_dtype)
634            .unwrap()
635            .execute::<DecimalArray>(&mut ctx)
636            .unwrap();
637
638        let mask = casted
639            .as_ref()
640            .validity()
641            .unwrap()
642            .execute_mask(
643                casted.as_ref().len(),
644                &mut array_session().create_execution_ctx(),
645            )
646            .unwrap();
647        assert!(mask.value(0));
648        assert!(!mask.value(1));
649        assert_eq!(casted.buffer::<i16>().as_ref()[0], 1);
650    }
651
652    #[test]
653    fn cast_upcast_precision_succeeds() {
654        let mut ctx = array_session().create_execution_ctx();
655        let array = DecimalArray::new(
656            buffer![100i32, 200, 300],
657            DecimalDType::new(10, 2),
658            Validity::NonNullable,
659        );
660
661        // Cast to higher precision with same scale - should succeed
662        let wider_dtype = DType::Decimal(DecimalDType::new(38, 2), Nullability::NonNullable);
663        let casted = array
664            .into_array()
665            .cast(wider_dtype)
666            .unwrap()
667            .execute::<DecimalArray>(&mut ctx)
668            .unwrap();
669
670        assert_eq!(casted.precision(), 38);
671        assert_eq!(casted.scale(), 2);
672        assert_eq!(casted.len(), 3);
673        // Should be stored in i128 now (precision 38 requires i128)
674        assert_eq!(casted.values_type(), DecimalType::I128);
675    }
676
677    #[test]
678    fn cast_widening_same_physical_type_is_zero_copy() {
679        let mut ctx = array_session().create_execution_ctx();
680        // Decimal(10,2) and Decimal(18,2) are both physically i64 with the same scale, so widening
681        // the precision must reuse the values buffer rather than allocate and re-scan it.
682        let array = DecimalArray::new(
683            buffer![100i64, 200, 300],
684            DecimalDType::new(10, 2),
685            Validity::NonNullable,
686        );
687        let src_ptr = array.buffer::<i64>().as_ptr();
688
689        let wider_dtype = DType::Decimal(DecimalDType::new(18, 2), Nullability::NonNullable);
690        let casted = array
691            .into_array()
692            .cast(wider_dtype)
693            .unwrap()
694            .execute::<DecimalArray>(&mut ctx)
695            .unwrap();
696
697        assert_eq!(casted.precision(), 18);
698        assert_eq!(casted.scale(), 2);
699        assert_eq!(casted.values_type(), DecimalType::I64);
700        assert_eq!(casted.buffer::<i64>().as_ref(), &[100, 200, 300]);
701        // The values buffer must be shared with the source (zero-copy), not reallocated.
702        assert_eq!(
703            casted.buffer::<i64>().as_ptr(),
704            src_ptr,
705            "precision-widening cast must reuse the source values buffer"
706        );
707    }
708
709    #[test]
710    fn cast_to_non_decimal_returns_err() {
711        let mut ctx = array_session().create_execution_ctx();
712        let array = DecimalArray::new(
713            buffer![100i32],
714            DecimalDType::new(10, 2),
715            Validity::NonNullable,
716        );
717
718        // Try to cast to non-decimal type - should fail since no kernel can handle it
719        let result = array
720            .into_array()
721            .cast(DType::Utf8(Nullability::NonNullable))
722            .and_then(|a| a.execute::<Canonical>(&mut ctx).map(|c| c.into_array()));
723
724        assert!(result.is_err());
725        assert!(
726            result
727                .unwrap_err()
728                .to_string()
729                .contains("No CastKernel to cast canonical array")
730        );
731    }
732
733    #[rstest]
734    #[case(DecimalArray::new(buffer![100i32, 200, 300], DecimalDType::new(10, 2), Validity::NonNullable))]
735    #[case(DecimalArray::new(buffer![10000i64, 20000, 30000], DecimalDType::new(18, 4), Validity::NonNullable))]
736    #[case(DecimalArray::from_option_iter([Some(100i32), None, Some(300)], DecimalDType::new(10, 2)))]
737    #[case(DecimalArray::new(buffer![42i32], DecimalDType::new(5, 1), Validity::NonNullable))]
738    fn test_cast_decimal_conformance(#[case] array: DecimalArray) {
739        test_cast_conformance(
740            &array.into_array(),
741            &mut array_session().create_execution_ctx(),
742        );
743    }
744
745    #[test]
746    fn upcast_decimal_values_i32_to_i64() {
747        let decimal_dtype = DecimalDType::new(10, 2);
748        let array = DecimalArray::new(
749            buffer![100i32, 200, 300],
750            decimal_dtype,
751            Validity::NonNullable,
752        );
753
754        assert_eq!(array.values_type(), DecimalType::I32);
755
756        let array = array.as_view();
757        let casted = upcast_decimal_values(array, DecimalType::I64).unwrap();
758
759        assert_eq!(casted.values_type(), DecimalType::I64);
760        assert_eq!(casted.decimal_dtype(), decimal_dtype);
761        assert_eq!(casted.len(), 3);
762
763        // Verify values are preserved
764        let buffer = casted.buffer::<i64>();
765        assert_eq!(buffer.as_ref(), &[100i64, 200, 300]);
766    }
767
768    #[test]
769    fn upcast_decimal_values_i64_to_i128() {
770        let decimal_dtype = DecimalDType::new(18, 4);
771        let array = DecimalArray::new(
772            buffer![10000i64, 20000, 30000],
773            decimal_dtype,
774            Validity::NonNullable,
775        );
776
777        let array = array.as_view();
778        let casted = upcast_decimal_values(array, DecimalType::I128).unwrap();
779
780        assert_eq!(casted.values_type(), DecimalType::I128);
781        assert_eq!(casted.decimal_dtype(), decimal_dtype);
782
783        let buffer = casted.buffer::<i128>();
784        assert_eq!(buffer.as_ref(), &[10000i128, 20000, 30000]);
785    }
786
787    #[test]
788    fn upcast_decimal_values_same_type_returns_clone() {
789        let decimal_dtype = DecimalDType::new(10, 2);
790        let array = DecimalArray::new(
791            buffer![100i32, 200, 300],
792            decimal_dtype,
793            Validity::NonNullable,
794        );
795
796        let array = array.as_view();
797        let casted = upcast_decimal_values(array, DecimalType::I32).unwrap();
798
799        assert_eq!(casted.values_type(), DecimalType::I32);
800        assert_eq!(casted.decimal_dtype(), decimal_dtype);
801    }
802
803    #[test]
804    fn upcast_decimal_values_with_nulls() {
805        let decimal_dtype = DecimalDType::new(10, 2);
806        let array = DecimalArray::from_option_iter([Some(100i32), None, Some(300)], decimal_dtype);
807
808        let array = array.as_view();
809        let casted = upcast_decimal_values(array, DecimalType::I64).unwrap();
810
811        assert_eq!(casted.values_type(), DecimalType::I64);
812        assert_eq!(casted.len(), 3);
813
814        // Check validity is preserved
815        let mask = casted
816            .as_ref()
817            .validity()
818            .unwrap()
819            .execute_mask(
820                casted.as_ref().len(),
821                &mut array_session().create_execution_ctx(),
822            )
823            .unwrap();
824        assert!(mask.value(0));
825        assert!(!mask.value(1));
826        assert!(mask.value(2));
827
828        // Check non-null values
829        let buffer = casted.buffer::<i64>();
830        assert_eq!(buffer[0], 100);
831        assert_eq!(buffer[2], 300);
832    }
833
834    #[test]
835    fn upcast_decimal_values_downcast_fails() {
836        let decimal_dtype = DecimalDType::new(18, 4);
837        let array = DecimalArray::new(
838            buffer![10000i64, 20000, 30000],
839            decimal_dtype,
840            Validity::NonNullable,
841        );
842
843        // Attempt to downcast from i64 to i32 should fail
844        let array = array.as_view();
845        let result = upcast_decimal_values(array, DecimalType::I32);
846        assert!(result.is_err());
847        assert!(
848            result
849                .unwrap_err()
850                .to_string()
851                .contains("Cannot downcast decimal values")
852        );
853    }
854
855    #[test]
856    fn cast_decimal_f64() {
857        let dtype = DecimalDType::new(6, 2);
858        let array = DecimalArray::new(buffer![125i32, 250, -375], dtype, Validity::NonNullable);
859        let target = DType::Primitive(PType::F64, Nullability::NonNullable);
860        let casted = array.into_array().cast(target.clone()).unwrap();
861        assert_eq!(casted.dtype(), &target);
862
863        let mut ctx = array_session().create_execution_ctx();
864        let primitive = casted
865            .execute::<Canonical>(&mut ctx)
866            .unwrap()
867            .into_primitive();
868        assert_eq!(primitive.to_buffer::<f64>().as_ref(), &[1.25, 2.5, -3.75]);
869    }
870
871    #[test]
872    fn cast_decimal_f64_null() {
873        let values = [Some(100i32), None, Some(-200)];
874        let array = DecimalArray::from_option_iter(values, DecimalDType::new(6, 2));
875        let target = DType::Primitive(PType::F64, Nullability::Nullable);
876        let casted = array.into_array().cast(target.clone()).unwrap();
877        assert_eq!(casted.dtype(), &target);
878
879        let mut ctx = array_session().create_execution_ctx();
880        let primitive = casted
881            .execute::<Canonical>(&mut ctx)
882            .unwrap()
883            .into_primitive();
884
885        assert!(primitive.is_valid(0, &mut ctx).unwrap());
886        assert!(!primitive.is_valid(1, &mut ctx).unwrap());
887        assert!(primitive.is_valid(2, &mut ctx).unwrap());
888
889        assert_eq!(
890            primitive.execute_scalar(0, &mut ctx).unwrap(),
891            Scalar::from(1f64)
892        );
893        assert_eq!(
894            primitive.execute_scalar(2, &mut ctx).unwrap(),
895            Scalar::from(-2f64)
896        );
897    }
898
899    #[test]
900    fn cast_decimal_f64_all_null() {
901        let dtype = DecimalDType::new(6, 2);
902        let buf = buffer![i32::MAX, i32::MIN, 12345];
903        let array = DecimalArray::new(buf, dtype, Validity::AllInvalid);
904        let target = DType::Primitive(PType::F64, Nullability::Nullable);
905        let casted = array.into_array().cast(target.clone()).unwrap();
906        assert_eq!(casted.dtype(), &target);
907
908        let primitive = casted
909            .execute::<Canonical>(&mut array_session().create_execution_ctx())
910            .unwrap()
911            .into_primitive();
912        let mut ctx = array_session().create_execution_ctx();
913        for i in 0..2 {
914            assert!(!primitive.is_valid(i, &mut ctx).unwrap());
915        }
916        assert_eq!(primitive.to_buffer::<f64>().as_ref(), &[0.0, 0.0, 0.0]);
917    }
918}