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