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