Skip to main content

vortex_array/
mask.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexResult;
5use vortex_error::vortex_bail;
6use vortex_mask::Mask;
7
8use crate::ArrayRef;
9use crate::Executable;
10use crate::ExecutionCtx;
11use crate::IntoArray;
12use crate::arrays::BoolArray;
13use crate::columnar::Columnar;
14use crate::dtype::DType;
15use crate::dtype::Nullability;
16use crate::validity::Validity;
17
18impl Executable for Mask {
19    /// Executes a boolean array into a [`Mask`].
20    ///
21    /// The array must have a non-nullable boolean dtype. To execute a nullable boolean array,
22    /// coercing null elements to `false`, first call
23    /// [`ArrayRef::fill_null(false)`](crate::builtins::ArrayBuiltins::fill_null).
24    fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
25        if !matches!(array.dtype(), DType::Bool(Nullability::NonNullable)) {
26            vortex_bail!(
27                "Mask array must have boolean(NonNullable) dtype, not {}",
28                array.dtype()
29            );
30        }
31
32        let array_len = array.len();
33        Ok(match array.execute(ctx)? {
34            Columnar::Constant(s) => {
35                Mask::new(array_len, s.scalar().as_bool().value().unwrap_or(false))
36            }
37            Columnar::Canonical(a) => {
38                let bool = a.into_array().execute::<BoolArray>(ctx)?;
39                Mask::from(bool.into_bit_buffer())
40            }
41        })
42    }
43}
44
45/// An adapter that coerces null elements of a boolean array to `false` before executing it into a
46/// [`Mask`]. Created by [`ArrayRef::null_as_false`].
47///
48/// Use for filter and pruning predicates over nullable data, where SQL semantics treat `NULL` as
49/// not matching.
50pub struct NullAsFalse(ArrayRef);
51
52impl ArrayRef {
53    /// Returns an adapter that treats null elements of this boolean array as `false` when executed
54    /// into a [`Mask`]. See [`NullAsFalse`].
55    pub fn null_as_false(self) -> NullAsFalse {
56        NullAsFalse(self)
57    }
58}
59
60impl NullAsFalse {
61    /// Executes the boolean array into a [`Mask`], coercing null elements to `false`.
62    ///
63    /// Canonicalizes the (possibly lazy) array exactly once and folds validity into the value bits
64    /// with a single `AND` that reuses the value buffer when it is uniquely owned.
65    pub fn execute(self, ctx: &mut ExecutionCtx) -> VortexResult<Mask> {
66        let array = self.0;
67        if !matches!(array.dtype(), DType::Bool(_)) {
68            vortex_bail!("Mask array must have boolean dtype, not {}", array.dtype());
69        }
70        // Non-nullable input needs no coercion; defer to the strict `Mask` execution.
71        if !array.dtype().is_nullable() {
72            return array.execute::<Mask>(ctx);
73        }
74
75        let len = array.len();
76        Ok(match array.execute::<Columnar>(ctx)? {
77            Columnar::Constant(c) => Mask::new(len, c.scalar().as_bool().value().unwrap_or(false)),
78            Columnar::Canonical(c) => {
79                let bool = c.into_array().execute::<BoolArray>(ctx)?;
80                match bool.as_ref().validity()? {
81                    Validity::NonNullable | Validity::AllValid => {
82                        Mask::from_buffer(bool.into_bit_buffer())
83                    }
84                    Validity::AllInvalid => Mask::new_false(len),
85                    Validity::Array(v) => {
86                        let validity_bits = v.execute::<BoolArray>(ctx)?.into_bit_buffer();
87                        Mask::from_buffer(bool.into_bit_buffer() & &validity_bits)
88                    }
89                }
90            }
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use vortex_error::VortexResult;
98    use vortex_mask::Mask;
99
100    use crate::ExecutionCtx;
101    use crate::IntoArray;
102    use crate::VortexSessionExecute;
103    use crate::array_session;
104    use crate::arrays::BoolArray;
105    use crate::arrays::ConstantArray;
106    use crate::builtins::ArrayBuiltins;
107    use crate::dtype::DType;
108    use crate::dtype::Nullability;
109    use crate::scalar::Scalar;
110
111    fn ctx() -> ExecutionCtx {
112        array_session().create_execution_ctx()
113    }
114
115    #[test]
116    fn mask_non_nullable() -> VortexResult<()> {
117        let array = BoolArray::from_iter([true, false, true]).into_array();
118        let mask = array.execute::<Mask>(&mut ctx())?;
119        assert_eq!(mask, Mask::from_iter([true, false, true]));
120        Ok(())
121    }
122
123    #[test]
124    fn mask_rejects_nullable() {
125        let array = BoolArray::from_iter([Some(true), None]).into_array();
126        assert!(array.execute::<Mask>(&mut ctx()).is_err());
127    }
128
129    #[test]
130    fn fill_null_then_mask_coerces_nulls() -> VortexResult<()> {
131        let array = BoolArray::from_iter([Some(true), None, Some(false), None]).into_array();
132        let mask = array.fill_null(false)?.execute::<Mask>(&mut ctx())?;
133        assert_eq!(mask, Mask::from_iter([true, false, false, false]));
134        Ok(())
135    }
136
137    #[test]
138    fn fill_null_then_mask_null_constant() -> VortexResult<()> {
139        let array =
140            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 4).into_array();
141        let mask = array.fill_null(false)?.execute::<Mask>(&mut ctx())?;
142        assert_eq!(mask, Mask::new_false(4));
143        Ok(())
144    }
145
146    #[test]
147    fn null_as_false_non_nullable() -> VortexResult<()> {
148        let array = BoolArray::from_iter([true, false, true]).into_array();
149        let mask = array.null_as_false().execute(&mut ctx())?;
150        assert_eq!(mask, Mask::from_iter([true, false, true]));
151        Ok(())
152    }
153
154    #[test]
155    fn null_as_false_treats_null_as_false() -> VortexResult<()> {
156        let array = BoolArray::from_iter([Some(true), None, Some(false), None]).into_array();
157        let mask = array.null_as_false().execute(&mut ctx())?;
158        assert_eq!(mask, Mask::from_iter([true, false, false, false]));
159        Ok(())
160    }
161
162    #[test]
163    fn null_as_false_null_constant() -> VortexResult<()> {
164        let array =
165            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), 4).into_array();
166        let mask = array.null_as_false().execute(&mut ctx())?;
167        assert_eq!(mask, Mask::new_false(4));
168        Ok(())
169    }
170
171    #[test]
172    fn null_as_false_matches_fill_null_then_mask() -> VortexResult<()> {
173        let array =
174            BoolArray::from_iter([Some(true), None, Some(false), Some(true), None]).into_array();
175        let via_fill_null = array.fill_null(false)?.execute::<Mask>(&mut ctx())?;
176        let via_coerce = array.null_as_false().execute(&mut ctx())?;
177        assert_eq!(via_coerce, via_fill_null);
178        Ok(())
179    }
180}