Skip to main content

vortex_array/arrays/primitive/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::NumCast;
7use vortex_buffer::Buffer;
8use vortex_buffer::BufferMut;
9use vortex_compute::lane_kernels::IndexedSinkExt;
10use vortex_compute::lane_kernels::IndexedSourceExt;
11use vortex_compute::lane_kernels::ReinterpretSink;
12use vortex_error::VortexError;
13use vortex_error::VortexResult;
14use vortex_error::vortex_bail;
15use vortex_error::vortex_err;
16use vortex_mask::Mask;
17
18use crate::ArrayRef;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::aggregate_fn;
22use crate::array::ArrayView;
23use crate::arrays::DecimalArray;
24use crate::arrays::Primitive;
25use crate::arrays::PrimitiveArray;
26use crate::arrays::primitive::PrimitiveArrayExt;
27use crate::dtype::BigCast;
28use crate::dtype::DType;
29use crate::dtype::DecimalDType;
30use crate::dtype::DecimalType;
31use crate::dtype::IntegerPType;
32use crate::dtype::NativeDecimalType;
33use crate::dtype::NativePType;
34use crate::dtype::Nullability;
35use crate::dtype::PType;
36use crate::dtype::ToI256;
37use crate::dtype::i256;
38use crate::expr::stats::Stat;
39use crate::expr::stats::StatsProvider;
40use crate::match_each_decimal_value_type;
41use crate::match_each_integer_ptype;
42use crate::match_each_native_ptype;
43use crate::match_each_signed_integer_ptype;
44use crate::scalar::DecimalValue;
45use crate::scalar_fn::fns::cast::CastKernel;
46use crate::scalar_fn::fns::cast::CastReduce;
47use crate::validity::Validity;
48
49impl CastReduce for Primitive {
50    fn cast(array: ArrayView<'_, Primitive>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
51        // Only the same ptype is reducible without execution; type changes need the kernel
52        // to verify values fit in the target range.
53        let DType::Primitive(new_ptype, new_nullability) = dtype else {
54            return Ok(None);
55        };
56        if *new_ptype != array.ptype() {
57            return Ok(None);
58        }
59
60        let Some(new_validity) = array
61            .validity()?
62            .trivially_cast_nullability(*new_nullability, array.len())?
63        else {
64            return Ok(None);
65        };
66
67        // SAFETY: validity and data buffer still have same length.
68        Ok(Some(unsafe {
69            PrimitiveArray::new_unchecked_from_handle(
70                array.buffer_handle().clone(),
71                array.ptype(),
72                new_validity,
73            )
74            .into_array()
75        }))
76    }
77}
78
79impl CastKernel for Primitive {
80    fn cast(
81        array: ArrayView<'_, Primitive>,
82        dtype: &DType,
83        ctx: &mut ExecutionCtx,
84    ) -> VortexResult<Option<ArrayRef>> {
85        if let DType::Decimal(decimal_dtype, nullability) = dtype {
86            return cast_to_decimal(array, *decimal_dtype, *nullability, ctx).map(Some);
87        }
88        let DType::Primitive(new_ptype, new_nullability) = dtype else {
89            return Ok(None);
90        };
91        let (new_ptype, new_nullability) = (*new_ptype, *new_nullability);
92        let src_ptype = array.ptype();
93
94        let new_validity = array
95            .validity()?
96            .cast_nullability(new_nullability, array.len(), ctx)?;
97
98        // Same bit representation: either the same ptype (only the nullability changed) or two
99        // same-width integers (identical layout under 2's complement). The only non-trivial case
100        // is the sign change between same-width ints, which still needs a value-range check.
101        let same_rep = src_ptype == new_ptype
102            || (src_ptype.is_int()
103                && new_ptype.is_int()
104                && src_ptype.byte_width() == new_ptype.byte_width());
105        if same_rep {
106            if !values_fit_in(array, new_ptype, ctx, true) {
107                vortex_bail!(
108                    Compute: "Cannot cast {} to {} — values exceed target range",
109                    src_ptype, new_ptype,
110                );
111            }
112            return Ok(Some(reinterpret(array, new_ptype, new_validity)));
113        }
114
115        // Different bit rep: cast each element. `cast_values` picks a pure or checked loop based
116        // on whether the conversion is statically infallible.
117        Ok(Some(match_each_native_ptype!(new_ptype, |T| {
118            match_each_native_ptype!(src_ptype, |F| {
119                cast_values::<F, T>(array, new_validity, ctx)?
120            })
121        })))
122    }
123}
124
125fn cast_to_decimal(
126    array: ArrayView<'_, Primitive>,
127    decimal_dtype: DecimalDType,
128    nullability: Nullability,
129    ctx: &mut ExecutionCtx,
130) -> VortexResult<ArrayRef> {
131    if !array.ptype().is_int() {
132        vortex_bail!(
133            Compute: "Cannot cast floating primitive {} to decimal {}",
134            array.ptype(), decimal_dtype
135        );
136    }
137
138    let source_validity = array.validity()?;
139    let validity = source_validity
140        .clone()
141        .cast_nullability(nullability, array.len(), ctx)?;
142    let values_type = DecimalType::smallest_decimal_value_type(&decimal_dtype);
143
144    if decimal_dtype.scale() == 0
145        && signed_primitive_decimal_type(array.ptype()) == Some(values_type)
146    {
147        return match_each_signed_integer_ptype!(array.ptype(), |S| {
148            cast_unscaled_same_width_signed_integer_to_decimal::<S>(
149                array,
150                decimal_dtype,
151                validity,
152                &source_validity,
153                ctx,
154            )
155        });
156    }
157
158    let valid_values = source_validity.execute_mask(array.len(), ctx)?;
159    match_each_integer_ptype!(array.ptype(), |S| {
160        match_each_decimal_value_type!(values_type, |T| {
161            cast_integer_values_to_decimal::<S, T>(array, decimal_dtype, validity, &valid_values)
162        })
163    })
164}
165
166fn cast_unscaled_same_width_signed_integer_to_decimal<S>(
167    array: ArrayView<'_, Primitive>,
168    decimal_dtype: DecimalDType,
169    validity: Validity,
170    source_validity: &Validity,
171    ctx: &mut ExecutionCtx,
172) -> VortexResult<ArrayRef>
173where
174    S: IntegerPType + NativeDecimalType + ToI256,
175{
176    let values = array.as_slice::<S>();
177    let target_dtype = DType::Decimal(decimal_dtype, Nullability::NonNullable);
178    if !cached_values_fit_in(array, &target_dtype).unwrap_or(false) {
179        let valid_values = source_validity.execute_mask(array.len(), ctx)?;
180        validate_unscaled_signed_integer_values_to_decimal(values, decimal_dtype, &valid_values)
181            .map_err(|idx| primitive_to_decimal_cast_error(values[idx], decimal_dtype))?;
182    }
183
184    // SAFETY: `S::DECIMAL_TYPE` has the same physical representation as the source ptype, and
185    // either exact min/max statistics or the validation above prove every valid value fits.
186    Ok(unsafe {
187        DecimalArray::new_unchecked_handle(
188            array.buffer_handle().clone(),
189            S::DECIMAL_TYPE,
190            decimal_dtype,
191            validity,
192        )
193        .into_array()
194    })
195}
196
197fn validate_unscaled_signed_integer_values_to_decimal<S>(
198    values: &[S],
199    decimal_dtype: DecimalDType,
200    valid_values: &Mask,
201) -> Result<(), usize>
202where
203    S: NativeDecimalType,
204{
205    let fits = |value| decimal_value_fits_precision(value, decimal_dtype);
206    match valid_values {
207        Mask::AllTrue(_) => values
208            .iter()
209            .position(|&value| !fits(value))
210            .map_or(Ok(()), Err),
211        Mask::AllFalse(_) => Ok(()),
212        Mask::Values(mask) => {
213            let mut first_failure = None;
214            mask.bit_buffer().for_each_set_index(|idx| {
215                if first_failure.is_none() && !fits(values[idx]) {
216                    first_failure = Some(idx);
217                }
218            });
219            first_failure.map_or(Ok(()), Err)
220        }
221    }
222}
223
224fn cast_integer_values_to_decimal<S, T>(
225    array: ArrayView<'_, Primitive>,
226    decimal_dtype: DecimalDType,
227    validity: Validity,
228    valid_values: &Mask,
229) -> VortexResult<ArrayRef>
230where
231    S: IntegerPType + ToI256,
232    T: NativeDecimalType + CheckedMul,
233{
234    let scale = decimal_dtype.scale();
235    let buffer = if scale == 0 {
236        cast_unscaled_integer_values_to_decimal::<S, T>(array, decimal_dtype, valid_values)?
237    } else if scale > 0 {
238        cast_scaled_up_integer_values_to_decimal::<S, T>(array, decimal_dtype, valid_values)?
239    } else {
240        cast_scaled_down_integer_values_to_decimal::<S, T>(array, decimal_dtype, valid_values)?
241    };
242
243    Ok(DecimalArray::new(buffer, decimal_dtype, validity).into_array())
244}
245
246fn cast_unscaled_integer_values_to_decimal<S, T>(
247    array: ArrayView<'_, Primitive>,
248    decimal_dtype: DecimalDType,
249    valid_values: &Mask,
250) -> VortexResult<Buffer<T>>
251where
252    S: IntegerPType + ToI256,
253    T: NativeDecimalType,
254{
255    let values = array.as_slice::<S>();
256    cast_integer_values_to_decimal_buffer(values, decimal_dtype, valid_values, |value| {
257        let value = <T as BigCast>::from(value)?;
258        decimal_value_fits_precision(value, decimal_dtype).then_some(value)
259    })
260}
261
262fn cast_scaled_up_integer_values_to_decimal<S, T>(
263    array: ArrayView<'_, Primitive>,
264    decimal_dtype: DecimalDType,
265    valid_values: &Mask,
266) -> VortexResult<Buffer<T>>
267where
268    S: IntegerPType + ToI256,
269    T: NativeDecimalType + CheckedMul,
270{
271    let values = array.as_slice::<S>();
272    let scale_factor = decimal_scale_factor::<T>(decimal_dtype.scale())?;
273    cast_integer_values_to_decimal_buffer(values, decimal_dtype, valid_values, |value| {
274        let value = <T as BigCast>::from(value)?;
275        let value = value.checked_mul(&scale_factor)?;
276        decimal_value_fits_precision(value, decimal_dtype).then_some(value)
277    })
278}
279
280fn cast_scaled_down_integer_values_to_decimal<S, T>(
281    array: ArrayView<'_, Primitive>,
282    decimal_dtype: DecimalDType,
283    valid_values: &Mask,
284) -> VortexResult<Buffer<T>>
285where
286    S: IntegerPType + ToI256,
287    T: NativeDecimalType,
288{
289    let values = array.as_slice::<S>();
290    if decimal_dtype.scale().unsigned_abs() >= primitive_max_decimal_digits(array.ptype()) {
291        // The scale factor exceeds every source value, so only zero can be exactly rescaled.
292        return cast_integer_values_to_decimal_buffer(
293            values,
294            decimal_dtype,
295            valid_values,
296            |value| (value == S::default()).then_some(T::default()),
297        );
298    }
299
300    // Scaling down can shrink a value into a narrower target type, so first select the smallest
301    // signed carrier that can represent both the source and target.
302    let carrier_type = primitive_decimal_carrier_type(array.ptype()).max(T::DECIMAL_TYPE);
303    match_each_decimal_value_type!(carrier_type, |W| {
304        let scale_factor = decimal_scale_factor::<W>(decimal_dtype.scale())?;
305        cast_integer_values_to_decimal_buffer(values, decimal_dtype, valid_values, |value| {
306            let value = <W as BigCast>::from(value)?;
307            let quotient = value / scale_factor;
308            let value = (quotient * scale_factor == value).then_some(quotient)?;
309            let value = <T as BigCast>::from(value)?;
310            decimal_value_fits_precision(value, decimal_dtype).then_some(value)
311        })
312    })
313}
314
315fn cast_integer_values_to_decimal_buffer<S, T>(
316    values: &[S],
317    decimal_dtype: DecimalDType,
318    valid_values: &Mask,
319    cast: impl Fn(S) -> Option<T>,
320) -> VortexResult<Buffer<T>>
321where
322    S: IntegerPType + ToI256,
323    T: NativeDecimalType,
324{
325    cast_primitive_to_decimal_buffer(values, valid_values, cast)
326        .map_err(|idx| primitive_to_decimal_cast_error(values[idx], decimal_dtype))
327}
328
329/// Decimal physical type with the same representation, when `ptype` is signed.
330fn signed_primitive_decimal_type(ptype: PType) -> Option<DecimalType> {
331    match ptype {
332        PType::I8 => Some(DecimalType::I8),
333        PType::I16 => Some(DecimalType::I16),
334        PType::I32 => Some(DecimalType::I32),
335        PType::I64 => Some(DecimalType::I64),
336        PType::U8 | PType::U16 | PType::U32 | PType::U64 => None,
337        PType::F16 | PType::F32 | PType::F64 => {
338            unreachable!("floating primitives are rejected before selecting a decimal type")
339        }
340    }
341}
342
343/// The smallest signed decimal physical type that represents every `ptype` value.
344fn primitive_decimal_carrier_type(ptype: PType) -> DecimalType {
345    match ptype {
346        PType::I8 => DecimalType::I8,
347        PType::U8 | PType::I16 => DecimalType::I16,
348        PType::U16 | PType::I32 => DecimalType::I32,
349        PType::U32 | PType::I64 => DecimalType::I64,
350        PType::U64 => DecimalType::I128,
351        PType::F16 | PType::F32 | PType::F64 => {
352            unreachable!("floating primitives are rejected before selecting a decimal carrier")
353        }
354    }
355}
356
357/// Maximum decimal digits in `ptype`; scales at least this large can exactly rescale only zero.
358fn primitive_max_decimal_digits(ptype: PType) -> u8 {
359    match ptype {
360        PType::U8 | PType::I8 => 3,
361        PType::U16 | PType::I16 => 5,
362        PType::U32 | PType::I32 => 10,
363        PType::U64 => 20,
364        PType::I64 => 19,
365        PType::F16 | PType::F32 | PType::F64 => {
366            unreachable!("floating primitives are rejected before inspecting decimal digits")
367        }
368    }
369}
370
371fn decimal_scale_factor<T>(scale: i8) -> VortexResult<T>
372where
373    T: NativeDecimalType + CheckedMul,
374{
375    let exponent = if scale > 0 {
376        scale as u32
377    } else {
378        (-(scale as i16)) as u32
379    };
380    let ten = <T as BigCast>::from(10i8).ok_or_else(|| scale_factor_error(scale))?;
381    let mut factor = <T as BigCast>::from(1i8).ok_or_else(|| scale_factor_error(scale))?;
382    for _ in 0..exponent {
383        factor = factor
384            .checked_mul(&ten)
385            .ok_or_else(|| scale_factor_error(scale))?;
386    }
387    Ok(factor)
388}
389
390#[cold]
391#[inline(never)]
392fn scale_factor_error(scale: i8) -> VortexError {
393    vortex_err!(Compute: "Cannot create decimal scale factor for scale {scale}")
394}
395
396fn decimal_value_fits_precision<T: NativeDecimalType>(
397    value: T,
398    decimal_dtype: DecimalDType,
399) -> bool {
400    let precision = decimal_dtype.precision() as usize;
401    value >= T::MIN_BY_PRECISION[precision] && value <= T::MAX_BY_PRECISION[precision]
402}
403
404fn cast_primitive_to_decimal_buffer<S, T>(
405    values: &[S],
406    valid_values: &Mask,
407    cast: impl Fn(S) -> Option<T>,
408) -> Result<Buffer<T>, usize>
409where
410    S: NativePType,
411    T: NativeDecimalType,
412{
413    if matches!(valid_values, Mask::AllFalse(_)) {
414        return Ok(BufferMut::<T>::zeroed(values.len()).freeze());
415    }
416
417    let mut buffer = BufferMut::<T>::with_capacity(values.len());
418    match valid_values {
419        Mask::AllTrue(_) => {
420            values.try_map_into(&mut buffer.spare_capacity_mut()[..values.len()], &cast)?;
421        }
422        Mask::Values(mask) => {
423            values.try_map_masked_into(
424                mask.bit_buffer(),
425                &mut buffer.spare_capacity_mut()[..values.len()],
426                &cast,
427            )?;
428        }
429        Mask::AllFalse(_) => unreachable!("all-null values are handled before allocating"),
430    }
431    // SAFETY: the selected map kernel initialized every lane before returning Ok.
432    unsafe { buffer.set_len(values.len()) };
433    Ok(buffer.freeze())
434}
435
436#[cold]
437#[inline(never)]
438fn primitive_to_decimal_cast_error<S>(value: S, decimal_dtype: DecimalDType) -> VortexError
439where
440    S: IntegerPType + ToI256,
441{
442    let Some(value) = <i256 as BigCast>::from(value) else {
443        return vortex_err!(
444            Compute: "primitive value cannot be represented while casting to {}",
445            decimal_dtype
446        );
447    };
448
449    match DecimalValue::rescale_i256(value, 0, decimal_dtype.scale())
450        .and_then(|value| DecimalValue::try_from_i256(value, decimal_dtype))
451    {
452        Err(error) => error,
453        Ok(_) => {
454            debug_assert!(
455                false,
456                "primitive-to-decimal fast path rejected a value that the scalar cast accepts"
457            );
458            vortex_err!(
459                Compute: "primitive value cannot be represented while casting to {}",
460                decimal_dtype
461            )
462        }
463    }
464}
465
466/// Cast Primitive values from `F` to `T`.
467fn cast_values<F, T>(
468    array: ArrayView<'_, Primitive>,
469    new_validity: Validity,
470    ctx: &mut ExecutionCtx,
471) -> VortexResult<ArrayRef>
472where
473    F: NativePType + AsPrimitive<T>,
474    T: NativePType,
475{
476    let overflow = || {
477        vortex_err!(
478            Compute: "Cannot cast {} to {} — value exceeds target range",
479            F::PTYPE, T::PTYPE,
480        )
481    };
482
483    // Returns `true` if every value of `from` is representable in `to` without loss.
484    fn casts_losslessly_to(from: PType, to: PType) -> bool {
485        if from == to {
486            return true;
487        }
488        if (from.is_unsigned_int() && to.is_unsigned_int())
489            || (from.is_signed_int() && to.is_signed_int())
490            || (from.is_float() && to.is_float())
491        {
492            return from.byte_width() <= to.byte_width();
493        }
494        if from.is_unsigned_int() && to.is_signed_int() {
495            return from.byte_width() < to.byte_width();
496        }
497        if from.is_int() && to.is_float() {
498            let minimum_float_width = match from.byte_width() {
499                1 => 2,
500                2 => 4,
501                4 => 8,
502                _ => return false,
503            };
504            return to.byte_width() >= minimum_float_width;
505        }
506        false
507    }
508
509    // Skip the fallible kernel when type widening or (cached) min/max prove every value fits.
510    let target_dtype = DType::Primitive(T::PTYPE, Nullability::NonNullable);
511    let infallible = casts_losslessly_to(F::PTYPE, T::PTYPE)
512        || cached_values_fit_in(array, &target_dtype).unwrap_or(false);
513
514    let len = array.len();
515
516    // If F and T have the same byte width, try to take unique ownership of the buffer.
517    let same_bit_width = F::PTYPE.byte_width() == T::PTYPE.byte_width();
518    let owned: Option<BufferMut<F>> = same_bit_width
519        .then(|| array.into_owned().try_into_buffer_mut::<F>().ok())
520        .flatten();
521    let values: &[F] = array.as_slice::<F>();
522
523    if infallible {
524        return match owned {
525            Some(mut buf) => {
526                ReinterpretSink::<F, T>::new(buf.as_mut_slice()).map_into_in_place(|v: F| v.as_());
527                // SAFETY: same size + alignment for NativePType
528                let result: BufferMut<T> = unsafe { buf.transmute::<T>() };
529                Ok(PrimitiveArray::new(result.freeze(), new_validity).into_array())
530            }
531            None => {
532                let mut buffer = BufferMut::<T>::with_capacity(len);
533                values.map_into(&mut buffer.spare_capacity_mut()[..len], |v| v.as_());
534                // SAFETY: map_into initializes every lane.
535                unsafe { buffer.set_len(len) };
536                Ok(PrimitiveArray::new(buffer.freeze(), new_validity).into_array())
537            }
538        };
539    }
540
541    let mask = array.validity()?.execute_mask(len, ctx)?;
542
543    let buffer: Buffer<T> = match (&mask, owned) {
544        (Mask::AllTrue(_), Some(mut buf)) => {
545            ReinterpretSink::<F, T>::new(buf.as_mut_slice())
546                .try_map_in_place(|v: F| <T as NumCast>::from(v))
547                .map_err(|_| overflow())?;
548            // SAFETY: same size + alignment for NativePType
549            let result: BufferMut<T> = unsafe { buf.transmute::<T>() };
550            result.freeze()
551        }
552        (Mask::AllTrue(_), None) => {
553            let mut buffer = BufferMut::<T>::with_capacity(len);
554            values
555                .try_map_into(&mut buffer.spare_capacity_mut()[..len], |v| {
556                    <T as NumCast>::from(v)
557                })
558                .map_err(|_| overflow())?;
559            // SAFETY: initialized every lane.
560            unsafe { buffer.set_len(len) };
561            buffer.freeze()
562        }
563        (Mask::AllFalse(_), _) => BufferMut::<T>::zeroed(len).freeze(),
564        (Mask::Values(m), Some(mut buf)) => {
565            ReinterpretSink::<F, T>::new(buf.as_mut_slice())
566                .try_map_masked_in_place(m.bit_buffer(), |v: F| <T as NumCast>::from(v))
567                .map_err(|_| overflow())?;
568            // SAFETY: same size + alignment for NativePType
569            let result: BufferMut<T> = unsafe { buf.transmute::<T>() };
570            result.freeze()
571        }
572        (Mask::Values(m), None) => {
573            let mut buffer = BufferMut::<T>::with_capacity(len);
574            values
575                .try_map_masked_into(
576                    m.bit_buffer(),
577                    &mut buffer.spare_capacity_mut()[..len],
578                    |v| <T as NumCast>::from(v),
579                )
580                .map_err(|_| overflow())?;
581            // SAFETY: initialized every lane.
582            unsafe { buffer.set_len(len) };
583            buffer.freeze()
584        }
585    };
586
587    Ok(PrimitiveArray::new(buffer, new_validity).into_array())
588}
589
590fn reinterpret(
591    array: ArrayView<'_, Primitive>,
592    new_ptype: PType,
593    new_validity: Validity,
594) -> ArrayRef {
595    // SAFETY: caller has verified the bit representation is compatible and that validity length
596    // still matches the buffer length.
597    unsafe {
598        PrimitiveArray::new_unchecked_from_handle(
599            array.buffer_handle().clone(),
600            new_ptype,
601            new_validity,
602        )
603    }
604    .into_array()
605}
606
607/// Returns `true` if all valid values in `array` are representable as `target_ptype`.
608///
609/// Cached min/max statistics are consulted first. If either bound is missing, the function either
610/// computes them with a single pass (when `compute` is `true`) or returns `false` so the caller
611/// can fall back to a slower path (when `compute` is `false`).
612fn values_fit_in(
613    array: ArrayView<'_, Primitive>,
614    target_ptype: PType,
615    ctx: &mut ExecutionCtx,
616    compute: bool,
617) -> bool {
618    let target_dtype = DType::Primitive(target_ptype, Nullability::NonNullable);
619    if let Some(fits) = cached_values_fit_in(array, &target_dtype) {
620        return fits;
621    }
622    if !compute {
623        return false;
624    }
625    aggregate_fn::fns::min_max::min_max(
626        array.array(),
627        ctx,
628        aggregate_fn::NumericalAggregateOpts::default(),
629    )
630    .ok()
631    .flatten()
632    .is_none_or(|mm| mm.min.cast(&target_dtype).is_ok() && mm.max.cast(&target_dtype).is_ok())
633}
634
635/// Cached-only check: returns `Some(fits)` if both `Min` and `Max` are present as `Exact` in the
636/// stats cache, otherwise `None`.
637fn cached_values_fit_in(array: ArrayView<'_, Primitive>, target_dtype: &DType) -> Option<bool> {
638    let stats = array.array().statistics();
639    let min = stats.get(Stat::Min).as_exact()?;
640    let max = stats.get(Stat::Max).as_exact()?;
641    Some(min.cast(target_dtype).is_ok() && max.cast(target_dtype).is_ok())
642}
643
644#[cfg(test)]
645mod test {
646    use rstest::rstest;
647    use vortex_buffer::BitBuffer;
648    use vortex_buffer::buffer;
649    use vortex_error::VortexError;
650    use vortex_error::VortexResult;
651    use vortex_mask::Mask;
652
653    use crate::ArrayRef;
654    use crate::IntoArray;
655    use crate::VortexSessionExecute;
656    use crate::array_session;
657    use crate::arrays::DecimalArray;
658    use crate::arrays::PrimitiveArray;
659    use crate::assert_arrays_eq;
660    use crate::builtins::ArrayBuiltins;
661    use crate::compute::conformance::cast::test_cast_conformance;
662    use crate::dtype::DType;
663    use crate::dtype::DecimalDType;
664    use crate::dtype::DecimalType;
665    use crate::dtype::Nullability;
666    use crate::dtype::PType;
667    use crate::dtype::i256;
668    use crate::expr::stats::Stat;
669    use crate::validity::Validity;
670
671    #[test]
672    fn cast_u32_u8() {
673        let mut ctx = array_session().create_execution_ctx();
674        let arr = buffer![0u32, 10, 200].into_array();
675
676        // cast from u32 to u8
677        let p = arr
678            .cast(PType::U8.into())
679            .unwrap()
680            .execute::<PrimitiveArray>(&mut ctx)
681            .unwrap();
682        assert_arrays_eq!(p, PrimitiveArray::from_iter([0u8, 10, 200]), &mut ctx);
683        assert!(matches!(p.validity(), Ok(Validity::NonNullable)));
684
685        // to nullable
686        let p = p
687            .into_array()
688            .cast(DType::Primitive(PType::U8, Nullability::Nullable))
689            .unwrap()
690            .execute::<PrimitiveArray>(&mut ctx)
691            .unwrap();
692        assert_arrays_eq!(
693            p,
694            PrimitiveArray::new(buffer![0u8, 10, 200], Validity::AllValid),
695            &mut ctx
696        );
697        assert!(matches!(p.validity(), Ok(Validity::AllValid)));
698
699        // back to non-nullable
700        let p = p
701            .into_array()
702            .cast(DType::Primitive(PType::U8, Nullability::NonNullable))
703            .unwrap()
704            .execute::<PrimitiveArray>(&mut ctx)
705            .unwrap();
706        assert_arrays_eq!(p, PrimitiveArray::from_iter([0u8, 10, 200]), &mut ctx);
707        assert!(matches!(p.validity(), Ok(Validity::NonNullable)));
708
709        // to nullable u32
710        let p = p
711            .into_array()
712            .cast(DType::Primitive(PType::U32, Nullability::Nullable))
713            .unwrap()
714            .execute::<PrimitiveArray>(&mut ctx)
715            .unwrap();
716        assert_arrays_eq!(
717            p,
718            PrimitiveArray::new(buffer![0u32, 10, 200], Validity::AllValid),
719            &mut ctx
720        );
721        assert!(matches!(p.validity(), Ok(Validity::AllValid)));
722
723        // to non-nullable u8
724        let p = p
725            .into_array()
726            .cast(DType::Primitive(PType::U8, Nullability::NonNullable))
727            .unwrap()
728            .execute::<PrimitiveArray>(&mut ctx)
729            .unwrap();
730        assert_arrays_eq!(p, PrimitiveArray::from_iter([0u8, 10, 200]), &mut ctx);
731        assert!(matches!(p.validity(), Ok(Validity::NonNullable)));
732    }
733
734    #[test]
735    fn cast_u32_f32() {
736        let mut ctx = array_session().create_execution_ctx();
737        let arr = buffer![0u32, 10, 200].into_array();
738        let u8arr = arr
739            .cast(PType::F32.into())
740            .unwrap()
741            .execute::<PrimitiveArray>(&mut ctx)
742            .unwrap();
743        assert_arrays_eq!(
744            u8arr,
745            PrimitiveArray::from_iter([0.0f32, 10., 200.]),
746            &mut ctx
747        );
748    }
749
750    #[test]
751    fn cast_integer_to_decimal_rescales() -> VortexResult<()> {
752        let mut ctx = array_session().create_execution_ctx();
753        let decimal_dtype = DecimalDType::new(5, 2);
754        let casted = PrimitiveArray::from_iter([42i32, -7])
755            .into_array()
756            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
757            .execute::<DecimalArray>(&mut ctx)?;
758
759        assert_eq!(
760            casted.dtype(),
761            &DType::Decimal(decimal_dtype, Nullability::NonNullable)
762        );
763        assert_eq!(casted.values_type(), DecimalType::I32);
764        assert_eq!(casted.buffer::<i32>().as_ref(), &[4_200, -700]);
765        Ok(())
766    }
767
768    #[test]
769    fn cast_same_width_signed_integer_to_decimal_reuses_buffer() -> VortexResult<()> {
770        let mut ctx = array_session().create_execution_ctx();
771        let source = PrimitiveArray::from_iter([42i32, -7]);
772        let source_ptr = source.as_slice::<i32>().as_ptr();
773        let casted = source
774            .into_array()
775            .cast(DType::Decimal(
776                DecimalDType::new(9, 0),
777                Nullability::NonNullable,
778            ))?
779            .execute::<DecimalArray>(&mut ctx)?;
780
781        assert_eq!(casted.buffer::<i32>().as_ptr(), source_ptr);
782        assert_eq!(casted.buffer::<i32>().as_ref(), &[42, -7]);
783        Ok(())
784    }
785
786    #[test]
787    fn cast_same_width_signed_integer_to_decimal_reuses_buffer_with_cached_bounds()
788    -> VortexResult<()> {
789        let mut ctx = array_session().create_execution_ctx();
790        let source = PrimitiveArray::from_iter([42i32, -7]);
791        let source_ptr = source.as_slice::<i32>().as_ptr();
792        let source = source.into_array();
793        source
794            .statistics()
795            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
796        let casted = source
797            .cast(DType::Decimal(
798                DecimalDType::new(9, 0),
799                Nullability::NonNullable,
800            ))?
801            .execute::<DecimalArray>(&mut ctx)?;
802
803        assert_eq!(casted.buffer::<i32>().as_ptr(), source_ptr);
804        Ok(())
805    }
806
807    #[test]
808    fn cast_same_width_signed_integer_to_decimal_ignores_out_of_range_nulls() -> VortexResult<()> {
809        let mut ctx = array_session().create_execution_ctx();
810        let source = PrimitiveArray::new(buffer![i32::MAX, 42], Validity::from_iter([false, true]));
811        let source_ptr = source.as_slice::<i32>().as_ptr();
812        let casted = source
813            .into_array()
814            .cast(DType::Decimal(
815                DecimalDType::new(9, 0),
816                Nullability::Nullable,
817            ))?
818            .execute::<DecimalArray>(&mut ctx)?;
819
820        assert_eq!(casted.buffer::<i32>().as_ptr(), source_ptr);
821        assert_eq!(
822            casted.validity()?.execute_mask(casted.len(), &mut ctx)?,
823            Mask::from(BitBuffer::from(vec![false, true]))
824        );
825        Ok(())
826    }
827
828    #[test]
829    fn cast_same_width_signed_integer_to_decimal_checks_precision() -> VortexResult<()> {
830        let mut ctx = array_session().create_execution_ctx();
831        let casted = PrimitiveArray::from_iter([i32::MAX])
832            .into_array()
833            .cast(DType::Decimal(
834                DecimalDType::new(9, 0),
835                Nullability::NonNullable,
836            ))?;
837
838        let error = casted.execute::<DecimalArray>(&mut ctx).unwrap_err();
839        assert!(error.to_string().contains("does not fit in precision"));
840        Ok(())
841    }
842
843    #[test]
844    fn cast_u64_to_decimal() -> VortexResult<()> {
845        let mut ctx = array_session().create_execution_ctx();
846        let decimal_dtype = DecimalDType::new(20, 0);
847        let casted = PrimitiveArray::from_iter([u64::MAX])
848            .into_array()
849            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
850            .execute::<DecimalArray>(&mut ctx)?;
851
852        assert_eq!(casted.values_type(), DecimalType::I128);
853        assert_eq!(casted.buffer::<i128>().as_ref(), &[i128::from(u64::MAX)]);
854        Ok(())
855    }
856
857    #[test]
858    fn cast_integer_to_decimal_reports_scale_up_overflow() -> VortexResult<()> {
859        let mut ctx = array_session().create_execution_ctx();
860        let dtype = DType::Decimal(DecimalDType::new(38, 20), Nullability::NonNullable);
861        let casted = PrimitiveArray::from_iter([u64::MAX])
862            .into_array()
863            .cast(dtype)?;
864        let actual = casted.execute::<DecimalArray>(&mut ctx).unwrap_err();
865
866        assert!(
867            actual
868                .to_string()
869                .contains("does not fit in precision of decimal(38,20)")
870        );
871        Ok(())
872    }
873
874    #[test]
875    fn cast_u8_to_negative_scale_decimal() -> VortexResult<()> {
876        let mut ctx = array_session().create_execution_ctx();
877        let decimal_dtype = DecimalDType::new(2, -2);
878        let casted = PrimitiveArray::from_iter([200u8])
879            .into_array()
880            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
881            .execute::<DecimalArray>(&mut ctx)?;
882
883        assert_eq!(casted.values_type(), DecimalType::I8);
884        assert_eq!(casted.buffer::<i8>().as_ref(), &[2]);
885        Ok(())
886    }
887
888    #[test]
889    fn cast_u64_to_negative_scale_decimal() -> VortexResult<()> {
890        let mut ctx = array_session().create_execution_ctx();
891        let decimal_dtype = DecimalDType::new(1, -19);
892        let casted = PrimitiveArray::from_iter([10_000_000_000_000_000_000u64])
893            .into_array()
894            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
895            .execute::<DecimalArray>(&mut ctx)?;
896
897        assert_eq!(casted.values_type(), DecimalType::I8);
898        assert_eq!(casted.buffer::<i8>().as_ref(), &[1]);
899        Ok(())
900    }
901
902    #[test]
903    fn cast_integer_to_i256_decimal() -> VortexResult<()> {
904        let mut ctx = array_session().create_execution_ctx();
905        let decimal_dtype = DecimalDType::new(39, 2);
906        let casted = PrimitiveArray::from_iter([42i64])
907            .into_array()
908            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
909            .execute::<DecimalArray>(&mut ctx)?;
910
911        assert_eq!(casted.values_type(), DecimalType::I256);
912        assert_eq!(casted.buffer::<i256>().as_ref(), &[i256::from_i128(4_200)]);
913        Ok(())
914    }
915
916    #[test]
917    fn cast_all_null_integer_to_decimal() -> VortexResult<()> {
918        let mut ctx = array_session().create_execution_ctx();
919        let decimal_dtype = DecimalDType::new(39, 2);
920        let casted = PrimitiveArray::new(buffer![i64::MAX, i64::MIN], Validity::AllInvalid)
921            .into_array()
922            .cast(DType::Decimal(decimal_dtype, Nullability::Nullable))?
923            .execute::<DecimalArray>(&mut ctx)?;
924
925        assert!(matches!(casted.validity(), Ok(Validity::AllInvalid)));
926        assert_eq!(casted.buffer::<i256>().as_ref(), &[i256::ZERO, i256::ZERO]);
927        Ok(())
928    }
929
930    #[test]
931    fn cast_integer_to_negative_scale_decimal_requires_exact_rescale() -> VortexResult<()> {
932        let mut ctx = array_session().create_execution_ctx();
933        let decimal_dtype = DecimalDType::new(3, -2);
934        let casted = PrimitiveArray::from_iter([1_200i32, -500])
935            .into_array()
936            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
937            .execute::<DecimalArray>(&mut ctx)?;
938
939        assert_eq!(casted.buffer::<i16>().as_ref(), &[12, -5]);
940
941        let error = PrimitiveArray::from_iter([42i32])
942            .into_array()
943            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
944            .execute::<DecimalArray>(&mut ctx)
945            .unwrap_err();
946        assert!(error.to_string().contains("would lose precision"));
947        Ok(())
948    }
949
950    #[test]
951    fn cast_zero_to_large_negative_scale_decimal() -> VortexResult<()> {
952        let mut ctx = array_session().create_execution_ctx();
953        let decimal_dtype = DecimalDType::new(3, -128);
954        let casted = PrimitiveArray::from_iter([0i32])
955            .into_array()
956            .cast(DType::Decimal(decimal_dtype, Nullability::NonNullable))?
957            .execute::<DecimalArray>(&mut ctx)?;
958
959        assert_eq!(casted.buffer::<i16>().as_ref(), &[0]);
960        Ok(())
961    }
962
963    #[test]
964    fn cast_integer_to_decimal_ignores_out_of_range_null_lanes() -> VortexResult<()> {
965        let mut ctx = array_session().create_execution_ctx();
966        let decimal_dtype = DecimalDType::new(3, 1);
967        let casted = PrimitiveArray::new(buffer![999i32, 42], Validity::from_iter([false, true]))
968            .into_array()
969            .cast(DType::Decimal(decimal_dtype, Nullability::Nullable))?
970            .execute::<DecimalArray>(&mut ctx)?;
971
972        assert_eq!(casted.buffer::<i16>().as_ref()[1], 420);
973        assert_eq!(
974            casted.validity()?.execute_mask(casted.len(), &mut ctx)?,
975            Mask::from(BitBuffer::from(vec![false, true]))
976        );
977        Ok(())
978    }
979
980    #[test]
981    fn cast_integer_to_decimal_checks_precision() -> VortexResult<()> {
982        let mut ctx = array_session().create_execution_ctx();
983        let casted = PrimitiveArray::from_iter([100i32])
984            .into_array()
985            .cast(DType::Decimal(
986                DecimalDType::new(2, 1),
987                Nullability::NonNullable,
988            ))?;
989
990        let error = casted.execute::<DecimalArray>(&mut ctx).unwrap_err();
991        assert!(error.to_string().contains("does not fit in precision"));
992        Ok(())
993    }
994
995    #[test]
996    fn cast_floating_primitive_to_decimal_fails() -> VortexResult<()> {
997        let mut ctx = array_session().create_execution_ctx();
998        let casted = PrimitiveArray::from_iter([1.0f64])
999            .into_array()
1000            .cast(DType::Decimal(
1001                DecimalDType::new(3, 1),
1002                Nullability::NonNullable,
1003            ))?;
1004
1005        let error = casted.execute::<DecimalArray>(&mut ctx).unwrap_err();
1006        assert!(
1007            error
1008                .to_string()
1009                .contains("Cannot cast floating primitive f64 to decimal decimal(3,1)")
1010        );
1011        Ok(())
1012    }
1013
1014    #[test]
1015    fn cast_i32_u32() {
1016        let arr = buffer![-1i32].into_array();
1017        #[expect(deprecated)]
1018        let error = arr
1019            .cast(PType::U32.into())
1020            .and_then(|a| a.to_canonical().map(|c| c.into_array()))
1021            .unwrap_err();
1022        assert!(matches!(error, VortexError::Compute(..)));
1023        assert!(error.to_string().contains("values exceed target range"));
1024    }
1025
1026    #[test]
1027    fn cast_array_with_nulls_to_nonnullable() {
1028        let arr = PrimitiveArray::from_option_iter([Some(-1i32), None, Some(10)]);
1029        #[expect(deprecated)]
1030        let err = arr
1031            .into_array()
1032            .cast(PType::I32.into())
1033            .and_then(|a| a.to_canonical().map(|c| c.into_array()))
1034            .unwrap_err();
1035
1036        assert!(matches!(err, VortexError::InvalidArgument(..)));
1037        assert!(
1038            err.to_string()
1039                .contains("Cannot cast array with invalid values to non-nullable type.")
1040        );
1041    }
1042
1043    #[test]
1044    fn cast_with_invalid_nulls() {
1045        let mut ctx = array_session().create_execution_ctx();
1046        let arr = PrimitiveArray::new(
1047            buffer![-1i32, 0, 10],
1048            Validity::from_iter([false, true, true]),
1049        );
1050        let p = arr
1051            .into_array()
1052            .cast(DType::Primitive(PType::U32, Nullability::Nullable))
1053            .unwrap()
1054            .execute::<PrimitiveArray>(&mut ctx)
1055            .unwrap();
1056        assert_arrays_eq!(
1057            p,
1058            PrimitiveArray::from_option_iter([None, Some(0u32), Some(10)]),
1059            &mut ctx
1060        );
1061        assert_eq!(
1062            p.as_ref()
1063                .validity()
1064                .unwrap()
1065                .execute_mask(
1066                    p.as_ref().len(),
1067                    &mut array_session().create_execution_ctx()
1068                )
1069                .unwrap(),
1070            Mask::from(BitBuffer::from(vec![false, true, true]))
1071        );
1072    }
1073
1074    /// Same-width integer cast where all values fit: should reinterpret the
1075    /// buffer without allocation (pointer identity).
1076    #[test]
1077    fn cast_same_width_int_reinterprets_buffer() -> VortexResult<()> {
1078        let mut ctx = array_session().create_execution_ctx();
1079        let src = PrimitiveArray::from_iter([0u32, 10, 100]);
1080        let src_ptr = src.as_slice::<u32>().as_ptr();
1081
1082        let dst = src
1083            .into_array()
1084            .cast(PType::I32.into())?
1085            .execute::<PrimitiveArray>(&mut ctx)?;
1086        let dst_ptr = dst.as_slice::<i32>().as_ptr();
1087
1088        // Zero-copy: the data pointer should be identical.
1089        assert_eq!(src_ptr as usize, dst_ptr as usize);
1090        assert_arrays_eq!(dst, PrimitiveArray::from_iter([0i32, 10, 100]), &mut ctx);
1091        Ok(())
1092    }
1093
1094    /// Same-width integer cast where values don't fit: should fall through
1095    /// to the allocating path and produce an error.
1096    #[test]
1097    fn cast_same_width_int_out_of_range_errors() {
1098        let arr = buffer![u32::MAX].into_array();
1099        #[expect(deprecated)]
1100        let err = arr
1101            .cast(PType::I32.into())
1102            .and_then(|a| a.to_canonical().map(|c| c.into_array()))
1103            .unwrap_err();
1104        assert!(matches!(err, VortexError::Compute(..)));
1105    }
1106
1107    /// All-null array cast between same-width types should succeed without
1108    /// touching the buffer contents.
1109    #[test]
1110    fn cast_same_width_all_null() -> VortexResult<()> {
1111        let mut ctx = array_session().create_execution_ctx();
1112        let arr = PrimitiveArray::new(buffer![0xFFu8, 0xFF], Validity::AllInvalid);
1113        let casted = arr
1114            .into_array()
1115            .cast(DType::Primitive(PType::I8, Nullability::Nullable))?
1116            .execute::<PrimitiveArray>(&mut ctx)?;
1117        assert_eq!(casted.len(), 2);
1118        assert!(matches!(casted.validity(), Ok(Validity::AllInvalid)));
1119        Ok(())
1120    }
1121
1122    /// Same-width integer cast with nullable values: out-of-range nulls should
1123    /// not prevent the cast from succeeding.
1124    #[test]
1125    fn cast_same_width_int_nullable_with_out_of_range_nulls() -> VortexResult<()> {
1126        let mut ctx = array_session().create_execution_ctx();
1127        // The null position holds u32::MAX which doesn't fit in i32, but it's
1128        // masked as invalid so the cast should still succeed via reinterpret.
1129        let arr = PrimitiveArray::new(
1130            buffer![u32::MAX, 0u32, 42u32],
1131            Validity::from_iter([false, true, true]),
1132        );
1133        let casted = arr
1134            .into_array()
1135            .cast(DType::Primitive(PType::I32, Nullability::Nullable))?
1136            .execute::<PrimitiveArray>(&mut ctx)?;
1137        assert_arrays_eq!(
1138            casted,
1139            PrimitiveArray::from_option_iter([None, Some(0i32), Some(42)]),
1140            &mut ctx
1141        );
1142        Ok(())
1143    }
1144
1145    #[test]
1146    fn cast_u32_to_u8_with_out_of_range_nulls() -> VortexResult<()> {
1147        let mut ctx = array_session().create_execution_ctx();
1148        let arr = PrimitiveArray::new(
1149            buffer![1000u32, 10u32, 42u32],
1150            Validity::from_iter([false, true, true]),
1151        );
1152        let casted = arr
1153            .into_array()
1154            .cast(DType::Primitive(PType::U8, Nullability::Nullable))?
1155            .execute::<PrimitiveArray>(&mut ctx)?;
1156        assert_arrays_eq!(
1157            casted,
1158            PrimitiveArray::from_option_iter([None, Some(10u8), Some(42)]),
1159            &mut ctx
1160        );
1161        Ok(())
1162    }
1163
1164    #[rstest]
1165    #[case(buffer![0u8, 1, 2, 3, 255].into_array())]
1166    #[case(buffer![0u16, 100, 1000, 65535].into_array())]
1167    #[case(buffer![0u32, 100, 1000, 1000000].into_array())]
1168    #[case(buffer![0u64, 100, 1000, 1000000000].into_array())]
1169    #[case(buffer![-128i8, -1, 0, 1, 127].into_array())]
1170    #[case(buffer![-1000i16, -1, 0, 1, 1000].into_array())]
1171    #[case(buffer![-1000000i32, -1, 0, 1, 1000000].into_array())]
1172    #[case(buffer![-1000000000i64, -1, 0, 1, 1000000000].into_array())]
1173    #[case(buffer![0.0f32, 1.5, -2.5, 100.0, 1e6].into_array())]
1174    #[case(buffer![f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0f32].into_array())]
1175    #[case(buffer![0.0f64, 1.5, -2.5, 100.0, 1e12].into_array())]
1176    #[case(buffer![f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.0f64].into_array())]
1177    #[case(PrimitiveArray::from_option_iter([Some(1u8), None, Some(255), Some(0), None]).into_array())]
1178    #[case(PrimitiveArray::from_option_iter([Some(1i32), None, Some(-100), Some(0), None]).into_array())]
1179    #[case(buffer![42u32].into_array())]
1180    fn test_cast_primitive_conformance(#[case] array: ArrayRef) {
1181        test_cast_conformance(&array, &mut array_session().create_execution_ctx());
1182    }
1183}