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