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