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 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 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
252fn 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 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}