Skip to main content

vortex_array/scalar_fn/fns/zip/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod 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/// An expression that conditionally selects between two arrays based on a boolean mask.
40///
41/// For each position `i`, `result[i] = if mask[i] then if_true[i] else if_false[i]`.
42///
43/// Null values in the mask are treated as false (selecting `if_false`). This follows
44/// SQL semantics (DuckDB, Trino) where a null condition falls through to the ELSE branch,
45/// rather than Arrow's `if_else` which propagates null conditions to the output.
46#[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        // A null in an unselected branch does not force a null output.
162        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    // `append_to_builder` requires exact dtype equality, so normalize branch
192    // nullability to the output dtype before appending slices into the builder.
193    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
243/// Combine two validities for a row-wise zip: take `if_true`'s validity where `mask` is set and
244/// `if_false`'s where it is not.
245///
246/// That selection is itself a zip over the two boolean validity bitmaps, so it is built as a (lazy)
247/// zip array — reusing the zip machinery rather than re-deriving the mask algebra. Trivial cases
248/// where both sides' validity already agrees skip the zip. `mask` must already be null-filled so the
249/// selection matches the accompanying value selection. Shared by the per-encoding zip kernels (e.g.
250/// `Bool`, `Primitive`) that build their result directly.
251pub(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}