Skip to main content

vortex_array/compute/conformance/
cast.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexExpect;
5use vortex_error::VortexResult;
6use vortex_error::vortex_panic;
7
8use crate::ArrayRef;
9use crate::ExecutionCtx;
10use crate::IntoArray;
11use crate::RecursiveCanonical;
12use crate::aggregate_fn::NumericalAggregateOpts;
13use crate::aggregate_fn::fns::min_max::MinMaxResult;
14use crate::aggregate_fn::fns::min_max::min_max;
15use crate::builtins::ArrayBuiltins;
16use crate::dtype::DType;
17use crate::dtype::Nullability;
18use crate::dtype::PType;
19use crate::scalar::Scalar;
20
21/// Cast and force execution via `to_canonical`, returning the canonical array.
22fn cast_and_execute(
23    array: &ArrayRef,
24    dtype: DType,
25    ctx: &mut ExecutionCtx,
26) -> VortexResult<ArrayRef> {
27    Ok(array
28        .cast(dtype)?
29        .execute::<RecursiveCanonical>(ctx)?
30        .0
31        .into_array())
32}
33
34/// Test conformance of the cast compute function for an array.
35///
36/// This function tests various casting scenarios including:
37/// - Casting between numeric types (widening and narrowing)
38/// - Casting between signed and unsigned types
39/// - Casting between integral and floating-point types
40/// - Casting with nullability changes
41/// - Casting between string types (Utf8/Binary)
42/// - Edge cases like overflow behavior
43pub fn test_cast_conformance(array: &ArrayRef, ctx: &mut ExecutionCtx) {
44    let dtype = array.dtype();
45
46    // Always test identity cast and nullability changes
47    test_cast_identity(array, ctx);
48
49    test_cast_to_non_nullable(array, ctx);
50    test_cast_to_nullable(array, ctx);
51
52    // Test based on the specific DType
53    match dtype {
54        DType::Null => test_cast_from_null(array, ctx),
55        DType::Primitive(ptype, ..) => match ptype {
56            PType::U8
57            | PType::U16
58            | PType::U32
59            | PType::U64
60            | PType::I8
61            | PType::I16
62            | PType::I32
63            | PType::I64 => test_cast_to_integral_types(array, ctx),
64            PType::F16 | PType::F32 | PType::F64 => test_cast_from_floating_point_types(array, ctx),
65        },
66        _ => {}
67    }
68}
69
70fn test_cast_identity(array: &ArrayRef, ctx: &mut ExecutionCtx) {
71    // Casting to the same type should be a no-op
72    let result = cast_and_execute(&array.clone(), array.dtype().clone(), ctx)
73        .vortex_expect("cast should succeed in conformance test");
74    assert_eq!(result.len(), array.len());
75    assert_eq!(result.dtype(), array.dtype());
76
77    // Verify values are unchanged
78    for i in 0..array.len().min(10) {
79        assert_eq!(
80            array
81                .execute_scalar(i, ctx)
82                .vortex_expect("scalar_at should succeed in conformance test"),
83            result
84                .execute_scalar(i, ctx)
85                .vortex_expect("scalar_at should succeed in conformance test")
86        );
87    }
88}
89
90fn test_cast_from_null(array: &ArrayRef, ctx: &mut ExecutionCtx) {
91    // Null can be cast to itself
92    let result = cast_and_execute(&array.clone(), DType::Null, ctx)
93        .vortex_expect("cast should succeed in conformance test");
94    assert_eq!(result.len(), array.len());
95    assert_eq!(result.dtype(), &DType::Null);
96
97    // Null can also be cast to any nullable type
98    let nullable_types = vec![
99        DType::Bool(Nullability::Nullable),
100        DType::Primitive(PType::I32, Nullability::Nullable),
101        DType::Primitive(PType::F64, Nullability::Nullable),
102        DType::Utf8(Nullability::Nullable),
103        DType::Binary(Nullability::Nullable),
104    ];
105
106    for dtype in nullable_types {
107        let result = cast_and_execute(&array.clone(), dtype.clone(), ctx)
108            .vortex_expect("cast should succeed in conformance test");
109        assert_eq!(result.len(), array.len());
110        assert_eq!(result.dtype(), &dtype);
111
112        // Verify all values are null
113        for i in 0..array.len().min(10) {
114            assert!(
115                result
116                    .execute_scalar(i, ctx)
117                    .vortex_expect("scalar_at should succeed in conformance test")
118                    .is_null()
119            );
120        }
121    }
122
123    // Casting to non-nullable types should fail
124    let non_nullable_types = vec![
125        DType::Bool(Nullability::NonNullable),
126        DType::Primitive(PType::I32, Nullability::NonNullable),
127    ];
128
129    for dtype in non_nullable_types {
130        assert!(cast_and_execute(&array.clone(), dtype.clone(), ctx).is_err());
131    }
132}
133
134fn test_cast_to_non_nullable(array: &ArrayRef, ctx: &mut ExecutionCtx) {
135    if array
136        .invalid_count(ctx)
137        .vortex_expect("invalid_count should succeed in conformance test")
138        == 0
139    {
140        let non_nullable = cast_and_execute(&array.clone(), array.dtype().as_nonnullable(), ctx)
141            .vortex_expect("arrays without nulls can cast to non-nullable");
142        assert_eq!(non_nullable.dtype(), &array.dtype().as_nonnullable());
143        assert_eq!(non_nullable.len(), array.len());
144
145        for i in 0..array.len().min(10) {
146            assert_eq!(
147                array
148                    .execute_scalar(i, ctx)
149                    .vortex_expect("scalar_at should succeed in conformance test"),
150                non_nullable
151                    .execute_scalar(i, ctx)
152                    .vortex_expect("scalar_at should succeed in conformance test")
153            );
154        }
155
156        let back_to_nullable = cast_and_execute(&non_nullable, array.dtype().clone(), ctx)
157            .vortex_expect("non-nullable arrays can cast to nullable");
158        assert_eq!(back_to_nullable.dtype(), array.dtype());
159        assert_eq!(back_to_nullable.len(), array.len());
160
161        for i in 0..array.len().min(10) {
162            assert_eq!(
163                array
164                    .execute_scalar(i, ctx)
165                    .vortex_expect("scalar_at should succeed in conformance test"),
166                back_to_nullable
167                    .execute_scalar(i, ctx)
168                    .vortex_expect("scalar_at should succeed in conformance test")
169            );
170        }
171    } else {
172        if &DType::Null == array.dtype() {
173            // DType::Null.as_nonnullable() (confusingly) returns DType:Null. Of course, a null
174            // array can be casted to DType::Null.
175            return;
176        }
177        cast_and_execute(&array.clone(), array.dtype().as_nonnullable(), ctx)
178            .err()
179            .unwrap_or_else(|| {
180                vortex_panic!(
181                    "arrays with nulls should error when casting to non-nullable {}",
182                    array,
183                )
184            });
185    }
186}
187
188fn test_cast_to_nullable(array: &ArrayRef, ctx: &mut ExecutionCtx) {
189    let nullable = cast_and_execute(&array.clone(), array.dtype().as_nullable(), ctx)
190        .vortex_expect("arrays without nulls can cast to nullable");
191    assert_eq!(nullable.dtype(), &array.dtype().as_nullable());
192    assert_eq!(nullable.len(), array.len());
193
194    for i in 0..array.len().min(10) {
195        assert_eq!(
196            array
197                .execute_scalar(i, ctx)
198                .vortex_expect("scalar_at should succeed in conformance test"),
199            nullable
200                .execute_scalar(i, ctx)
201                .vortex_expect("scalar_at should succeed in conformance test")
202        );
203    }
204
205    let back = cast_and_execute(&nullable, array.dtype().clone(), ctx)
206        .vortex_expect("casting to nullable and back should be a no-op");
207    assert_eq!(back.dtype(), array.dtype());
208    assert_eq!(back.len(), array.len());
209
210    for i in 0..array.len().min(10) {
211        assert_eq!(
212            array
213                .execute_scalar(i, ctx)
214                .vortex_expect("scalar_at should succeed in conformance test"),
215            back.execute_scalar(i, ctx)
216                .vortex_expect("scalar_at should succeed in conformance test")
217        );
218    }
219}
220
221fn test_cast_from_floating_point_types(array: &ArrayRef, ctx: &mut ExecutionCtx) {
222    let ptype = array.dtype().as_ptype();
223    test_cast_to_primitive(array, PType::I8, false, ctx);
224    test_cast_to_primitive(array, PType::U8, false, ctx);
225    test_cast_to_primitive(array, PType::I16, false, ctx);
226    test_cast_to_primitive(array, PType::U16, false, ctx);
227    test_cast_to_primitive(array, PType::I32, false, ctx);
228    test_cast_to_primitive(array, PType::U32, false, ctx);
229    test_cast_to_primitive(array, PType::I64, false, ctx);
230    test_cast_to_primitive(array, PType::U64, false, ctx);
231    test_cast_to_primitive(array, PType::F16, matches!(ptype, PType::F16), ctx);
232    test_cast_to_primitive(
233        array,
234        PType::F32,
235        matches!(ptype, PType::F16 | PType::F32),
236        ctx,
237    );
238    test_cast_to_primitive(array, PType::F64, true, ctx);
239}
240
241fn test_cast_to_integral_types(array: &ArrayRef, ctx: &mut ExecutionCtx) {
242    test_cast_to_primitive(array, PType::I8, true, ctx);
243    test_cast_to_primitive(array, PType::U8, true, ctx);
244    test_cast_to_primitive(array, PType::I16, true, ctx);
245    test_cast_to_primitive(array, PType::U16, true, ctx);
246    test_cast_to_primitive(array, PType::I32, true, ctx);
247    test_cast_to_primitive(array, PType::U32, true, ctx);
248    test_cast_to_primitive(array, PType::I64, true, ctx);
249    test_cast_to_primitive(array, PType::U64, true, ctx);
250}
251
252/// Does this scalar fit in this type?
253fn fits(value: &Scalar, ptype: PType) -> bool {
254    let dtype = DType::Primitive(ptype, value.dtype().nullability());
255    value.cast(&dtype).is_ok()
256}
257
258fn test_cast_to_primitive(
259    array: &ArrayRef,
260    target_ptype: PType,
261    test_round_trip: bool,
262    ctx: &mut ExecutionCtx,
263) {
264    let maybe_min_max = min_max(array, ctx, NumericalAggregateOpts::default())
265        .vortex_expect("cast should succeed in conformance test");
266
267    if let Some(MinMaxResult { min, max }) = maybe_min_max
268        && (!fits(&min, target_ptype) || !fits(&max, target_ptype))
269    {
270        cast_and_execute(
271            &array.clone(),
272            DType::Primitive(target_ptype, array.dtype().nullability()),
273            ctx,
274        )
275        .err()
276        .unwrap_or_else(|| {
277            vortex_panic!(
278                "Cast must fail because some values are out of bounds. {} {:?} {:?} {} {}",
279                target_ptype,
280                min,
281                max,
282                array,
283                array.display_values(),
284            )
285        });
286        return;
287    }
288
289    // Otherwise, all values must fit.
290    let casted = cast_and_execute(
291        &array.clone(),
292        DType::Primitive(target_ptype, array.dtype().nullability()),
293        ctx,
294    )
295    .unwrap_or_else(|e| {
296        vortex_panic!(
297            "Cast must succeed because all values are within bounds. {} {}: {e}",
298            target_ptype,
299            array.display_values(),
300        )
301    });
302    assert_eq!(
303        array
304            .validity()
305            .vortex_expect("validity_mask should succeed in conformance test")
306            .execute_mask(array.len(), ctx)
307            .vortex_expect("Failed to compute validity mask"),
308        casted
309            .validity()
310            .vortex_expect("validity_mask should succeed in conformance test")
311            .execute_mask(casted.len(), ctx)
312            .vortex_expect("Failed to compute validity mask")
313    );
314    for i in 0..array.len().min(10) {
315        let original = array
316            .execute_scalar(i, ctx)
317            .vortex_expect("scalar_at should succeed in conformance test");
318        let casted = casted
319            .execute_scalar(i, ctx)
320            .vortex_expect("scalar_at should succeed in conformance test");
321        assert_eq!(
322            original
323                .cast(casted.dtype())
324                .vortex_expect("cast should succeed in conformance test"),
325            casted,
326            "{i} {original} {casted}"
327        );
328        if test_round_trip {
329            assert_eq!(
330                original,
331                casted
332                    .cast(original.dtype())
333                    .vortex_expect("cast should succeed in conformance test"),
334                "{i} {original} {casted}"
335            );
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use std::sync::LazyLock;
343
344    use vortex_buffer::buffer;
345    use vortex_session::VortexSession;
346
347    use super::*;
348    use crate::IntoArray;
349    use crate::VortexSessionExecute;
350    use crate::array_session;
351    use crate::arrays::BoolArray;
352    use crate::arrays::ListArray;
353    use crate::arrays::NullArray;
354    use crate::arrays::PrimitiveArray;
355    use crate::arrays::StructArray;
356    use crate::arrays::VarBinArray;
357    use crate::dtype::DType;
358    use crate::dtype::FieldNames;
359    use crate::dtype::Nullability;
360
361    static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
362
363    #[test]
364    fn test_cast_conformance_u32() {
365        let array = buffer![0u32, 100, 200, 65535, 1000000].into_array();
366        test_cast_conformance(&array, &mut SESSION.create_execution_ctx());
367    }
368
369    #[test]
370    fn test_cast_conformance_i32() {
371        let array = buffer![-100i32, -1, 0, 1, 100].into_array();
372        test_cast_conformance(&array, &mut SESSION.create_execution_ctx());
373    }
374
375    #[test]
376    fn test_cast_conformance_f32() {
377        let array = buffer![0.0f32, 1.5, -2.5, 100.0, 1e6].into_array();
378        test_cast_conformance(&array, &mut SESSION.create_execution_ctx());
379    }
380
381    #[test]
382    fn test_cast_conformance_nullable() {
383        let array = PrimitiveArray::from_option_iter([Some(1u8), None, Some(255), Some(0), None]);
384        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
385    }
386
387    #[test]
388    fn test_cast_conformance_bool() {
389        let array = BoolArray::from_iter(vec![true, false, true, false]);
390        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
391    }
392
393    #[test]
394    fn test_cast_conformance_null() {
395        let array = NullArray::new(5);
396        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
397    }
398
399    #[test]
400    fn test_cast_conformance_utf8() {
401        let array = VarBinArray::from_iter(
402            vec![Some("hello"), None, Some("world")],
403            DType::Utf8(Nullability::Nullable),
404        );
405        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
406    }
407
408    #[test]
409    fn test_cast_conformance_binary() {
410        let array = VarBinArray::from_iter(
411            vec![Some(b"data".as_slice()), None, Some(b"bytes".as_slice())],
412            DType::Binary(Nullability::Nullable),
413        );
414        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
415    }
416
417    #[test]
418    fn test_cast_conformance_struct() {
419        let names = FieldNames::from(["a", "b"]);
420
421        let a = buffer![1i32, 2, 3].into_array();
422        let b = VarBinArray::from_iter(
423            vec![Some("x"), None, Some("z")],
424            DType::Utf8(Nullability::Nullable),
425        )
426        .into_array();
427
428        let array =
429            StructArray::try_new(names, vec![a, b], 3, crate::validity::Validity::NonNullable)
430                .unwrap();
431        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
432    }
433
434    #[test]
435    fn test_cast_conformance_list() {
436        let data = buffer![1i32, 2, 3, 4, 5, 6].into_array();
437        let offsets = buffer![0i64, 2, 2, 5, 6].into_array();
438
439        let array =
440            ListArray::try_new(data, offsets, crate::validity::Validity::NonNullable).unwrap();
441        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
442    }
443}