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 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 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 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 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 &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 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
257fn 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 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}