1mod kernel;
5
6use std::fmt::Display;
7use std::fmt::Formatter;
8
9pub use kernel::*;
10use vortex_error::VortexExpect as _;
11use vortex_error::VortexResult;
12use vortex_error::vortex_ensure;
13use vortex_mask::Mask;
14use vortex_mask::MaskValues;
15use vortex_session::VortexSession;
16use vortex_session::registry::CachedId;
17
18use crate::ArrayRef;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::arrays::BoolArray;
22use crate::arrays::bool::BoolArrayExt;
23use crate::builders::ArrayBuilder;
24use crate::builders::builder_with_capacity;
25use crate::builtins::ArrayBuiltins;
26use crate::dtype::DType;
27use crate::expr::Expression;
28use crate::expr::display::ExprDisplay;
29use crate::scalar_fn::Arity;
30use crate::scalar_fn::ChildName;
31use crate::scalar_fn::EmptyOptions;
32use crate::scalar_fn::ExecutionArgs;
33use crate::scalar_fn::ScalarFnId;
34use crate::scalar_fn::ScalarFnVTable;
35use crate::scalar_fn::SimplifyCtx;
36use crate::scalar_fn::fns::literal::Literal;
37use crate::validity::Validity;
38
39#[derive(Clone)]
47pub struct Zip;
48
49impl ScalarFnVTable for Zip {
50 type Options = EmptyOptions;
51
52 fn id(&self) -> ScalarFnId {
53 static ID: CachedId = CachedId::new("vortex.zip");
54 *ID
55 }
56
57 fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
58 Ok(Some(vec![]))
59 }
60
61 fn deserialize(
62 &self,
63 _metadata: &[u8],
64 _session: &VortexSession,
65 ) -> VortexResult<Self::Options> {
66 Ok(EmptyOptions)
67 }
68
69 fn arity(&self, _options: &Self::Options) -> Arity {
70 Arity::Exact(3)
71 }
72
73 fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
74 match child_idx {
75 0 => ChildName::from("if_true"),
76 1 => ChildName::from("if_false"),
77 2 => ChildName::from("mask"),
78 _ => unreachable!("Invalid child index {} for Zip expression", child_idx),
79 }
80 }
81
82 fn fmt_sql(
83 &self,
84 _options: &Self::Options,
85 expr: &dyn ExprDisplay,
86 f: &mut Formatter<'_>,
87 ) -> std::fmt::Result {
88 write!(f, "zip(")?;
89 Display::fmt(expr.display_child(0), f)?;
90 write!(f, ", ")?;
91 Display::fmt(expr.display_child(1), f)?;
92 write!(f, ", ")?;
93 Display::fmt(expr.display_child(2), f)?;
94 write!(f, ")")
95 }
96
97 fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
98 vortex_ensure!(
99 matches!(arg_dtypes[2], DType::Bool(_)),
100 "zip requires mask to be a boolean type, got {}",
101 arg_dtypes[2]
102 );
103 zip_return_dtype(&arg_dtypes[0], &arg_dtypes[1])
104 }
105
106 fn execute(
107 &self,
108 _options: &Self::Options,
109 args: &dyn ExecutionArgs,
110 ctx: &mut ExecutionCtx,
111 ) -> VortexResult<ArrayRef> {
112 let if_true = args.get(0)?;
113 let if_false = args.get(1)?;
114 let mask_array = args.get(2)?;
115
116 let mask = mask_array
117 .execute::<BoolArray>(ctx)?
118 .to_mask_fill_null_false(ctx);
119
120 let return_dtype = zip_return_dtype(if_true.dtype(), if_false.dtype())?;
121
122 if mask.all_true() {
123 return if_true.cast(return_dtype)?.execute(ctx);
124 }
125
126 if mask.all_false() {
127 return if_false.cast(return_dtype)?.execute(ctx);
128 }
129
130 if !if_true.is_canonical() || !if_false.is_canonical() {
131 let if_true = if_true.execute::<ArrayRef>(ctx)?;
132 let if_false = if_false.execute::<ArrayRef>(ctx)?;
133 return mask.into_array().zip(if_true, if_false);
134 }
135
136 zip_impl(&if_true, &if_false, &mask, ctx)
137 }
138
139 fn simplify(
140 &self,
141 _options: &Self::Options,
142 expr: &Expression,
143 _ctx: &dyn SimplifyCtx,
144 ) -> VortexResult<Option<Expression>> {
145 let Some(mask_lit) = expr.child(2).as_opt::<Literal>() else {
146 return Ok(None);
147 };
148
149 if let Some(mask_val) = mask_lit.as_bool().value() {
150 if mask_val {
151 return Ok(Some(expr.child(0).clone()));
152 } else {
153 return Ok(Some(expr.child(1).clone()));
154 }
155 }
156
157 Ok(None)
158 }
159
160 fn is_strict(&self, _options: &Self::Options) -> bool {
161 false
163 }
164
165 fn is_fallible(&self, _options: &Self::Options) -> bool {
166 false
167 }
168}
169
170pub(crate) fn zip_impl(
171 if_true: &ArrayRef,
172 if_false: &ArrayRef,
173 mask: &Mask,
174 ctx: &mut ExecutionCtx,
175) -> VortexResult<ArrayRef> {
176 assert_eq!(
177 if_true.len(),
178 if_false.len(),
179 "zip requires arrays to have the same size"
180 );
181
182 let return_type = zip_return_dtype(if_true.dtype(), if_false.dtype())?;
183
184 if mask.all_true() {
185 return if_true.cast(return_type);
186 }
187 if mask.all_false() {
188 return if_false.cast(return_type);
189 }
190
191 let if_true = if_true.cast(return_type.clone())?;
194 let if_false = if_false.cast(return_type.clone())?;
195
196 zip_impl_with_builder(
197 &if_true,
198 &if_false,
199 mask.values()
200 .vortex_expect("zip_impl_with_builder: mask is not all-true or all-false"),
201 builder_with_capacity(&return_type, if_true.len()),
202 ctx,
203 )
204}
205
206fn zip_return_dtype(if_true: &DType, if_false: &DType) -> VortexResult<DType> {
207 vortex_ensure!(
208 if_true.eq_ignore_nullability(if_false),
209 "zip requires if_true and if_false to have the same base type, got {} and {}",
210 if_true,
211 if_false
212 );
213 Ok(if_true
214 .least_supertype(if_false)
215 .vortex_expect("zip inputs with the same base type must have a common dtype"))
216}
217
218fn zip_impl_with_builder(
219 if_true: &ArrayRef,
220 if_false: &ArrayRef,
221 mask: &MaskValues,
222 mut builder: Box<dyn ArrayBuilder>,
223 ctx: &mut ExecutionCtx,
224) -> VortexResult<ArrayRef> {
225 for (start, end) in mask.slices() {
226 if builder.len() < *start {
227 if_false
228 .slice(builder.len()..*start)?
229 .append_to_builder(builder.as_mut(), ctx)?;
230 }
231 if_true
232 .slice(*start..*end)?
233 .append_to_builder(builder.as_mut(), ctx)?;
234 }
235 if builder.len() < if_false.len() {
236 if_false
237 .slice(builder.len()..if_false.len())?
238 .append_to_builder(builder.as_mut(), ctx)?;
239 }
240 Ok(builder.finish())
241}
242
243pub(crate) fn zip_validity(
252 if_true: Validity,
253 if_false: Validity,
254 mask: &Mask,
255) -> VortexResult<Validity> {
256 match (&if_true, &if_false) {
257 (Validity::NonNullable, Validity::NonNullable) => return Ok(Validity::NonNullable),
258 (Validity::AllValid, Validity::AllValid) => return Ok(Validity::AllValid),
259 (Validity::AllInvalid, Validity::AllInvalid) => return Ok(Validity::AllInvalid),
260 _ => {}
261 }
262
263 let len = mask.len();
264 let validity = mask
265 .clone()
266 .into_array()
267 .zip(if_true.to_array(len), if_false.to_array(len))?;
268 Ok(Validity::Array(validity))
269}
270
271#[cfg(test)]
272mod tests {
273 use vortex_buffer::buffer;
274 use vortex_error::VortexResult;
275 use vortex_mask::Mask;
276
277 use super::zip_impl;
278 use crate::ArrayRef;
279 use crate::IntoArray;
280 use crate::VortexSessionExecute;
281 use crate::array_session;
282 use crate::arrays::ConstantArray;
283 use crate::arrays::PrimitiveArray;
284 use crate::arrays::Struct;
285 use crate::arrays::StructArray;
286 use crate::arrays::VarBinView;
287 use crate::arrays::VarBinViewArray;
288 use crate::assert_arrays_eq;
289 use crate::builders::ArrayBuilder;
290 use crate::builders::BufferGrowthStrategy;
291 use crate::builders::VarBinViewBuilder;
292 use crate::builtins::ArrayBuiltins;
293 use crate::columnar::Columnar;
294 use crate::dtype::DType;
295 use crate::dtype::Nullability;
296 use crate::dtype::PType;
297 use crate::expr::lit;
298 use crate::expr::root;
299 use crate::expr::zip_expr;
300 use crate::scalar::Scalar;
301
302 #[test]
303 fn dtype() {
304 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
305 let expr = zip_expr(lit(true), root(), lit(0i32));
306 let result_dtype = expr.return_dtype(&dtype).unwrap();
307 assert_eq!(
308 result_dtype,
309 DType::Primitive(PType::I32, Nullability::NonNullable)
310 );
311 }
312
313 #[test]
314 fn test_display() {
315 let expr = zip_expr(lit(true), root(), lit(0i32));
316 assert_eq!(expr.to_string(), "zip($, 0i32, true)");
317 }
318
319 #[test]
320 fn test_zip_basic() {
321 let mut ctx = array_session().create_execution_ctx();
322 let mask = Mask::from_iter([true, false, false, true, false]);
323 let if_true = buffer![10, 20, 30, 40, 50].into_array();
324 let if_false = buffer![1, 2, 3, 4, 5].into_array();
325
326 let result = mask.into_array().zip(if_true, if_false).unwrap();
327 let expected = buffer![10, 2, 3, 40, 5].into_array();
328
329 assert_arrays_eq!(result, expected, &mut ctx);
330 }
331
332 #[test]
333 fn test_zip_all_true() {
334 let mut ctx = array_session().create_execution_ctx();
335 let mask = Mask::new_true(4);
336 let if_true = buffer![10, 20, 30, 40].into_array();
337 let if_false =
338 PrimitiveArray::from_option_iter([Some(1), Some(2), Some(3), None]).into_array();
339
340 let result = mask.into_array().zip(if_true, if_false.clone()).unwrap();
341 let expected =
342 PrimitiveArray::from_option_iter([Some(10), Some(20), Some(30), Some(40)]).into_array();
343
344 assert_arrays_eq!(result, expected, &mut ctx);
345 assert_eq!(result.dtype(), if_false.dtype())
346 }
347
348 #[test]
349 fn test_zip_all_false_widens_nullability() {
350 let mut ctx = array_session().create_execution_ctx();
351 let mask = Mask::new_false(4);
352 let if_true =
353 PrimitiveArray::from_option_iter([Some(10), Some(20), Some(30), None]).into_array();
354 let if_false = buffer![1i32, 2, 3, 4].into_array();
355
356 let result = mask.into_array().zip(if_true.clone(), if_false).unwrap();
357 let expected =
358 PrimitiveArray::from_option_iter([Some(1), Some(2), Some(3), Some(4)]).into_array();
359
360 assert_arrays_eq!(result, expected, &mut ctx);
361 assert_eq!(result.dtype(), if_true.dtype());
362 }
363
364 #[test]
365 fn test_zip_impl_all_true_widens_nullability() -> VortexResult<()> {
366 let mask = Mask::new_true(4);
367 let if_true = buffer![10i32, 20, 30, 40].into_array();
368 let if_false =
369 PrimitiveArray::from_option_iter([Some(1), Some(2), Some(3), None]).into_array();
370
371 let mut ctx = array_session().create_execution_ctx();
372 let result = zip_impl(&if_true, &if_false, &mask, &mut ctx)?;
373 assert_arrays_eq!(
374 result,
375 PrimitiveArray::from_option_iter([Some(10i32), Some(20), Some(30), Some(40)])
376 .into_array(),
377 &mut ctx
378 );
379 assert_eq!(result.dtype(), if_false.dtype());
380 Ok(())
381 }
382
383 #[test]
384 fn test_zip_impl_all_false_widens_nullability() -> VortexResult<()> {
385 let mask = Mask::new_false(4);
386 let if_true =
387 PrimitiveArray::from_option_iter([Some(10), Some(20), Some(30), None]).into_array();
388 let if_false = buffer![1i32, 2, 3, 4].into_array();
389
390 let mut ctx = array_session().create_execution_ctx();
391 let result = zip_impl(&if_true, &if_false, &mask, &mut ctx)?;
392 assert_arrays_eq!(
393 result,
394 PrimitiveArray::from_option_iter([Some(1i32), Some(2), Some(3), Some(4)]).into_array(),
395 &mut ctx
396 );
397 assert_eq!(result.dtype(), if_true.dtype());
398 Ok(())
399 }
400
401 #[test]
402 #[should_panic]
403 fn test_invalid_lengths() {
404 let mask = Mask::new_false(4);
405 let if_true = buffer![10, 20, 30].into_array();
406 let if_false = buffer![1, 2, 3, 4].into_array();
407
408 let _result = mask.into_array().zip(if_true, if_false).unwrap();
409 }
410
411 #[test]
412 fn test_fragmentation() -> VortexResult<()> {
413 let len = 100;
414
415 let const1 = ConstantArray::new(
416 Scalar::utf8("hello_this_is_a_longer_string", Nullability::Nullable),
417 len,
418 )
419 .into_array();
420
421 let const2 = ConstantArray::new(
422 Scalar::utf8("world_this_is_another_string", Nullability::Nullable),
423 len,
424 )
425 .into_array();
426
427 let indices: Vec<usize> = (0..len).step_by(2).collect();
428 let mask = Mask::from_indices(len, indices);
429 let mask_array = mask.into_array();
430
431 let mut ctx = array_session().create_execution_ctx();
432 let result = mask_array
433 .zip(const1.clone(), const2.clone())?
434 .execute::<Columnar>(&mut ctx)?
435 .into_array();
436
437 insta::assert_snapshot!(result.display_tree(), @r"
438 root: vortex.varbinview(utf8?, len=100) nbytes=1.66 kB (100.00%) [all_valid]
439 metadata:
440 buffer: buffer_0 host 29 B (align=1) (1.75%)
441 buffer: buffer_1 host 28 B (align=1) (1.69%)
442 buffer: views host 1.60 kB (align=16) (96.56%)
443 ");
444
445 let wrapped1 = StructArray::try_from_iter([("nested", const1)])?.into_array();
446 let wrapped2 = StructArray::try_from_iter([("nested", const2)])?.into_array();
447
448 let wrapped_result = mask_array
449 .zip(wrapped1, wrapped2)?
450 .execute::<ArrayRef>(&mut ctx)?;
451 assert!(wrapped_result.is::<Struct>());
452
453 Ok(())
454 }
455
456 #[test]
457 fn test_varbinview_zip() {
458 let if_true = {
459 let mut builder = VarBinViewBuilder::new(
460 DType::Utf8(Nullability::NonNullable),
461 10,
462 Default::default(),
463 BufferGrowthStrategy::fixed(64 * 1024),
464 0.0,
465 );
466 for _ in 0..100 {
467 builder.append_value("Hello");
468 builder.append_value("Hello this is a long string that won't be inlined.");
469 }
470 builder.finish()
471 };
472
473 let if_false = {
474 let mut builder = VarBinViewBuilder::new(
475 DType::Utf8(Nullability::NonNullable),
476 10,
477 Default::default(),
478 BufferGrowthStrategy::fixed(64 * 1024),
479 0.0,
480 );
481 for _ in 0..100 {
482 builder.append_value("Hello2");
483 builder.append_value("Hello2 this is a long string that won't be inlined.");
484 }
485 builder.finish()
486 };
487
488 let mask = Mask::from_indices(200, (0..100).filter(|i| i % 3 != 0));
489 let mask_array = mask.clone().into_array();
490
491 let mut ctx = array_session().create_execution_ctx();
492 let zipped = mask_array
493 .zip(if_true, if_false)
494 .unwrap()
495 .execute::<ArrayRef>(&mut ctx)
496 .unwrap();
497 let zipped = zipped.as_opt::<VarBinView>().unwrap();
498 assert_eq!(zipped.data_buffers().len(), 2);
499
500 let true_value = |i: usize| {
501 if i.is_multiple_of(2) {
502 "Hello"
503 } else {
504 "Hello this is a long string that won't be inlined."
505 }
506 };
507 let false_value = |i: usize| {
508 if i.is_multiple_of(2) {
509 "Hello2"
510 } else {
511 "Hello2 this is a long string that won't be inlined."
512 }
513 };
514 let expected = VarBinViewArray::from_iter_str((0..200).map(|i| {
515 if mask.value(i) {
516 true_value(i)
517 } else {
518 false_value(i)
519 }
520 }));
521 assert_arrays_eq!(zipped.array().clone(), expected, &mut ctx);
522 }
523}