Skip to main content

vortex_array/scalar_fn/fns/binary/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6
7#[expect(deprecated)]
8pub use boolean::and_kleene;
9#[expect(deprecated)]
10pub use boolean::or_kleene;
11use prost::Message;
12use vortex_error::VortexResult;
13use vortex_error::vortex_bail;
14use vortex_session::VortexSession;
15use vortex_session::registry::CachedId;
16
17use crate::ArrayRef;
18use crate::ExecutionCtx;
19use crate::arrays::ScalarFnArray;
20use crate::dtype::DType;
21use crate::dtype::Nullability;
22use crate::expr::BoundExpression;
23use crate::expr::bound;
24use crate::expr::display::ExprDisplay;
25use crate::proto::expr as pb;
26use crate::scalar_fn::Arity;
27use crate::scalar_fn::ChildName;
28use crate::scalar_fn::ExecutionArgs;
29use crate::scalar_fn::ReduceNode;
30use crate::scalar_fn::ScalarFnId;
31use crate::scalar_fn::ScalarFnVTable;
32use crate::scalar_fn::ScalarFnVTableExt;
33use crate::scalar_fn::fns::literal::Literal;
34use crate::scalar_fn::fns::operators::CompareOperator;
35use crate::scalar_fn::fns::operators::Operator;
36
37pub mod boolean;
38pub use boolean::BooleanExecuteAdaptor;
39pub use boolean::BooleanKernel;
40pub(crate) use boolean::execute_boolean;
41pub use boolean::kleene_boolean_buffer_scalar;
42pub use boolean::kleene_boolean_buffers;
43mod compare;
44pub use compare::*;
45mod numeric;
46pub(crate) use numeric::*;
47mod primitive_operand;
48
49use crate::scalar::NumericOperator;
50use crate::scalar::Scalar;
51
52#[derive(Clone)]
53pub struct Binary;
54
55impl Binary {
56    /// Creates a lazy binary operation over `lhs` and `rhs`.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the children have different lengths or incompatible dtypes.
61    pub fn try_new(
62        lhs: ArrayRef,
63        rhs: ArrayRef,
64        operator: Operator,
65    ) -> VortexResult<ScalarFnArray> {
66        ScalarFnArray::try_new(Binary.bind(operator), [lhs, rhs])
67    }
68}
69
70/// Kleene and/or lookup table where both arguments are constant and non-NULL.
71/// is_and, left, right
72const KLEENE_LUT: [[[bool; 2]; 2]; 2] = [
73    [[false, true], [true, true]],   // or
74    [[false, false], [false, true]], // and
75];
76
77/// Kleene and/or reduction where one argument is constant non-NULL and other
78/// is non-constant.
79fn kleene_one_const<T: ReduceNode>(node: T, constant: bool, is_and: bool) -> T {
80    match (is_and, constant) {
81        (true, true) | (false, false) => node,
82        (is_and, constant) => node.new_constant((constant && !is_and).into()),
83    }
84}
85
86impl ScalarFnVTable for Binary {
87    type Options = Operator;
88
89    fn id(&self) -> ScalarFnId {
90        static ID: CachedId = CachedId::new("vortex.binary");
91        *ID
92    }
93
94    fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
95        Ok(Some(
96            pb::BinaryOpts {
97                op: (*instance).into(),
98            }
99            .encode_to_vec(),
100        ))
101    }
102
103    fn deserialize(
104        &self,
105        _metadata: &[u8],
106        _session: &VortexSession,
107    ) -> VortexResult<Self::Options> {
108        let opts = pb::BinaryOpts::decode(_metadata)?;
109        Operator::try_from(opts.op)
110    }
111
112    fn arity(&self, _options: &Self::Options) -> Arity {
113        Arity::Exact(2)
114    }
115
116    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
117        match child_idx {
118            0 => ChildName::from("lhs"),
119            1 => ChildName::from("rhs"),
120            _ => unreachable!("Binary has only two children"),
121        }
122    }
123
124    fn fmt_sql(
125        &self,
126        operator: &Operator,
127        expr: &dyn ExprDisplay,
128        f: &mut Formatter<'_>,
129    ) -> std::fmt::Result {
130        write!(f, "(")?;
131        Display::fmt(expr.display_child(0), f)?;
132        write!(f, " {} ", operator)?;
133        Display::fmt(expr.display_child(1), f)?;
134        write!(f, ")")
135    }
136
137    fn return_dtype(&self, operator: &Operator, arg_dtypes: &[DType]) -> VortexResult<DType> {
138        let lhs = &arg_dtypes[0];
139        let rhs = &arg_dtypes[1];
140
141        if operator.is_arithmetic() {
142            if lhs.is_primitive() && lhs.eq_ignore_nullability(rhs) {
143                return Ok(lhs.with_nullability(lhs.nullability() | rhs.nullability()));
144            }
145
146            if let DType::Decimal(decimal_dtype, _) = lhs
147                && lhs.eq_ignore_nullability(rhs)
148            {
149                let numeric_op = NumericOperator::try_from(*operator)?;
150                return Ok(DType::Decimal(
151                    numeric_op_result_decimal_dtype(*decimal_dtype, numeric_op)?,
152                    lhs.nullability() | rhs.nullability(),
153                ));
154            }
155            vortex_bail!(
156                "incompatible types for arithmetic operation: {} {}",
157                lhs,
158                rhs
159            );
160        }
161
162        if operator.is_comparison()
163            && !lhs.eq_ignore_nullability(rhs)
164            && !lhs.is_extension()
165            && !rhs.is_extension()
166        {
167            vortex_bail!("Cannot compare different DTypes {} and {}", lhs, rhs);
168        }
169
170        if matches!(operator, Operator::And | Operator::Or)
171            && !(lhs.is_boolean() && rhs.is_boolean())
172        {
173            vortex_bail!("'{operator}' requires bool operands, got {lhs} and {rhs}",);
174        }
175
176        Ok(DType::Bool((lhs.is_nullable() || rhs.is_nullable()).into()))
177    }
178
179    fn execute(
180        &self,
181        op: &Operator,
182        args: &dyn ExecutionArgs,
183        ctx: &mut ExecutionCtx,
184    ) -> VortexResult<ArrayRef> {
185        let lhs = args.get(0)?;
186        let rhs = args.get(1)?;
187
188        match op {
189            Operator::Eq => execute_compare(&lhs, &rhs, CompareOperator::Eq, ctx),
190            Operator::NotEq => execute_compare(&lhs, &rhs, CompareOperator::NotEq, ctx),
191            Operator::Lt => execute_compare(&lhs, &rhs, CompareOperator::Lt, ctx),
192            Operator::Lte => execute_compare(&lhs, &rhs, CompareOperator::Lte, ctx),
193            Operator::Gt => execute_compare(&lhs, &rhs, CompareOperator::Gt, ctx),
194            Operator::Gte => execute_compare(&lhs, &rhs, CompareOperator::Gte, ctx),
195            Operator::And => execute_boolean(lhs, rhs, Operator::And, ctx),
196            Operator::Or => execute_boolean(lhs, rhs, Operator::Or, ctx),
197            Operator::Add => execute_numeric(&lhs, &rhs, NumericOperator::Add, ctx),
198            Operator::Sub => execute_numeric(&lhs, &rhs, NumericOperator::Sub, ctx),
199            Operator::Mul => execute_numeric(&lhs, &rhs, NumericOperator::Mul, ctx),
200            Operator::Div => execute_numeric(&lhs, &rhs, NumericOperator::Div, ctx),
201        }
202    }
203
204    fn simplify(
205        &self,
206        operator: &Operator,
207        expr: &BoundExpression,
208    ) -> VortexResult<Option<BoundExpression>> {
209        let lhs = expr.child(0);
210        let rhs = expr.child(1);
211
212        let is_literal_null =
213            |expr: &BoundExpression| expr.as_opt::<Literal>().is_some_and(Scalar::is_null);
214
215        // Binding already rejected ill-typed comparisons such as `int_col = null_utf8`, so a
216        // comparison against a null literal always evaluates to null.
217        if operator.is_comparison() && (is_literal_null(lhs) || is_literal_null(rhs)) {
218            return Ok(Some(bound::lit(Scalar::null(DType::Bool(
219                Nullability::Nullable,
220            )))));
221        }
222
223        let bool_literal = |expr: &BoundExpression| {
224            expr.as_opt::<Literal>()?
225                .as_bool_opt()
226                .map(|value| value.value())
227        };
228
229        // AND/OR use Kleene three-valued logic. `None` below is a boolean null.
230        //
231        // AND:
232        // - false AND x => false
233        // - true  AND x => x
234        // - null  AND null => null
235        //
236        // OR:
237        // - true  OR x => true
238        // - false OR x => x
239        // - null  OR null => null
240        //
241        // Other null cases either fall out of the identity/annihilator rules
242        // above (`null AND true`, `null OR false`) or cannot be simplified under
243        // Kleene semantics (`null AND x`, `null OR x` for non-literal `x`).
244        // Simplification must keep the result nullability binding inferred: the annihilator
245        // literal and the identity child can both be less nullable than `lhs OP rhs`.
246        let result_nullable = lhs.dtype().is_nullable() || rhs.dtype().is_nullable();
247        let nullability = Nullability::from(result_nullable);
248        let bool_lit = |value: bool| bound::lit(Scalar::bool(value, nullability));
249        let widen = |child: &BoundExpression| {
250            if child.dtype().is_nullable() == result_nullable {
251                child.clone()
252            } else {
253                bound::cast(child.clone(), DType::Bool(nullability))
254            }
255        };
256
257        Ok(match operator {
258            Operator::And => match (bool_literal(lhs), bool_literal(rhs)) {
259                (Some(Some(false)), _) | (_, Some(Some(false))) => Some(bool_lit(false)),
260                (Some(Some(true)), _) => Some(widen(rhs)),
261                (_, Some(Some(true))) => Some(widen(lhs)),
262                (Some(None), Some(None)) => Some(widen(lhs)),
263                _ => None,
264            },
265            Operator::Or => match (bool_literal(lhs), bool_literal(rhs)) {
266                (Some(Some(true)), _) | (_, Some(Some(true))) => Some(bool_lit(true)),
267                (Some(Some(false)), _) => Some(widen(rhs)),
268                (_, Some(Some(false))) => Some(widen(lhs)),
269                (Some(None), Some(None)) => Some(widen(lhs)),
270                _ => None,
271            },
272            _ => None,
273        })
274    }
275
276    fn reduce<T: ReduceNode>(&self, operator: &Operator, node: &T) -> VortexResult<Option<T>> {
277        if !matches!(operator, Operator::And | Operator::Or) {
278            return Ok(None);
279        }
280        let left = node.child(0);
281        let right = node.child(1);
282
283        let left_const = left.as_constant();
284        let right_const = right.as_constant();
285
286        // We don't handle Kleene NULL reduction here (.value() returns None
287        // for NULL). This will be reduced in the boolean kernel during
288        // execution. Not handling the case keeps the code much simpler.
289        let left_const = left_const
290            .and_then(|s| s.value().cloned())
291            .map(|v| v.as_bool());
292        let right_const = right_const
293            .and_then(|s| s.value().cloned())
294            .map(|v| v.as_bool());
295        let is_and = *operator == Operator::And;
296
297        Ok(Some(match (left_const, right_const) {
298            (None, None) => return Ok(None),
299            (Some(left_const), Some(right_const)) => left.new_constant(
300                KLEENE_LUT[is_and as usize][left_const as usize][right_const as usize].into(),
301            ),
302            (Some(constant), None) => kleene_one_const(right, constant, is_and),
303            (None, Some(constant)) => kleene_one_const(left, constant, is_and),
304        }))
305    }
306
307    fn is_strict(&self, operator: &Operator) -> bool {
308        !matches!(operator, Operator::And | Operator::Or)
309    }
310
311    fn is_infallible(&self, operator: &Operator) -> bool {
312        // Arithmetic operations could be better modelled here.
313        matches!(
314            operator,
315            Operator::Eq
316                | Operator::NotEq
317                | Operator::Gt
318                | Operator::Gte
319                | Operator::Lt
320                | Operator::Lte
321                | Operator::And
322                | Operator::Or
323        )
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use vortex_error::VortexExpect;
330    use vortex_error::VortexResult;
331
332    use super::*;
333    use crate::IntoArray;
334    use crate::VortexSessionExecute;
335    use crate::array_session;
336    use crate::arrays::Bool;
337    use crate::arrays::BoolArray;
338    use crate::arrays::ConstantArray;
339    use crate::arrays::PrimitiveArray;
340    use crate::arrays::ScalarFn;
341    use crate::arrays::scalar_fn::ExactScalarFn;
342    use crate::arrays::scalar_fn::ScalarFnArrayExt;
343    use crate::assert_arrays_eq;
344    use crate::builtins::ArrayBuiltins;
345    use crate::dtype::DType;
346    use crate::dtype::Nullability;
347    use crate::dtype::PType;
348    use crate::expr::Expression;
349    use crate::expr::and;
350    use crate::expr::and_collect;
351    use crate::expr::col;
352    use crate::expr::eq;
353    use crate::expr::gt;
354    use crate::expr::gt_eq;
355    use crate::expr::lit;
356    use crate::expr::lt;
357    use crate::expr::lt_eq;
358    use crate::expr::not_eq;
359    use crate::expr::or;
360    use crate::expr::or_collect;
361    use crate::expr::test_harness;
362    use crate::optimizer::ArrayOptimizer;
363    use crate::scalar::Scalar;
364
365    #[test]
366    fn and_collect_balanced() {
367        let values = vec![lit(1), lit(2), lit(3), lit(4), lit(5)];
368
369        insta::assert_snapshot!(and_collect(values.into_iter()).unwrap().display_tree(), @r"
370        vortex.binary(and)
371        ├── lhs: vortex.binary(and)
372        │   ├── lhs: vortex.literal(1i32)
373        │   └── rhs: vortex.literal(2i32)
374        └── rhs: vortex.binary(and)
375            ├── lhs: vortex.binary(and)
376            │   ├── lhs: vortex.literal(3i32)
377            │   └── rhs: vortex.literal(4i32)
378            └── rhs: vortex.literal(5i32)
379        ");
380
381        // 4 elements: and(and(1, 2), and(3, 4)) - perfectly balanced
382        let values = vec![lit(1), lit(2), lit(3), lit(4)];
383        insta::assert_snapshot!(and_collect(values.into_iter()).unwrap().display_tree(), @r"
384        vortex.binary(and)
385        ├── lhs: vortex.binary(and)
386        │   ├── lhs: vortex.literal(1i32)
387        │   └── rhs: vortex.literal(2i32)
388        └── rhs: vortex.binary(and)
389            ├── lhs: vortex.literal(3i32)
390            └── rhs: vortex.literal(4i32)
391        ");
392
393        // 1 element: just the element
394        let values = vec![lit(1)];
395        insta::assert_snapshot!(and_collect(values.into_iter()).unwrap().display_tree(), @"vortex.literal(1i32)");
396
397        // 0 elements: None
398        let values: Vec<Expression> = vec![];
399        assert!(and_collect(values.into_iter()).is_none());
400    }
401
402    #[test]
403    fn or_collect_balanced() {
404        // 4 elements: or(or(1, 2), or(3, 4)) - perfectly balanced
405        let values = vec![lit(1), lit(2), lit(3), lit(4)];
406        insta::assert_snapshot!(or_collect(values.into_iter()).unwrap().display_tree(), @r"
407        vortex.binary(or)
408        ├── lhs: vortex.binary(or)
409        │   ├── lhs: vortex.literal(1i32)
410        │   └── rhs: vortex.literal(2i32)
411        └── rhs: vortex.binary(or)
412            ├── lhs: vortex.literal(3i32)
413            └── rhs: vortex.literal(4i32)
414        ");
415    }
416
417    #[test]
418    fn dtype() {
419        let dtype = test_harness::struct_dtype();
420        let bool1: Expression = col("bool1");
421        let bool2: Expression = col("bool2");
422        assert_eq!(
423            and(bool1.clone(), bool2.clone())
424                .return_dtype(&dtype)
425                .unwrap(),
426            DType::Bool(Nullability::NonNullable)
427        );
428        assert_eq!(
429            or(bool1, bool2).return_dtype(&dtype).unwrap(),
430            DType::Bool(Nullability::NonNullable)
431        );
432
433        let col1: Expression = col("col1");
434        let col2: Expression = col("col2");
435
436        assert_eq!(
437            eq(col1.clone(), col2.clone()).return_dtype(&dtype).unwrap(),
438            DType::Bool(Nullability::Nullable)
439        );
440        assert_eq!(
441            not_eq(col1.clone(), col2.clone())
442                .return_dtype(&dtype)
443                .unwrap(),
444            DType::Bool(Nullability::Nullable)
445        );
446        assert_eq!(
447            gt(col1.clone(), col2.clone()).return_dtype(&dtype).unwrap(),
448            DType::Bool(Nullability::Nullable)
449        );
450        assert_eq!(
451            gt_eq(col1.clone(), col2.clone())
452                .return_dtype(&dtype)
453                .unwrap(),
454            DType::Bool(Nullability::Nullable)
455        );
456        assert_eq!(
457            lt(col1.clone(), col2.clone()).return_dtype(&dtype).unwrap(),
458            DType::Bool(Nullability::Nullable)
459        );
460        assert_eq!(
461            lt_eq(col1.clone(), col2.clone())
462                .return_dtype(&dtype)
463                .unwrap(),
464            DType::Bool(Nullability::Nullable)
465        );
466
467        assert_eq!(
468            or(lt(col1.clone(), col2.clone()), not_eq(col1, col2))
469                .return_dtype(&dtype)
470                .unwrap(),
471            DType::Bool(Nullability::Nullable)
472        );
473    }
474
475    #[test]
476    fn comparison_with_typed_null_simplifies_after_type_check() -> VortexResult<()> {
477        let dtype = test_harness::struct_dtype();
478
479        let expr = eq(
480            col("col1"),
481            lit(Scalar::null(DType::Primitive(
482                PType::U16,
483                Nullability::Nullable,
484            ))),
485        );
486
487        assert_eq!(
488            expr.bind(&dtype)?.optimize_recursive()?,
489            lit(Scalar::null(DType::Bool(Nullability::Nullable))).bind(&dtype)?
490        );
491        Ok(())
492    }
493
494    #[test]
495    fn comparison_with_incompatible_null_still_type_checks() {
496        let dtype = test_harness::struct_dtype();
497        let expr = eq(
498            col("col1"),
499            lit(Scalar::null(DType::Utf8(Nullability::Nullable))),
500        );
501
502        assert!(expr.bind(&dtype).is_err());
503    }
504
505    #[test]
506    fn test_display_print() {
507        let expr = gt(lit(1), lit(2));
508        assert_eq!(format!("{expr}"), "(1i32 > 2i32)");
509    }
510
511    /// Regression test for GitHub issue #5947: struct comparison in filter expressions should work
512    /// using `make_comparator` instead of Arrow's `cmp` functions which don't support nested types.
513    #[test]
514    fn test_struct_comparison() {
515        use crate::IntoArray;
516        use crate::arrays::StructArray;
517
518        // Create a struct array with one element for testing.
519        let lhs_struct = StructArray::from_fields(&[
520            ("a", PrimitiveArray::from_iter([1i32]).into_array()),
521            ("b", PrimitiveArray::from_iter([3i32]).into_array()),
522        ])
523        .unwrap()
524        .into_array();
525
526        let rhs_struct_equal = StructArray::from_fields(&[
527            ("a", PrimitiveArray::from_iter([1i32]).into_array()),
528            ("b", PrimitiveArray::from_iter([3i32]).into_array()),
529        ])
530        .unwrap()
531        .into_array();
532
533        let rhs_struct_different = StructArray::from_fields(&[
534            ("a", PrimitiveArray::from_iter([1i32]).into_array()),
535            ("b", PrimitiveArray::from_iter([4i32]).into_array()),
536        ])
537        .unwrap()
538        .into_array();
539
540        // Test using binary method directly
541        let result_equal = lhs_struct.binary(rhs_struct_equal, Operator::Eq).unwrap();
542        assert_eq!(
543            result_equal
544                .execute_scalar(0, &mut array_session().create_execution_ctx())
545                .vortex_expect("value"),
546            Scalar::bool(true, Nullability::NonNullable),
547            "Equal structs should be equal"
548        );
549
550        let result_different = lhs_struct
551            .binary(rhs_struct_different, Operator::Eq)
552            .unwrap();
553        assert_eq!(
554            result_different
555                .execute_scalar(0, &mut array_session().create_execution_ctx())
556                .vortex_expect("value"),
557            Scalar::bool(false, Nullability::NonNullable),
558            "Different structs should not be equal"
559        );
560    }
561
562    #[test]
563    fn test_or_kleene_validity() {
564        let mut ctx = array_session().create_execution_ctx();
565        use crate::arrays::StructArray;
566        use crate::expr::col;
567
568        let struct_arr = StructArray::from_fields(&[
569            ("a", BoolArray::from_iter([Some(true)]).into_array()),
570            (
571                "b",
572                BoolArray::from_iter([Option::<bool>::None]).into_array(),
573            ),
574        ])
575        .unwrap()
576        .into_array();
577
578        let expr = or(col("a"), col("b"));
579        let result = struct_arr.apply(&expr).unwrap();
580
581        assert_arrays_eq!(
582            result,
583            BoolArray::from_iter([Some(true)]).into_array(),
584            &mut ctx
585        )
586    }
587
588    #[test]
589    fn test_and_reduce() -> VortexResult<()> {
590        let mut ctx = array_session().create_execution_ctx();
591        let left = BoolArray::from_iter([true, true, false]).into_array();
592        let right = ConstantArray::new(true, 3).into_array();
593
594        let array = Binary::try_new(left.clone(), right.clone(), Operator::And)?
595            .into_array()
596            .optimize()?; // calls reduce()
597        assert!(array.is::<Bool>()); // and(left, const) -> left
598        assert_arrays_eq!(array, left, &mut ctx);
599
600        let array = Binary::try_new(right.clone(), left.clone(), Operator::And)?
601            .into_array()
602            .optimize()?;
603        assert!(array.is::<Bool>()); // and(const, left) -> left
604        assert_arrays_eq!(array, left, &mut ctx);
605
606        let array = Binary::try_new(left.clone(), right.clone(), Operator::Or)?
607            .into_array()
608            .optimize()?;
609        assert_eq!(array.as_constant(), Some(true.into())); // or(left, const) -> true
610
611        let array = Binary::try_new(right.clone(), left, Operator::Or)?
612            .into_array()
613            .optimize()?;
614        assert_eq!(array.as_constant(), Some(true.into())); // or(const, left) -> true
615
616        let array = Binary::try_new(right.clone(), right.clone(), Operator::Or)?
617            .into_array()
618            .optimize()?;
619        assert_eq!(array.as_constant(), Some(true.into())); // or(const, const) -> const
620
621        let left_false = ConstantArray::new(false, 3).into_array();
622
623        let array = Binary::try_new(right.clone(), left_false.clone(), Operator::Or)?
624            .into_array()
625            .optimize()?;
626        assert_eq!(array.as_constant(), Some(true.into()));
627        let array = Binary::try_new(left_false, right, Operator::And)?
628            .into_array()
629            .optimize()?;
630        assert_eq!(array.as_constant(), Some(false.into()));
631
632        Ok(())
633    }
634
635    #[test]
636    fn test_and_reduce_nullable() -> VortexResult<()> {
637        let mut ctx = array_session().create_execution_ctx();
638        let nullable_true =
639            ConstantArray::new(Scalar::bool(true, Nullability::Nullable), 3).into_array();
640        let right = BoolArray::from_iter([true, false, true]).into_array();
641
642        let array = Binary::try_new(nullable_true, right.clone(), Operator::And)?
643            .into_array()
644            .optimize()?;
645        assert_arrays_eq!(array, right, &mut ctx);
646        Ok(())
647    }
648
649    #[test]
650    fn test_and_reject_non_bool() {
651        let lhs = ConstantArray::new(7i32, 3).into_array();
652        let rhs = PrimitiveArray::from_iter([1i32, 2, 3]).into_array();
653        assert!(Binary::try_new(lhs, rhs, Operator::And).is_err());
654    }
655
656    #[test]
657    fn test_isnull_and_reduce() -> VortexResult<()> {
658        let mut ctx = array_session().create_execution_ctx();
659        let left = BoolArray::from_iter([Some(true), Some(true), None]).into_array();
660        let right = BoolArray::from_iter([true, false, true]).into_array();
661
662        // IsNull(and(x, y)) -> and(IsNull(x), y) -> and(not(x.validity), y)
663        //            ^ nullable
664        for (lhs, rhs) in [(left.clone(), right.clone()), (right.clone(), left)] {
665            let array = lhs
666                .binary(rhs.clone(), Operator::And)?
667                .is_null()?
668                .optimize()?;
669            assert_eq!(*array.as_::<ExactScalarFn<Binary>>().options, Operator::And);
670            assert_arrays_eq!(
671                array.as_::<ScalarFn>().get_child(0), // not(left.validity())
672                BoolArray::from_iter([false, false, true]),
673                &mut ctx
674            );
675            assert_arrays_eq!(array.as_::<ScalarFn>().get_child(1), right, &mut ctx);
676            assert_arrays_eq!(array, BoolArray::from_iter([false, false, true]), &mut ctx);
677        }
678        Ok(())
679    }
680
681    #[test]
682    fn test_isnotnull_or_reduce() -> VortexResult<()> {
683        let mut ctx = array_session().create_execution_ctx();
684        let left = BoolArray::from_iter([Some(true), Some(true), None]).into_array();
685        let right = BoolArray::from_iter([true, false, true]).into_array();
686
687        // IsNotNull(or(x, y)) -> or(IsNotNull(x), y) -> or(x.validity, y)
688        //              ^ nullable
689        for (lhs, rhs) in [(left.clone(), right.clone()), (right.clone(), left)] {
690            let array = lhs
691                .binary(rhs.clone(), Operator::Or)?
692                .is_not_null()?
693                .optimize()?;
694            assert_eq!(*array.as_::<ExactScalarFn<Binary>>().options, Operator::Or);
695            assert_arrays_eq!(
696                array.as_::<ScalarFn>().get_child(0), // left.validity()
697                BoolArray::from_iter([true, true, false]),
698                &mut ctx
699            );
700            assert_arrays_eq!(array.as_::<ScalarFn>().get_child(1), right, &mut ctx);
701            assert_arrays_eq!(array, BoolArray::from_iter([true, true, true]), &mut ctx);
702        }
703        Ok(())
704    }
705}