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    // DType::Null has no non-nullable form
136    if &DType::Null == array.dtype() {
137        return;
138    }
139
140    if array
141        .invalid_count(ctx)
142        .vortex_expect("invalid_count should succeed in conformance test")
143        == 0
144    {
145        let non_nullable = cast_and_execute(&array.clone(), array.dtype().as_nonnullable(), ctx)
146            .vortex_expect("arrays without nulls can cast to non-nullable");
147        assert_eq!(non_nullable.dtype(), &array.dtype().as_nonnullable());
148        assert_eq!(non_nullable.len(), array.len());
149
150        for i in 0..array.len().min(10) {
151            assert_eq!(
152                array
153                    .execute_scalar(i, ctx)
154                    .vortex_expect("scalar_at should succeed in conformance test"),
155                non_nullable
156                    .execute_scalar(i, ctx)
157                    .vortex_expect("scalar_at should succeed in conformance test")
158            );
159        }
160
161        let back_to_nullable = cast_and_execute(&non_nullable, array.dtype().clone(), ctx)
162            .vortex_expect("non-nullable arrays can cast to nullable");
163        assert_eq!(back_to_nullable.dtype(), array.dtype());
164        assert_eq!(back_to_nullable.len(), array.len());
165
166        for i in 0..array.len().min(10) {
167            assert_eq!(
168                array
169                    .execute_scalar(i, ctx)
170                    .vortex_expect("scalar_at should succeed in conformance test"),
171                back_to_nullable
172                    .execute_scalar(i, ctx)
173                    .vortex_expect("scalar_at should succeed in conformance test")
174            );
175        }
176    } else {
177        if &DType::Null == array.dtype() {
178            // DType::Null has no non-nullable form. A null array can only
179            // be cast to DType::Null
180            return;
181        }
182        cast_and_execute(&array.clone(), array.dtype().as_nonnullable(), ctx)
183            .err()
184            .unwrap_or_else(|| {
185                vortex_panic!(
186                    "arrays with nulls should error when casting to non-nullable {}",
187                    array,
188                )
189            });
190    }
191}
192
193fn test_cast_to_nullable(array: &ArrayRef, ctx: &mut ExecutionCtx) {
194    let nullable = cast_and_execute(&array.clone(), array.dtype().as_nullable(), ctx)
195        .vortex_expect("arrays without nulls can cast to nullable");
196    assert_eq!(nullable.dtype(), &array.dtype().as_nullable());
197    assert_eq!(nullable.len(), array.len());
198
199    for i in 0..array.len().min(10) {
200        assert_eq!(
201            array
202                .execute_scalar(i, ctx)
203                .vortex_expect("scalar_at should succeed in conformance test"),
204            nullable
205                .execute_scalar(i, ctx)
206                .vortex_expect("scalar_at should succeed in conformance test")
207        );
208    }
209
210    let back = cast_and_execute(&nullable, array.dtype().clone(), ctx)
211        .vortex_expect("casting to nullable and back should be a no-op");
212    assert_eq!(back.dtype(), array.dtype());
213    assert_eq!(back.len(), array.len());
214
215    for i in 0..array.len().min(10) {
216        assert_eq!(
217            array
218                .execute_scalar(i, ctx)
219                .vortex_expect("scalar_at should succeed in conformance test"),
220            back.execute_scalar(i, ctx)
221                .vortex_expect("scalar_at should succeed in conformance test")
222        );
223    }
224}
225
226fn test_cast_from_floating_point_types(array: &ArrayRef, ctx: &mut ExecutionCtx) {
227    let ptype = array.dtype().as_ptype();
228    test_cast_to_primitive(array, PType::I8, false, ctx);
229    test_cast_to_primitive(array, PType::U8, false, ctx);
230    test_cast_to_primitive(array, PType::I16, false, ctx);
231    test_cast_to_primitive(array, PType::U16, false, ctx);
232    test_cast_to_primitive(array, PType::I32, false, ctx);
233    test_cast_to_primitive(array, PType::U32, false, ctx);
234    test_cast_to_primitive(array, PType::I64, false, ctx);
235    test_cast_to_primitive(array, PType::U64, false, ctx);
236    test_cast_to_primitive(array, PType::F16, matches!(ptype, PType::F16), ctx);
237    test_cast_to_primitive(
238        array,
239        PType::F32,
240        matches!(ptype, PType::F16 | PType::F32),
241        ctx,
242    );
243    test_cast_to_primitive(array, PType::F64, true, ctx);
244}
245
246fn test_cast_to_integral_types(array: &ArrayRef, ctx: &mut ExecutionCtx) {
247    test_cast_to_primitive(array, PType::I8, true, ctx);
248    test_cast_to_primitive(array, PType::U8, true, ctx);
249    test_cast_to_primitive(array, PType::I16, true, ctx);
250    test_cast_to_primitive(array, PType::U16, true, ctx);
251    test_cast_to_primitive(array, PType::I32, true, ctx);
252    test_cast_to_primitive(array, PType::U32, true, ctx);
253    test_cast_to_primitive(array, PType::I64, true, ctx);
254    test_cast_to_primitive(array, PType::U64, true, ctx);
255}
256
257/// Does this scalar fit in this type?
258fn fits(value: &Scalar, ptype: PType) -> bool {
259    let dtype = DType::Primitive(ptype, value.dtype().nullability());
260    value.cast(&dtype).is_ok()
261}
262
263fn test_cast_to_primitive(
264    array: &ArrayRef,
265    target_ptype: PType,
266    test_round_trip: bool,
267    ctx: &mut ExecutionCtx,
268) {
269    let maybe_min_max = min_max(array, ctx, NumericalAggregateOpts::default())
270        .vortex_expect("cast should succeed in conformance test");
271
272    if let Some(MinMaxResult { min, max }) = maybe_min_max
273        && (!fits(&min, target_ptype) || !fits(&max, target_ptype))
274    {
275        cast_and_execute(
276            &array.clone(),
277            DType::Primitive(target_ptype, array.dtype().nullability()),
278            ctx,
279        )
280        .err()
281        .unwrap_or_else(|| {
282            vortex_panic!(
283                "Cast must fail because some values are out of bounds. {} {:?} {:?} {} {}",
284                target_ptype,
285                min,
286                max,
287                array,
288                array.display_values(),
289            )
290        });
291        return;
292    }
293
294    // Otherwise, all values must fit.
295    let casted = cast_and_execute(
296        &array.clone(),
297        DType::Primitive(target_ptype, array.dtype().nullability()),
298        ctx,
299    )
300    .unwrap_or_else(|e| {
301        vortex_panic!(
302            "Cast must succeed because all values are within bounds. {} {}: {e}",
303            target_ptype,
304            array.display_values(),
305        )
306    });
307    assert_eq!(
308        array
309            .validity()
310            .vortex_expect("validity_mask should succeed in conformance test")
311            .execute_mask(array.len(), ctx)
312            .vortex_expect("Failed to compute validity mask"),
313        casted
314            .validity()
315            .vortex_expect("validity_mask should succeed in conformance test")
316            .execute_mask(casted.len(), ctx)
317            .vortex_expect("Failed to compute validity mask")
318    );
319    for i in 0..array.len().min(10) {
320        let original = array
321            .execute_scalar(i, ctx)
322            .vortex_expect("scalar_at should succeed in conformance test");
323        let casted = casted
324            .execute_scalar(i, ctx)
325            .vortex_expect("scalar_at should succeed in conformance test");
326        assert_eq!(
327            original
328                .cast(casted.dtype())
329                .vortex_expect("cast should succeed in conformance test"),
330            casted,
331            "{i} {original} {casted}"
332        );
333        if test_round_trip {
334            assert_eq!(
335                original,
336                casted
337                    .cast(original.dtype())
338                    .vortex_expect("cast should succeed in conformance test"),
339                "{i} {original} {casted}"
340            );
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use std::sync::LazyLock;
348
349    use vortex_buffer::buffer;
350    use vortex_session::VortexSession;
351
352    use super::*;
353    use crate::IntoArray;
354    use crate::VortexSessionExecute;
355    use crate::array_session;
356    use crate::arrays::BoolArray;
357    use crate::arrays::ListArray;
358    use crate::arrays::NullArray;
359    use crate::arrays::PrimitiveArray;
360    use crate::arrays::StructArray;
361    use crate::arrays::VarBinArray;
362    use crate::dtype::DType;
363    use crate::dtype::FieldNames;
364    use crate::dtype::Nullability;
365
366    static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
367
368    #[test]
369    fn test_cast_conformance_u32() {
370        let array = buffer![0u32, 100, 200, 65535, 1000000].into_array();
371        test_cast_conformance(&array, &mut SESSION.create_execution_ctx());
372    }
373
374    #[test]
375    fn test_cast_conformance_i32() {
376        let array = buffer![-100i32, -1, 0, 1, 100].into_array();
377        test_cast_conformance(&array, &mut SESSION.create_execution_ctx());
378    }
379
380    #[test]
381    fn test_cast_conformance_f32() {
382        let array = buffer![0.0f32, 1.5, -2.5, 100.0, 1e6].into_array();
383        test_cast_conformance(&array, &mut SESSION.create_execution_ctx());
384    }
385
386    #[test]
387    fn test_cast_conformance_nullable() {
388        let array = PrimitiveArray::from_option_iter([Some(1u8), None, Some(255), Some(0), None]);
389        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
390    }
391
392    #[test]
393    fn test_cast_conformance_bool() {
394        let array = BoolArray::from_iter(vec![true, false, true, false]);
395        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
396    }
397
398    #[test]
399    fn test_cast_conformance_null() {
400        let array = NullArray::new(5);
401        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
402    }
403
404    #[test]
405    fn test_cast_conformance_utf8() {
406        let array = VarBinArray::from_iter(
407            vec![Some("hello"), None, Some("world")],
408            DType::Utf8(Nullability::Nullable),
409        );
410        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
411    }
412
413    #[test]
414    fn test_cast_conformance_binary() {
415        let array = VarBinArray::from_iter(
416            vec![Some(b"data".as_slice()), None, Some(b"bytes".as_slice())],
417            DType::Binary(Nullability::Nullable),
418        );
419        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
420    }
421
422    #[test]
423    fn test_cast_conformance_struct() {
424        let names = FieldNames::from(["a", "b"]);
425
426        let a = buffer![1i32, 2, 3].into_array();
427        let b = VarBinArray::from_iter(
428            vec![Some("x"), None, Some("z")],
429            DType::Utf8(Nullability::Nullable),
430        )
431        .into_array();
432
433        let array =
434            StructArray::try_new(names, vec![a, b], 3, crate::validity::Validity::NonNullable)
435                .unwrap();
436        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
437    }
438
439    #[test]
440    fn test_cast_conformance_list() {
441        let data = buffer![1i32, 2, 3, 4, 5, 6].into_array();
442        let offsets = buffer![0i64, 2, 2, 5, 6].into_array();
443
444        let array =
445            ListArray::try_new(data, offsets, crate::validity::Validity::NonNullable).unwrap();
446        test_cast_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
447    }
448}