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