vortex_array/compute/
boolean.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::any::Any;
5use std::sync::LazyLock;
6
7use arcref::ArcRef;
8use arrow_array::cast::AsArray;
9use arrow_schema::DataType;
10use vortex_dtype::DType;
11use vortex_error::{VortexError, VortexExpect, VortexResult, vortex_bail, vortex_err};
12
13use crate::arrow::{FromArrowArray, IntoArrowArray};
14use crate::compute::{ComputeFn, ComputeFnVTable, InvocationArgs, Kernel, Options, Output};
15use crate::vtable::VTable;
16use crate::{Array, ArrayRef};
17
18static BOOLEAN_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
19    let compute = ComputeFn::new("boolean".into(), ArcRef::new_ref(&Boolean));
20    for kernel in inventory::iter::<BooleanKernelRef> {
21        compute.register_kernel(kernel.0.clone());
22    }
23    compute
24});
25
26pub(crate) fn warm_up_vtable() -> usize {
27    BOOLEAN_FN.kernels().len()
28}
29
30/// Point-wise logical _and_ between two Boolean arrays.
31///
32/// This method uses Arrow-style null propagation rather than the Kleene logic semantics. This
33/// semantics is also known as "Bochvar logic" and "weak Kleene logic".
34///
35/// See also [BooleanOperator::And]
36pub fn and(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
37    boolean(lhs, rhs, BooleanOperator::And)
38}
39
40/// Point-wise Kleene logical _and_ between two Boolean arrays.
41///
42/// See also [BooleanOperator::AndKleene]
43pub fn and_kleene(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
44    boolean(lhs, rhs, BooleanOperator::AndKleene)
45}
46
47/// Point-wise logical _or_ between two Boolean arrays.
48///
49/// This method uses Arrow-style null propagation rather than the Kleene logic semantics. This
50/// semantics is also known as "Bochvar logic" and "weak Kleene logic".
51///
52/// See also [BooleanOperator::Or]
53pub fn or(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
54    boolean(lhs, rhs, BooleanOperator::Or)
55}
56
57/// Point-wise Kleene logical _or_ between two Boolean arrays.
58///
59/// See also [BooleanOperator::OrKleene]
60pub fn or_kleene(lhs: &dyn Array, rhs: &dyn Array) -> VortexResult<ArrayRef> {
61    boolean(lhs, rhs, BooleanOperator::OrKleene)
62}
63
64/// Point-wise logical operator between two Boolean arrays.
65///
66/// This method uses Arrow-style null propagation rather than the Kleene logic semantics. This
67/// semantics is also known as "Bochvar logic" and "weak Kleene logic".
68pub fn boolean(lhs: &dyn Array, rhs: &dyn Array, op: BooleanOperator) -> VortexResult<ArrayRef> {
69    BOOLEAN_FN
70        .invoke(&InvocationArgs {
71            inputs: &[lhs.into(), rhs.into()],
72            options: &op,
73        })?
74        .unwrap_array()
75}
76
77pub struct BooleanKernelRef(ArcRef<dyn Kernel>);
78inventory::collect!(BooleanKernelRef);
79
80pub trait BooleanKernel: VTable {
81    fn boolean(
82        &self,
83        array: &Self::Array,
84        other: &dyn Array,
85        op: BooleanOperator,
86    ) -> VortexResult<Option<ArrayRef>>;
87}
88
89#[derive(Debug)]
90pub struct BooleanKernelAdapter<V: VTable>(pub V);
91
92impl<V: VTable + BooleanKernel> BooleanKernelAdapter<V> {
93    pub const fn lift(&'static self) -> BooleanKernelRef {
94        BooleanKernelRef(ArcRef::new_ref(self))
95    }
96}
97
98impl<V: VTable + BooleanKernel> Kernel for BooleanKernelAdapter<V> {
99    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
100        let inputs = BooleanArgs::try_from(args)?;
101        let Some(array) = inputs.lhs.as_opt::<V>() else {
102            return Ok(None);
103        };
104        Ok(V::boolean(&self.0, array, inputs.rhs, inputs.operator)?.map(|array| array.into()))
105    }
106}
107
108struct Boolean;
109
110impl ComputeFnVTable for Boolean {
111    fn invoke(
112        &self,
113        args: &InvocationArgs,
114        kernels: &[ArcRef<dyn Kernel>],
115    ) -> VortexResult<Output> {
116        let BooleanArgs { lhs, rhs, operator } = BooleanArgs::try_from(args)?;
117
118        let rhs_is_constant = rhs.is_constant();
119
120        // If LHS is constant, then we make sure it's on the RHS.
121        if lhs.is_constant() && !rhs_is_constant {
122            return Ok(boolean(rhs, lhs, operator)?.into());
123        }
124
125        // If the RHS is constant and the LHS is Arrow, we can't do any better than arrow_compare.
126        if lhs.is_arrow() && (rhs.is_arrow() || rhs_is_constant) {
127            return Ok(arrow_boolean(lhs.to_array(), rhs.to_array(), operator)?.into());
128        }
129
130        // Check if either LHS or RHS supports the operation directly.
131        for kernel in kernels {
132            if let Some(output) = kernel.invoke(args)? {
133                return Ok(output);
134            }
135        }
136        if let Some(output) = lhs.invoke(&BOOLEAN_FN, args)? {
137            return Ok(output);
138        }
139
140        let inverse_args = InvocationArgs {
141            inputs: &[rhs.into(), lhs.into()],
142            options: &operator,
143        };
144        for kernel in kernels {
145            if let Some(output) = kernel.invoke(&inverse_args)? {
146                return Ok(output);
147            }
148        }
149        if let Some(output) = rhs.invoke(&BOOLEAN_FN, &inverse_args)? {
150            return Ok(output);
151        }
152
153        log::debug!(
154            "No boolean implementation found for LHS {}, RHS {}, and operator {:?} (or inverse)",
155            rhs.encoding_id(),
156            lhs.encoding_id(),
157            operator,
158        );
159
160        // If neither side implements the trait, then we delegate to Arrow compute.
161        Ok(arrow_boolean(lhs.to_array(), rhs.to_array(), operator)?.into())
162    }
163
164    fn return_dtype(&self, args: &InvocationArgs) -> VortexResult<DType> {
165        let BooleanArgs { lhs, rhs, .. } = BooleanArgs::try_from(args)?;
166
167        if !lhs.dtype().is_boolean()
168            || !rhs.dtype().is_boolean()
169            || !lhs.dtype().eq_ignore_nullability(rhs.dtype())
170        {
171            vortex_bail!(
172                "Boolean operations are only supported on boolean arrays: {} and {}",
173                lhs.dtype(),
174                rhs.dtype()
175            )
176        }
177
178        Ok(DType::Bool(
179            (lhs.dtype().is_nullable() || rhs.dtype().is_nullable()).into(),
180        ))
181    }
182
183    fn return_len(&self, args: &InvocationArgs) -> VortexResult<usize> {
184        let BooleanArgs { lhs, rhs, .. } = BooleanArgs::try_from(args)?;
185
186        if lhs.len() != rhs.len() {
187            vortex_bail!(
188                "Boolean operations aren't supported on arrays of different lengths: {} and {}",
189                lhs.len(),
190                rhs.len()
191            )
192        }
193
194        Ok(lhs.len())
195    }
196
197    fn is_elementwise(&self) -> bool {
198        true
199    }
200}
201
202/// Operations over the nullable Boolean values.
203///
204/// All three operators accept and produce values from the set {true, false, and null}.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum BooleanOperator {
207    /// Logical and, unless either value is null, in which case the result is null.
208    ///
209    /// | A ∧ B |       | **B** |       |       |
210    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
211    /// |       |       | **F** | **U** | **T** |
212    /// | **A** | **F** | F     | U     | F     |
213    /// |       | **U** | U     | U     | U     |
214    /// |       | **T** | F     | U     | T     |
215    And,
216    /// [Kleene (three-valued) logical and](https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics).
217    ///
218    /// | A ∧ B |       | **B** |       |       |
219    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
220    /// |       |       | **F** | **U** | **T** |
221    /// | **A** | **F** | F     | F     | F     |
222    /// |       | **U** | F     | U     | U     |
223    /// |       | **T** | F     | U     | T     |
224    AndKleene,
225    /// Logical or, unless either value is null, in which case the result is null.
226    ///
227    /// | A ∨ B |       | **B** |       |       |
228    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
229    /// |       |       | **F** | **U** | **T** |
230    /// | **A** | **F** | F     | U     | T     |
231    /// |       | **U** | U     | U     | U     |
232    /// |       | **T** | T     | U     | T     |
233    Or,
234    /// [Kleene (three-valued) logical or](https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics).
235    ///
236    /// | A ∨ B |       | **B** |       |       |
237    /// |:-----:|:-----:|:-----:|:-----:|:-----:|
238    /// |       |       | **F** | **U** | **T** |
239    /// | **A** | **F** | F     | U     | T     |
240    /// |       | **U** | U     | U     | T     |
241    /// |       | **T** | T     | T     | T     |
242    OrKleene,
243    // AndNot,
244    // AndNotKleene,
245    // Xor,
246}
247
248impl Options for BooleanOperator {
249    fn as_any(&self) -> &dyn Any {
250        self
251    }
252}
253
254struct BooleanArgs<'a> {
255    lhs: &'a dyn Array,
256    rhs: &'a dyn Array,
257    operator: BooleanOperator,
258}
259
260impl<'a> TryFrom<&InvocationArgs<'a>> for BooleanArgs<'a> {
261    type Error = VortexError;
262
263    fn try_from(value: &InvocationArgs<'a>) -> VortexResult<Self> {
264        if value.inputs.len() != 2 {
265            vortex_bail!("Expected 2 inputs, found {}", value.inputs.len());
266        }
267        let lhs = value.inputs[0]
268            .array()
269            .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
270        let rhs = value.inputs[1]
271            .array()
272            .ok_or_else(|| vortex_err!("Expected input 1 to be an array"))?;
273        let operator = value
274            .options
275            .as_any()
276            .downcast_ref::<BooleanOperator>()
277            .vortex_expect("Expected options to be an operator");
278
279        Ok(BooleanArgs {
280            lhs,
281            rhs,
282            operator: *operator,
283        })
284    }
285}
286
287/// Implementation of `BinaryBooleanFn` using the Arrow crate.
288///
289/// Note that other encodings should handle a constant RHS value, so we can assume here that
290/// the RHS is not constant and expand to a full array.
291pub(crate) fn arrow_boolean(
292    lhs: ArrayRef,
293    rhs: ArrayRef,
294    operator: BooleanOperator,
295) -> VortexResult<ArrayRef> {
296    let nullable = lhs.dtype().is_nullable() || rhs.dtype().is_nullable();
297
298    let lhs = lhs.into_arrow(&DataType::Boolean)?.as_boolean().clone();
299    let rhs = rhs.into_arrow(&DataType::Boolean)?.as_boolean().clone();
300
301    let array = match operator {
302        BooleanOperator::And => arrow_arith::boolean::and(&lhs, &rhs)?,
303        BooleanOperator::AndKleene => arrow_arith::boolean::and_kleene(&lhs, &rhs)?,
304        BooleanOperator::Or => arrow_arith::boolean::or(&lhs, &rhs)?,
305        BooleanOperator::OrKleene => arrow_arith::boolean::or_kleene(&lhs, &rhs)?,
306    };
307
308    Ok(ArrayRef::from_arrow(&array, nullable))
309}
310
311#[cfg(test)]
312mod tests {
313    use rstest::rstest;
314
315    use super::*;
316    use crate::IntoArray;
317    use crate::arrays::BoolArray;
318    use crate::canonical::ToCanonical;
319    #[rstest]
320    #[case(BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter())
321    .into_array(), BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter())
322    .into_array())]
323    #[case(BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter()).into_array(),
324        BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter()).into_array())]
325    fn test_or(#[case] lhs: ArrayRef, #[case] rhs: ArrayRef) {
326        let r = or(&lhs, &rhs).unwrap();
327
328        let r = r.to_bool().into_array();
329
330        let v0 = r.scalar_at(0).as_bool().value();
331        let v1 = r.scalar_at(1).as_bool().value();
332        let v2 = r.scalar_at(2).as_bool().value();
333        let v3 = r.scalar_at(3).as_bool().value();
334
335        assert!(v0.unwrap());
336        assert!(v1.unwrap());
337        assert!(v2.unwrap());
338        assert!(!v3.unwrap());
339    }
340
341    #[rstest]
342    #[case(BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter())
343    .into_array(), BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter())
344    .into_array())]
345    #[case(BoolArray::from_iter([Some(true), Some(false), Some(true), Some(false)].into_iter()).into_array(),
346        BoolArray::from_iter([Some(true), Some(true), Some(false), Some(false)].into_iter()).into_array())]
347    fn test_and(#[case] lhs: ArrayRef, #[case] rhs: ArrayRef) {
348        let r = and(&lhs, &rhs).unwrap().to_bool().into_array();
349
350        let v0 = r.scalar_at(0).as_bool().value();
351        let v1 = r.scalar_at(1).as_bool().value();
352        let v2 = r.scalar_at(2).as_bool().value();
353        let v3 = r.scalar_at(3).as_bool().value();
354
355        assert!(v0.unwrap());
356        assert!(!v1.unwrap());
357        assert!(!v2.unwrap());
358        assert!(!v3.unwrap());
359    }
360}