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;
8use std::sync::Arc;
9
10pub use kernel::*;
11use vortex_error::VortexExpect as _;
12use vortex_error::VortexResult;
13use vortex_error::vortex_ensure;
14use vortex_error::vortex_err;
15use vortex_mask::Mask;
16use vortex_mask::MaskValues;
17use vortex_session::VortexSession;
18use vortex_session::registry::CachedId;
19
20use crate::ArrayRef;
21use crate::ExecutionCtx;
22use crate::IntoArray;
23use crate::arrays::BoolArray;
24use crate::arrays::ScalarFnArray;
25use crate::arrays::bool::BoolArrayExt;
26use crate::builders::ArrayBuilder;
27use crate::builders::builder_with_capacity;
28use crate::builtins::ArrayBuiltins;
29use crate::dtype::DType;
30use crate::dtype::StructFields;
31use crate::expr::Expression;
32use crate::expr::display::ExprDisplay;
33use crate::scalar_fn::Arity;
34use crate::scalar_fn::ChildName;
35use crate::scalar_fn::EmptyOptions;
36use crate::scalar_fn::ExecutionArgs;
37use crate::scalar_fn::ScalarFnId;
38use crate::scalar_fn::ScalarFnVTable;
39use crate::scalar_fn::ScalarFnVTableExt;
40use crate::scalar_fn::SimplifyCtx;
41use crate::scalar_fn::fns::literal::Literal;
42use crate::validity::Validity;
43
44/// An expression that conditionally selects between two arrays based on a boolean mask.
45///
46/// For each position `i`, `result[i] = if mask[i] then if_true[i] else if_false[i]`.
47///
48/// Null values in the mask are treated as false (selecting `if_false`). This follows
49/// SQL semantics (DuckDB, Trino) where a null condition falls through to the ELSE branch,
50/// rather than Arrow's `if_else` which propagates null conditions to the output.
51#[derive(Clone)]
52pub struct Zip;
53
54impl Zip {
55    /// Creates a lazy conditional selection between `if_true` and `if_false`.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the children have different lengths, the values have incompatible
60    /// dtypes, or `mask` is not boolean data.
61    pub fn try_new(
62        if_true: ArrayRef,
63        if_false: ArrayRef,
64        mask: ArrayRef,
65    ) -> VortexResult<ScalarFnArray> {
66        ScalarFnArray::try_new(Zip.bind(EmptyOptions), vec![if_true, if_false, mask])
67    }
68}
69
70impl ScalarFnVTable for Zip {
71    type Options = EmptyOptions;
72
73    fn id(&self) -> ScalarFnId {
74        static ID: CachedId = CachedId::new("vortex.zip");
75        *ID
76    }
77
78    fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
79        Ok(Some(vec![]))
80    }
81
82    fn deserialize(
83        &self,
84        _metadata: &[u8],
85        _session: &VortexSession,
86    ) -> VortexResult<Self::Options> {
87        Ok(EmptyOptions)
88    }
89
90    fn arity(&self, _options: &Self::Options) -> Arity {
91        Arity::Exact(3)
92    }
93
94    fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
95        match child_idx {
96            0 => ChildName::from("if_true"),
97            1 => ChildName::from("if_false"),
98            2 => ChildName::from("mask"),
99            _ => unreachable!("Invalid child index {} for Zip expression", child_idx),
100        }
101    }
102
103    fn fmt_sql(
104        &self,
105        _options: &Self::Options,
106        expr: &dyn ExprDisplay,
107        f: &mut Formatter<'_>,
108    ) -> std::fmt::Result {
109        write!(f, "zip(")?;
110        Display::fmt(expr.display_child(0), f)?;
111        write!(f, ", ")?;
112        Display::fmt(expr.display_child(1), f)?;
113        write!(f, ", ")?;
114        Display::fmt(expr.display_child(2), f)?;
115        write!(f, ")")
116    }
117
118    fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
119        vortex_ensure!(
120            matches!(arg_dtypes[2], DType::Bool(_)),
121            "zip requires mask to be a boolean type, got {}",
122            arg_dtypes[2]
123        );
124        zip_return_dtype(&arg_dtypes[0], &arg_dtypes[1])
125    }
126
127    fn execute(
128        &self,
129        _options: &Self::Options,
130        args: &dyn ExecutionArgs,
131        ctx: &mut ExecutionCtx,
132    ) -> VortexResult<ArrayRef> {
133        let if_true = args.get(0)?;
134        let if_false = args.get(1)?;
135        let mask_array = args.get(2)?;
136
137        let mask = mask_array
138            .execute::<BoolArray>(ctx)?
139            .to_mask_fill_null_false(ctx);
140
141        let return_dtype = zip_return_dtype(if_true.dtype(), if_false.dtype())?;
142
143        if mask.all_true() {
144            return if_true.cast(return_dtype)?.execute(ctx);
145        }
146
147        if mask.all_false() {
148            return if_false.cast(return_dtype)?.execute(ctx);
149        }
150
151        if !if_true.is_canonical() || !if_false.is_canonical() {
152            let if_true = if_true.execute::<ArrayRef>(ctx)?;
153            let if_false = if_false.execute::<ArrayRef>(ctx)?;
154            return mask.into_array().zip(if_true, if_false);
155        }
156
157        zip_impl(&if_true, &if_false, &mask, ctx)
158    }
159
160    fn simplify(
161        &self,
162        _options: &Self::Options,
163        expr: &Expression,
164        _ctx: &dyn SimplifyCtx,
165    ) -> VortexResult<Option<Expression>> {
166        let Some(mask_lit) = expr.child(2).as_opt::<Literal>() else {
167            return Ok(None);
168        };
169
170        if let Some(mask_val) = mask_lit.as_bool().value() {
171            if mask_val {
172                return Ok(Some(expr.child(0).clone()));
173            } else {
174                return Ok(Some(expr.child(1).clone()));
175            }
176        }
177
178        Ok(None)
179    }
180
181    fn is_strict(&self, _options: &Self::Options) -> bool {
182        // A null in an unselected branch does not force a null output.
183        false
184    }
185
186    fn is_infallible(&self, _options: &Self::Options) -> bool {
187        true
188    }
189}
190
191pub(crate) fn zip_impl(
192    if_true: &ArrayRef,
193    if_false: &ArrayRef,
194    mask: &Mask,
195    ctx: &mut ExecutionCtx,
196) -> VortexResult<ArrayRef> {
197    assert_eq!(
198        if_true.len(),
199        if_false.len(),
200        "zip requires arrays to have the same size"
201    );
202
203    let return_type = zip_return_dtype(if_true.dtype(), if_false.dtype())?;
204
205    if mask.all_true() {
206        return if_true.cast(return_type);
207    }
208    if mask.all_false() {
209        return if_false.cast(return_type);
210    }
211
212    // `append_to_builder` requires exact dtype equality, so normalize branch
213    // nullability to the output dtype before appending slices into the builder.
214    let if_true = if_true.cast(return_type.clone())?;
215    let if_false = if_false.cast(return_type.clone())?;
216
217    zip_impl_with_builder(
218        &if_true,
219        &if_false,
220        mask.values()
221            .vortex_expect("zip_impl_with_builder: mask is not all-true or all-false"),
222        builder_with_capacity(&return_type, if_true.len()),
223        ctx,
224    )
225}
226
227fn zip_return_dtype(if_true: &DType, if_false: &DType) -> VortexResult<DType> {
228    zip_nullability_union(if_true, if_false).ok_or_else(|| {
229        vortex_err!(
230            "zip requires if_true and if_false to have the same base type, got {} and {}",
231            if_true,
232            if_false
233        )
234    })
235}
236
237fn zip_nullability_union(lhs: &DType, rhs: &DType) -> Option<DType> {
238    let nullability = lhs.nullability() | rhs.nullability();
239
240    match (lhs, rhs) {
241        (DType::List(lhs_element, _), DType::List(rhs_element, _)) => Some(DType::List(
242            Arc::new(zip_nullability_union(lhs_element, rhs_element)?),
243            nullability,
244        )),
245        (
246            DType::FixedSizeList(lhs_element, lhs_size, _),
247            DType::FixedSizeList(rhs_element, rhs_size, _),
248        ) if lhs_size == rhs_size => Some(DType::FixedSizeList(
249            Arc::new(zip_nullability_union(lhs_element, rhs_element)?),
250            *lhs_size,
251            nullability,
252        )),
253        (DType::Map(lhs_map, _), DType::Map(rhs_map, _))
254            if lhs_map.keys_sorted() == rhs_map.keys_sorted() =>
255        {
256            DType::map(
257                zip_nullability_union(&lhs_map.key_dtype(), &rhs_map.key_dtype())?,
258                zip_nullability_union(&lhs_map.value_dtype(), &rhs_map.value_dtype())?,
259                lhs_map.keys_sorted(),
260                nullability,
261            )
262            .ok()
263        }
264        (DType::Struct(lhs_fields, _), DType::Struct(rhs_fields, _))
265            if lhs_fields.names() == rhs_fields.names() =>
266        {
267            let fields = lhs_fields
268                .fields()
269                .zip(rhs_fields.fields())
270                .map(|(lhs, rhs)| zip_nullability_union(&lhs, &rhs))
271                .collect::<Option<Vec<_>>>()?;
272            Some(DType::Struct(
273                StructFields::new(lhs_fields.names().clone(), fields),
274                nullability,
275            ))
276        }
277        (DType::Union(lhs_variants, _), DType::Union(rhs_variants, _))
278            if lhs_variants == rhs_variants =>
279        {
280            Some(DType::Union(lhs_variants.clone(), nullability))
281        }
282        _ if lhs.eq_ignore_nullability(rhs) => Some(lhs.with_nullability(nullability)),
283        _ => None,
284    }
285}
286
287fn zip_impl_with_builder(
288    if_true: &ArrayRef,
289    if_false: &ArrayRef,
290    mask: &MaskValues,
291    mut builder: Box<dyn ArrayBuilder>,
292    ctx: &mut ExecutionCtx,
293) -> VortexResult<ArrayRef> {
294    for (start, end) in mask.slices() {
295        if builder.len() < *start {
296            if_false
297                .slice(builder.len()..*start)?
298                .append_to_builder(builder.as_mut(), ctx)?;
299        }
300        if_true
301            .slice(*start..*end)?
302            .append_to_builder(builder.as_mut(), ctx)?;
303    }
304    if builder.len() < if_false.len() {
305        if_false
306            .slice(builder.len()..if_false.len())?
307            .append_to_builder(builder.as_mut(), ctx)?;
308    }
309    Ok(builder.finish())
310}
311
312/// Combine two validities for a row-wise zip: take `if_true`'s validity where `mask` is set and
313/// `if_false`'s where it is not.
314///
315/// That selection is itself a zip over the two boolean validity bitmaps, so it is built as a (lazy)
316/// zip array — reusing the zip machinery rather than re-deriving the mask algebra. Trivial cases
317/// where both sides' validity already agrees skip the zip. `mask` must already be null-filled so the
318/// selection matches the accompanying value selection. Shared by the per-encoding zip kernels (e.g.
319/// `Bool`, `Primitive`) that build their result directly.
320pub(crate) fn zip_validity(
321    if_true: Validity,
322    if_false: Validity,
323    mask: &Mask,
324) -> VortexResult<Validity> {
325    match (&if_true, &if_false) {
326        (Validity::NonNullable, Validity::NonNullable) => return Ok(Validity::NonNullable),
327        (Validity::AllValid, Validity::AllValid) => return Ok(Validity::AllValid),
328        (Validity::AllInvalid, Validity::AllInvalid) => return Ok(Validity::AllInvalid),
329        _ => {}
330    }
331
332    let len = mask.len();
333    let validity = mask
334        .clone()
335        .into_array()
336        .zip(if_true.to_array(len), if_false.to_array(len))?;
337    Ok(Validity::Array(validity))
338}
339
340#[cfg(test)]
341mod tests {
342    use vortex_buffer::buffer;
343    use vortex_error::VortexResult;
344    use vortex_mask::Mask;
345
346    use super::zip_impl;
347    use crate::ArrayRef;
348    use crate::IntoArray;
349    use crate::VortexSessionExecute;
350    use crate::array_session;
351    use crate::arrays::ConstantArray;
352    use crate::arrays::PrimitiveArray;
353    use crate::arrays::Struct;
354    use crate::arrays::StructArray;
355    use crate::arrays::VarBinView;
356    use crate::arrays::VarBinViewArray;
357    use crate::assert_arrays_eq;
358    use crate::builders::ArrayBuilder;
359    use crate::builders::BufferGrowthStrategy;
360    use crate::builders::VarBinViewBuilder;
361    use crate::builtins::ArrayBuiltins;
362    use crate::columnar::Columnar;
363    use crate::dtype::DType;
364    use crate::dtype::Nullability;
365    use crate::dtype::PType;
366    use crate::expr::lit;
367    use crate::expr::root;
368    use crate::expr::zip_expr;
369    use crate::scalar::Scalar;
370
371    #[test]
372    fn dtype() {
373        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
374        let expr = zip_expr(lit(true), root(), lit(0i32));
375        let result_dtype = expr.return_dtype(&dtype).unwrap();
376        assert_eq!(
377            result_dtype,
378            DType::Primitive(PType::I32, Nullability::NonNullable)
379        );
380    }
381
382    #[test]
383    fn test_display() {
384        let expr = zip_expr(lit(true), root(), lit(0i32));
385        assert_eq!(expr.to_string(), "zip($, 0i32, true)");
386    }
387
388    #[test]
389    fn test_zip_basic() {
390        let mut ctx = array_session().create_execution_ctx();
391        let mask = Mask::from_iter([true, false, false, true, false]);
392        let if_true = buffer![10, 20, 30, 40, 50].into_array();
393        let if_false = buffer![1, 2, 3, 4, 5].into_array();
394
395        let result = mask.into_array().zip(if_true, if_false).unwrap();
396        let expected = buffer![10, 2, 3, 40, 5].into_array();
397
398        assert_arrays_eq!(result, expected, &mut ctx);
399    }
400
401    #[test]
402    fn test_zip_all_true() {
403        let mut ctx = array_session().create_execution_ctx();
404        let mask = Mask::new_true(4);
405        let if_true = buffer![10, 20, 30, 40].into_array();
406        let if_false =
407            PrimitiveArray::from_option_iter([Some(1), Some(2), Some(3), None]).into_array();
408
409        let result = mask.into_array().zip(if_true, if_false.clone()).unwrap();
410        let expected =
411            PrimitiveArray::from_option_iter([Some(10), Some(20), Some(30), Some(40)]).into_array();
412
413        assert_arrays_eq!(result, expected, &mut ctx);
414        assert_eq!(result.dtype(), if_false.dtype())
415    }
416
417    #[test]
418    fn test_zip_all_false_widens_nullability() {
419        let mut ctx = array_session().create_execution_ctx();
420        let mask = Mask::new_false(4);
421        let if_true =
422            PrimitiveArray::from_option_iter([Some(10), Some(20), Some(30), None]).into_array();
423        let if_false = buffer![1i32, 2, 3, 4].into_array();
424
425        let result = mask.into_array().zip(if_true.clone(), if_false).unwrap();
426        let expected =
427            PrimitiveArray::from_option_iter([Some(1), Some(2), Some(3), Some(4)]).into_array();
428
429        assert_arrays_eq!(result, expected, &mut ctx);
430        assert_eq!(result.dtype(), if_true.dtype());
431    }
432
433    #[test]
434    fn test_zip_impl_all_true_widens_nullability() -> VortexResult<()> {
435        let mask = Mask::new_true(4);
436        let if_true = buffer![10i32, 20, 30, 40].into_array();
437        let if_false =
438            PrimitiveArray::from_option_iter([Some(1), Some(2), Some(3), None]).into_array();
439
440        let mut ctx = array_session().create_execution_ctx();
441        let result = zip_impl(&if_true, &if_false, &mask, &mut ctx)?;
442        assert_arrays_eq!(
443            result,
444            PrimitiveArray::from_option_iter([Some(10i32), Some(20), Some(30), Some(40)])
445                .into_array(),
446            &mut ctx
447        );
448        assert_eq!(result.dtype(), if_false.dtype());
449        Ok(())
450    }
451
452    #[test]
453    fn test_zip_impl_all_false_widens_nullability() -> VortexResult<()> {
454        let mask = Mask::new_false(4);
455        let if_true =
456            PrimitiveArray::from_option_iter([Some(10), Some(20), Some(30), None]).into_array();
457        let if_false = buffer![1i32, 2, 3, 4].into_array();
458
459        let mut ctx = array_session().create_execution_ctx();
460        let result = zip_impl(&if_true, &if_false, &mask, &mut ctx)?;
461        assert_arrays_eq!(
462            result,
463            PrimitiveArray::from_option_iter([Some(1i32), Some(2), Some(3), Some(4)]).into_array(),
464            &mut ctx
465        );
466        assert_eq!(result.dtype(), if_true.dtype());
467        Ok(())
468    }
469
470    #[test]
471    #[should_panic]
472    fn test_invalid_lengths() {
473        let mask = Mask::new_false(4);
474        let if_true = buffer![10, 20, 30].into_array();
475        let if_false = buffer![1, 2, 3, 4].into_array();
476
477        let _result = mask.into_array().zip(if_true, if_false).unwrap();
478    }
479
480    #[test]
481    fn test_fragmentation() -> VortexResult<()> {
482        let len = 100;
483
484        let const1 = ConstantArray::new(
485            Scalar::utf8("hello_this_is_a_longer_string", Nullability::Nullable),
486            len,
487        )
488        .into_array();
489
490        let const2 = ConstantArray::new(
491            Scalar::utf8("world_this_is_another_string", Nullability::Nullable),
492            len,
493        )
494        .into_array();
495
496        let indices: Vec<usize> = (0..len).step_by(2).collect();
497        let mask = Mask::from_indices(len, indices);
498        let mask_array = mask.into_array();
499
500        let mut ctx = array_session().create_execution_ctx();
501        let result = mask_array
502            .zip(const1.clone(), const2.clone())?
503            .execute::<Columnar>(&mut ctx)?
504            .into_array();
505
506        insta::assert_snapshot!(result.display_tree(), @r"
507        root: vortex.varbinview(utf8?, len=100) nbytes=1.66 kB (100.00%) [all_valid]
508          metadata: 
509          buffer: buffer_0 host 29 B (align=1) (1.75%)
510          buffer: buffer_1 host 28 B (align=1) (1.69%)
511          buffer: views host 1.60 kB (align=16) (96.56%)
512        ");
513
514        let wrapped1 = StructArray::try_from_iter([("nested", const1)])?.into_array();
515        let wrapped2 = StructArray::try_from_iter([("nested", const2)])?.into_array();
516
517        let wrapped_result = mask_array
518            .zip(wrapped1, wrapped2)?
519            .execute::<ArrayRef>(&mut ctx)?;
520        assert!(wrapped_result.is::<Struct>());
521
522        Ok(())
523    }
524
525    #[test]
526    fn test_varbinview_zip() {
527        let if_true = {
528            let mut builder = VarBinViewBuilder::new(
529                DType::Utf8(Nullability::NonNullable),
530                10,
531                Default::default(),
532                BufferGrowthStrategy::fixed(64 * 1024),
533                0.0,
534            );
535            for _ in 0..100 {
536                builder.append_value("Hello");
537                builder.append_value("Hello this is a long string that won't be inlined.");
538            }
539            builder.finish()
540        };
541
542        let if_false = {
543            let mut builder = VarBinViewBuilder::new(
544                DType::Utf8(Nullability::NonNullable),
545                10,
546                Default::default(),
547                BufferGrowthStrategy::fixed(64 * 1024),
548                0.0,
549            );
550            for _ in 0..100 {
551                builder.append_value("Hello2");
552                builder.append_value("Hello2 this is a long string that won't be inlined.");
553            }
554            builder.finish()
555        };
556
557        let mask = Mask::from_indices(200, (0..100).filter(|i| i % 3 != 0));
558        let mask_array = mask.clone().into_array();
559
560        let mut ctx = array_session().create_execution_ctx();
561        let zipped = mask_array
562            .zip(if_true, if_false)
563            .unwrap()
564            .execute::<ArrayRef>(&mut ctx)
565            .unwrap();
566        let zipped = zipped.as_opt::<VarBinView>().unwrap();
567        assert_eq!(zipped.data_buffers().len(), 2);
568
569        let true_value = |i: usize| {
570            if i.is_multiple_of(2) {
571                "Hello"
572            } else {
573                "Hello this is a long string that won't be inlined."
574            }
575        };
576        let false_value = |i: usize| {
577            if i.is_multiple_of(2) {
578                "Hello2"
579            } else {
580                "Hello2 this is a long string that won't be inlined."
581            }
582        };
583        let expected = VarBinViewArray::from_iter_str((0..200).map(|i| {
584            if mask.value(i) {
585                true_value(i)
586            } else {
587                false_value(i)
588            }
589        }));
590        assert_arrays_eq!(zipped.array().clone(), expected, &mut ctx);
591    }
592}