Skip to main content

vortex_fastlanes/delta/compute/
cast.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_array::ArrayRef;
5use vortex_array::ArrayView;
6use vortex_array::IntoArray;
7use vortex_array::builtins::ArrayBuiltins;
8use vortex_array::dtype::DType;
9use vortex_array::dtype::Nullability::NonNullable;
10use vortex_array::scalar_fn::fns::cast::CastReduce;
11use vortex_error::VortexResult;
12
13use crate::delta::Delta;
14use crate::delta::array::DeltaArrayExt;
15use crate::delta::array::DeltaArraySlotsExt;
16
17impl CastReduce for Delta {
18    fn cast(array: ArrayView<'_, Self>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
19        let DType::Primitive(target_ptype, target_nullability) = dtype else {
20            return Ok(None);
21        };
22
23        // Only a nullability change at the same primitive type can reuse the encoded
24        // components; everything else defers to the generic decompress path.
25        if array.dtype().as_ptype() != *target_ptype {
26            return Ok(None);
27        }
28        if *target_nullability == NonNullable && array.dtype().nullability() != NonNullable {
29            return Ok(None);
30        }
31
32        let casted_bases = array.bases().cast(dtype.with_nullability(NonNullable))?;
33        let casted_deltas = array.deltas().cast(dtype.clone())?;
34
35        Ok(Some(
36            Delta::try_new(casted_bases, casted_deltas, array.offset(), array.len())?.into_array(),
37        ))
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use std::sync::LazyLock;
44
45    use rstest::rstest;
46    use vortex_array::IntoArray;
47    use vortex_array::VortexSessionExecute;
48    use vortex_array::arrays::PrimitiveArray;
49    use vortex_array::assert_arrays_eq;
50    use vortex_array::builtins::ArrayBuiltins;
51    use vortex_array::compute::conformance::cast::test_cast_conformance;
52    use vortex_array::dtype::DType;
53    use vortex_array::dtype::Nullability;
54    use vortex_array::dtype::PType;
55    use vortex_buffer::buffer;
56    use vortex_error::VortexResult;
57    use vortex_session::VortexSession;
58
59    use crate::Delta;
60    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
61        let session = vortex_array::array_session();
62        crate::initialize(&session);
63        session
64    });
65
66    #[test]
67    fn test_cast_delta_unsigned_widening_wraps() {
68        // A valid non-monotonic unsigned sequence whose deltas wrap at the source width.
69        // 200u8 -> 50u8 stores delta 0x6A (106); decode does 200 wrapping_add 106 = 306
70        // mod 256 = 50. Widening the delta to u32 preserves the value 106 but changes the
71        // reconstruction modulus to 2^32, so 200 + 106 = 306 instead of 50. Every index
72        // after a wrap is corrupted, so the widening cast must not reuse the components.
73        let primitive = PrimitiveArray::from_iter([200u8, 50, 75, 10, 255]);
74        let array =
75            Delta::try_from_primitive_array(&primitive, &mut SESSION.create_execution_ctx())
76                .unwrap();
77
78        let casted = array
79            .into_array()
80            .cast(DType::Primitive(PType::U32, Nullability::NonNullable))
81            .unwrap();
82
83        assert_arrays_eq!(
84            casted,
85            PrimitiveArray::from_iter([200u32, 50, 75, 10, 255]),
86            &mut SESSION.create_execution_ctx()
87        );
88    }
89
90    #[test]
91    fn test_cast_delta_same_width_float_target() {
92        // u32 and f32 share a bit width but are not interchangeable: the fast path must not
93        // reuse the integer components for a float target (that would error in `try_new`).
94        // The cast must succeed via the decompress-and-re-encode fallback.
95        let primitive = PrimitiveArray::from_iter([10u32, 20, 30, 40, 50]);
96        let array =
97            Delta::try_from_primitive_array(&primitive, &mut SESSION.create_execution_ctx())
98                .unwrap();
99
100        let casted = array
101            .into_array()
102            .cast(DType::Primitive(PType::F32, Nullability::NonNullable))
103            .unwrap();
104
105        assert_arrays_eq!(
106            casted,
107            PrimitiveArray::from_iter([10f32, 20.0, 30.0, 40.0, 50.0]),
108            &mut SESSION.create_execution_ctx()
109        );
110    }
111
112    #[test]
113    fn test_cast_delta_add_nullability() -> VortexResult<()> {
114        // Same ptype, only adding nullability — handled by the kernel without decompressing.
115        let values = PrimitiveArray::from_iter([10u32, 20, 5, 30, 15]);
116        let array = Delta::try_from_primitive_array(&values, &mut SESSION.create_execution_ctx())?;
117
118        let casted = array
119            .into_array()
120            .cast(DType::Primitive(PType::U32, Nullability::Nullable))?;
121
122        assert_eq!(
123            casted.dtype(),
124            &DType::Primitive(PType::U32, Nullability::Nullable)
125        );
126        assert_arrays_eq!(
127            casted,
128            PrimitiveArray::from_option_iter([Some(10u32), Some(20), Some(5), Some(30), Some(15)]),
129            &mut SESSION.create_execution_ctx()
130        );
131        Ok(())
132    }
133
134    #[test]
135    fn test_cast_delta_nullability_preserves_nulls() -> VortexResult<()> {
136        // A nullable Delta array carries its validity in the deltas child; a same-ptype
137        // nullability cast must round-trip the null positions.
138        let values =
139            PrimitiveArray::from_option_iter([Some(10u32), None, Some(30), Some(15), None]);
140        let array = Delta::try_from_primitive_array(&values, &mut SESSION.create_execution_ctx())?;
141
142        let casted = array
143            .into_array()
144            .cast(DType::Primitive(PType::U32, Nullability::Nullable))?;
145
146        assert_arrays_eq!(
147            casted,
148            PrimitiveArray::from_option_iter([Some(10u32), None, Some(30), Some(15), None]),
149            &mut SESSION.create_execution_ctx()
150        );
151        Ok(())
152    }
153
154    #[test]
155    fn test_cast_delta_drop_nullability_with_nulls_errors() -> VortexResult<()> {
156        // Dropping nullability when real nulls are present must error, matching the generic
157        // cast semantics rather than silently discarding the null mask.
158        let values = PrimitiveArray::from_option_iter([Some(10u32), None, Some(30)]);
159        let array = Delta::try_from_primitive_array(&values, &mut SESSION.create_execution_ctx())?;
160
161        // The cast is lazy; the nullability check fires when it is executed.
162        #[expect(deprecated)]
163        let result = array
164            .into_array()
165            .cast(DType::Primitive(PType::U32, Nullability::NonNullable))
166            .and_then(|a| a.to_canonical().map(|c| c.into_array()));
167
168        assert!(
169            result.is_err(),
170            "dropping nullability with real nulls must error, got {result:?}"
171        );
172        Ok(())
173    }
174
175    #[test]
176    fn test_cast_delta_u8_to_u32() {
177        let primitive = PrimitiveArray::from_iter([10u8, 20, 30, 40, 50]);
178        let array =
179            Delta::try_from_primitive_array(&primitive, &mut SESSION.create_execution_ctx())
180                .unwrap();
181
182        let casted = array
183            .into_array()
184            .cast(DType::Primitive(PType::U32, Nullability::NonNullable))
185            .unwrap();
186        assert_eq!(
187            casted.dtype(),
188            &DType::Primitive(PType::U32, Nullability::NonNullable)
189        );
190
191        // Verify by decoding
192        assert_arrays_eq!(
193            casted,
194            PrimitiveArray::from_iter([10u32, 20, 30, 40, 50]),
195            &mut SESSION.create_execution_ctx()
196        );
197    }
198
199    #[test]
200    fn test_cast_delta_nullable() {
201        // A combined ptype + nullability change goes through the generic decompress path.
202        let values = PrimitiveArray::new(
203            buffer![100u16, 0, 200, 300, 0],
204            vortex_array::validity::Validity::NonNullable,
205        );
206        let array =
207            Delta::try_from_primitive_array(&values, &mut SESSION.create_execution_ctx()).unwrap();
208
209        let casted = array
210            .into_array()
211            .cast(DType::Primitive(PType::U32, Nullability::Nullable))
212            .unwrap();
213        assert_eq!(
214            casted.dtype(),
215            &DType::Primitive(PType::U32, Nullability::Nullable)
216        );
217    }
218
219    #[rstest]
220    #[case::u8(
221        PrimitiveArray::new(
222            buffer![0u8, 10, 20, 30, 40, 50],
223            vortex_array::validity::Validity::NonNullable,
224        )
225    )]
226    #[case::u16(
227        PrimitiveArray::new(
228            buffer![0u16, 100, 200, 300, 400, 500],
229            vortex_array::validity::Validity::NonNullable,
230        )
231    )]
232    #[case::u32(
233        PrimitiveArray::new(
234            buffer![0u32, 1000, 2000, 3000, 4000],
235            vortex_array::validity::Validity::NonNullable,
236        )
237    )]
238    #[case::u64(
239        PrimitiveArray::new(
240            buffer![0u64, 10000, 20000, 30000],
241            vortex_array::validity::Validity::NonNullable,
242        )
243    )]
244    fn test_cast_delta_conformance(#[case] primitive: PrimitiveArray) {
245        let delta_array =
246            Delta::try_from_primitive_array(&primitive, &mut SESSION.create_execution_ctx())
247                .unwrap();
248        test_cast_conformance(
249            &delta_array.into_array(),
250            &mut SESSION.create_execution_ctx(),
251        );
252    }
253}