vortex_array/expr/exprs/
binary.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Formatter;
5
6use arrow_ord::cmp;
7use prost::Message;
8use vortex_compute::arrow::IntoArrow;
9use vortex_compute::arrow::IntoVector;
10use vortex_compute::logical::LogicalAndKleene;
11use vortex_compute::logical::LogicalOrKleene;
12use vortex_dtype::DType;
13use vortex_error::VortexExpect;
14use vortex_error::VortexResult;
15use vortex_error::vortex_bail;
16use vortex_error::vortex_err;
17use vortex_proto::expr as pb;
18use vortex_vector::Datum;
19use vortex_vector::VectorOps;
20
21use crate::ArrayRef;
22use crate::compute;
23use crate::compute::add;
24use crate::compute::and_kleene;
25use crate::compute::compare;
26use crate::compute::div;
27use crate::compute::mul;
28use crate::compute::or_kleene;
29use crate::compute::sub;
30use crate::expr::Arity;
31use crate::expr::ChildName;
32use crate::expr::ExecutionArgs;
33use crate::expr::ExprId;
34use crate::expr::StatsCatalog;
35use crate::expr::VTable;
36use crate::expr::VTableExt;
37use crate::expr::expression::Expression;
38use crate::expr::exprs::literal::lit;
39use crate::expr::exprs::operators::Operator;
40use crate::expr::stats::Stat;
41
42pub struct Binary;
43
44impl VTable for Binary {
45    type Options = Operator;
46
47    fn id(&self) -> ExprId {
48        ExprId::from("vortex.binary")
49    }
50
51    fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
52        Ok(Some(
53            pb::BinaryOpts {
54                op: (*instance).into(),
55            }
56            .encode_to_vec(),
57        ))
58    }
59
60    fn deserialize(&self, metadata: &[u8]) -> VortexResult<Self::Options> {
61        let opts = pb::BinaryOpts::decode(metadata)?;
62        Operator::try_from(opts.op)
63    }
64
65    fn arity(&self, _options: &Self::Options) -> Arity {
66        Arity::Exact(2)
67    }
68
69    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
70        match child_idx {
71            0 => ChildName::from("lhs"),
72            1 => ChildName::from("rhs"),
73            _ => unreachable!("Binary has only two children"),
74        }
75    }
76
77    fn fmt_sql(
78        &self,
79        operator: &Operator,
80        expr: &Expression,
81        f: &mut Formatter<'_>,
82    ) -> std::fmt::Result {
83        write!(f, "(")?;
84        expr.child(0).fmt_sql(f)?;
85        write!(f, " {} ", operator)?;
86        expr.child(1).fmt_sql(f)?;
87        write!(f, ")")
88    }
89
90    fn return_dtype(&self, operator: &Operator, arg_dtypes: &[DType]) -> VortexResult<DType> {
91        let lhs = &arg_dtypes[0];
92        let rhs = &arg_dtypes[1];
93
94        if operator.is_arithmetic() {
95            if lhs.is_primitive() && lhs.eq_ignore_nullability(rhs) {
96                return Ok(lhs.with_nullability(lhs.nullability() | rhs.nullability()));
97            }
98            vortex_bail!(
99                "incompatible types for arithmetic operation: {} {}",
100                lhs,
101                rhs
102            );
103        }
104
105        Ok(DType::Bool((lhs.is_nullable() || rhs.is_nullable()).into()))
106    }
107
108    fn evaluate(
109        &self,
110        operator: &Operator,
111        expr: &Expression,
112        scope: &ArrayRef,
113    ) -> VortexResult<ArrayRef> {
114        let lhs = expr.child(0).evaluate(scope)?;
115        let rhs = expr.child(1).evaluate(scope)?;
116
117        match operator {
118            Operator::Eq => compare(&lhs, &rhs, compute::Operator::Eq),
119            Operator::NotEq => compare(&lhs, &rhs, compute::Operator::NotEq),
120            Operator::Lt => compare(&lhs, &rhs, compute::Operator::Lt),
121            Operator::Lte => compare(&lhs, &rhs, compute::Operator::Lte),
122            Operator::Gt => compare(&lhs, &rhs, compute::Operator::Gt),
123            Operator::Gte => compare(&lhs, &rhs, compute::Operator::Gte),
124            Operator::And => and_kleene(&lhs, &rhs),
125            Operator::Or => or_kleene(&lhs, &rhs),
126            Operator::Add => add(&lhs, &rhs),
127            Operator::Sub => sub(&lhs, &rhs),
128            Operator::Mul => mul(&lhs, &rhs),
129            Operator::Div => div(&lhs, &rhs),
130        }
131    }
132
133    fn execute(&self, op: &Operator, args: ExecutionArgs) -> VortexResult<Datum> {
134        let [lhs, rhs]: [Datum; _] = args
135            .datums
136            .try_into()
137            .map_err(|_| vortex_err!("Wrong arg count"))?;
138
139        match op {
140            Operator::And => {
141                return Ok(LogicalAndKleene::and_kleene(&lhs.into_bool(), &rhs.into_bool()).into());
142            }
143            Operator::Or => {
144                return Ok(LogicalOrKleene::or_kleene(&lhs.into_bool(), &rhs.into_bool()).into());
145            }
146            _ => {}
147        }
148
149        let lhs = lhs.into_arrow()?;
150        let rhs = rhs.into_arrow()?;
151
152        let vector = match op {
153            Operator::Eq => cmp::eq(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(),
154            Operator::NotEq => cmp::neq(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(),
155            Operator::Gt => cmp::gt(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(),
156            Operator::Gte => cmp::gt_eq(lhs.as_ref(), rhs.as_ref())?
157                .into_vector()?
158                .into(),
159            Operator::Lt => cmp::lt(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(),
160            Operator::Lte => cmp::lt_eq(lhs.as_ref(), rhs.as_ref())?
161                .into_vector()?
162                .into(),
163
164            Operator::Add => {
165                arrow_arith::numeric::add(lhs.as_ref(), rhs.as_ref())?.into_vector()?
166            }
167            Operator::Sub => {
168                arrow_arith::numeric::sub(lhs.as_ref(), rhs.as_ref())?.into_vector()?
169            }
170            Operator::Mul => {
171                arrow_arith::numeric::mul(lhs.as_ref(), rhs.as_ref())?.into_vector()?
172            }
173            Operator::Div => {
174                arrow_arith::numeric::div(lhs.as_ref(), rhs.as_ref())?.into_vector()?
175            }
176            Operator::And | Operator::Or => {
177                unreachable!("Already dealt with above")
178            }
179        };
180
181        // If both inputs are scalars, return a scalar datum.
182        if lhs.get().1 && rhs.get().1 {
183            return Ok(Datum::Scalar(vector.scalar_at(0)));
184        }
185
186        Ok(Datum::Vector(vector))
187    }
188
189    fn stat_falsification(
190        &self,
191        operator: &Operator,
192        expr: &Expression,
193        catalog: &dyn StatsCatalog,
194    ) -> Option<Expression> {
195        // Wrap another predicate with an optional NaNCount check, if the stat is available.
196        //
197        // For example, regular pruning conversion for `A >= B` would be
198        //
199        //      A.max < B.min
200        //
201        // With NaN predicate introduction, we'd conjunct it with a check for NaNCount, resulting
202        // in:
203        //
204        //      (A.nan_count = 0) AND (B.nan_count = 0) AND A.max < B.min
205        //
206        // Non-floating point column and literal expressions should be unaffected as they do not
207        // have a nan_count statistic defined.
208        #[inline]
209        fn with_nan_predicate(
210            lhs: &Expression,
211            rhs: &Expression,
212            value_predicate: Expression,
213            catalog: &dyn StatsCatalog,
214        ) -> Expression {
215            let nan_predicate = lhs
216                .stat_expression(Stat::NaNCount, catalog)
217                .into_iter()
218                .chain(rhs.stat_expression(Stat::NaNCount, catalog))
219                .map(|nans| eq(nans, lit(0u64)))
220                .reduce(and);
221
222            if let Some(nan_check) = nan_predicate {
223                and(nan_check, value_predicate)
224            } else {
225                value_predicate
226            }
227        }
228
229        let lhs = expr.child(0);
230        let rhs = expr.child(1);
231        match operator {
232            Operator::Eq => {
233                let min_lhs = lhs.stat_min(catalog);
234                let max_lhs = lhs.stat_max(catalog);
235
236                let min_rhs = rhs.stat_min(catalog);
237                let max_rhs = rhs.stat_max(catalog);
238
239                let left = min_lhs.zip(max_rhs).map(|(a, b)| gt(a, b));
240                let right = min_rhs.zip(max_lhs).map(|(a, b)| gt(a, b));
241
242                let min_max_check = left.into_iter().chain(right).reduce(or)?;
243
244                // NaN is not captured by the min/max stat, so we must check NaNCount before pruning
245                Some(with_nan_predicate(lhs, rhs, min_max_check, catalog))
246            }
247            Operator::NotEq => {
248                let min_lhs = lhs.stat_min(catalog)?;
249                let max_lhs = lhs.stat_max(catalog)?;
250
251                let min_rhs = rhs.stat_min(catalog)?;
252                let max_rhs = rhs.stat_max(catalog)?;
253
254                let min_max_check = and(eq(min_lhs, max_rhs), eq(max_lhs, min_rhs));
255
256                Some(with_nan_predicate(lhs, rhs, min_max_check, catalog))
257            }
258            Operator::Gt => {
259                let min_max_check = lt_eq(lhs.stat_max(catalog)?, rhs.stat_min(catalog)?);
260
261                Some(with_nan_predicate(lhs, rhs, min_max_check, catalog))
262            }
263            Operator::Gte => {
264                // NaN is not captured by the min/max stat, so we must check NaNCount before pruning
265                let min_max_check = lt(lhs.stat_max(catalog)?, rhs.stat_min(catalog)?);
266
267                Some(with_nan_predicate(lhs, rhs, min_max_check, catalog))
268            }
269            Operator::Lt => {
270                // NaN is not captured by the min/max stat, so we must check NaNCount before pruning
271                let min_max_check = gt_eq(lhs.stat_min(catalog)?, rhs.stat_max(catalog)?);
272
273                Some(with_nan_predicate(lhs, rhs, min_max_check, catalog))
274            }
275            Operator::Lte => {
276                // NaN is not captured by the min/max stat, so we must check NaNCount before pruning
277                let min_max_check = gt(lhs.stat_min(catalog)?, rhs.stat_max(catalog)?);
278
279                Some(with_nan_predicate(lhs, rhs, min_max_check, catalog))
280            }
281            Operator::And => lhs
282                .stat_falsification(catalog)
283                .into_iter()
284                .chain(rhs.stat_falsification(catalog))
285                .reduce(or),
286            Operator::Or => Some(and(
287                lhs.stat_falsification(catalog)?,
288                rhs.stat_falsification(catalog)?,
289            )),
290            Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => None,
291        }
292    }
293
294    fn is_null_sensitive(&self, _operator: &Operator) -> bool {
295        false
296    }
297
298    fn is_fallible(&self, operator: &Operator) -> bool {
299        // Opt-in not out for fallibility.
300        // Arithmetic operations could be better modelled here.
301        let infallible = matches!(
302            operator,
303            Operator::Eq
304                | Operator::NotEq
305                | Operator::Gt
306                | Operator::Gte
307                | Operator::Lt
308                | Operator::Lte
309                | Operator::And
310                | Operator::Or
311        );
312
313        !infallible
314    }
315}
316
317/// Create a new [`Binary`] using the [`Eq`](crate::expr::exprs::operators::Operator::Eq) operator.
318///
319/// ## Example usage
320///
321/// ```
322/// # use vortex_array::arrays::{BoolArray, PrimitiveArray};
323/// # use vortex_array::{Array, IntoArray, ToCanonical};
324/// # use vortex_array::validity::Validity;
325/// # use vortex_buffer::buffer;
326/// # use vortex_array::expr::{eq, root, lit};
327/// let xs = PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::NonNullable);
328/// let result = eq(root(), lit(3)).evaluate(&xs.to_array()).unwrap();
329///
330/// assert_eq!(
331///     result.to_bool().bit_buffer(),
332///     BoolArray::from_iter(vec![false, false, true]).bit_buffer(),
333/// );
334/// ```
335pub fn eq(lhs: Expression, rhs: Expression) -> Expression {
336    Binary
337        .try_new_expr(Operator::Eq, [lhs, rhs])
338        .vortex_expect("Failed to create Eq binary expression")
339}
340
341/// Create a new [`Binary`] using the [`NotEq`](crate::expr::exprs::operators::Operator::NotEq) operator.
342///
343/// ## Example usage
344///
345/// ```
346/// # use vortex_array::arrays::{BoolArray, PrimitiveArray};
347/// # use vortex_array::{IntoArray, ToCanonical};
348/// # use vortex_array::validity::Validity;
349/// # use vortex_buffer::buffer;
350/// # use vortex_array::expr::{root, lit, not_eq};
351/// let xs = PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::NonNullable);
352/// let result = not_eq(root(), lit(3)).evaluate(&xs.to_array()).unwrap();
353///
354/// assert_eq!(
355///     result.to_bool().bit_buffer(),
356///     BoolArray::from_iter(vec![true, true, false]).bit_buffer(),
357/// );
358/// ```
359pub fn not_eq(lhs: Expression, rhs: Expression) -> Expression {
360    Binary
361        .try_new_expr(Operator::NotEq, [lhs, rhs])
362        .vortex_expect("Failed to create NotEq binary expression")
363}
364
365/// Create a new [`Binary`] using the [`Gte`](crate::expr::exprs::operators::Operator::Gte) operator.
366///
367/// ## Example usage
368///
369/// ```
370/// # use vortex_array::arrays::{BoolArray, PrimitiveArray };
371/// # use vortex_array::{IntoArray, ToCanonical};
372/// # use vortex_array::validity::Validity;
373/// # use vortex_buffer::buffer;
374/// # use vortex_array::expr::{gt_eq, root, lit};
375/// let xs = PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::NonNullable);
376/// let result = gt_eq(root(), lit(3)).evaluate(&xs.to_array()).unwrap();
377///
378/// assert_eq!(
379///     result.to_bool().bit_buffer(),
380///     BoolArray::from_iter(vec![false, false, true]).bit_buffer(),
381/// );
382/// ```
383pub fn gt_eq(lhs: Expression, rhs: Expression) -> Expression {
384    Binary
385        .try_new_expr(Operator::Gte, [lhs, rhs])
386        .vortex_expect("Failed to create Gte binary expression")
387}
388
389/// Create a new [`Binary`] using the [`Gt`](crate::expr::exprs::operators::Operator::Gt) operator.
390///
391/// ## Example usage
392///
393/// ```
394/// # use vortex_array::arrays::{BoolArray, PrimitiveArray };
395/// # use vortex_array::{IntoArray, ToCanonical};
396/// # use vortex_array::validity::Validity;
397/// # use vortex_buffer::buffer;
398/// # use vortex_array::expr::{gt, root, lit};
399/// let xs = PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::NonNullable);
400/// let result = gt(root(), lit(2)).evaluate(&xs.to_array()).unwrap();
401///
402/// assert_eq!(
403///     result.to_bool().bit_buffer(),
404///     BoolArray::from_iter(vec![false, false, true]).bit_buffer(),
405/// );
406/// ```
407pub fn gt(lhs: Expression, rhs: Expression) -> Expression {
408    Binary
409        .try_new_expr(Operator::Gt, [lhs, rhs])
410        .vortex_expect("Failed to create Gt binary expression")
411}
412
413/// Create a new [`Binary`] using the [`Lte`](crate::expr::exprs::operators::Operator::Lte) operator.
414///
415/// ## Example usage
416///
417/// ```
418/// # use vortex_array::arrays::{BoolArray, PrimitiveArray };
419/// # use vortex_array::{IntoArray, ToCanonical};
420/// # use vortex_array::validity::Validity;
421/// # use vortex_buffer::buffer;
422/// # use vortex_array::expr::{root, lit, lt_eq};
423/// let xs = PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::NonNullable);
424/// let result = lt_eq(root(), lit(2)).evaluate(&xs.to_array()).unwrap();
425///
426/// assert_eq!(
427///     result.to_bool().bit_buffer(),
428///     BoolArray::from_iter(vec![true, true, false]).bit_buffer(),
429/// );
430/// ```
431pub fn lt_eq(lhs: Expression, rhs: Expression) -> Expression {
432    Binary
433        .try_new_expr(Operator::Lte, [lhs, rhs])
434        .vortex_expect("Failed to create Lte binary expression")
435}
436
437/// Create a new [`Binary`] using the [`Lt`](crate::expr::exprs::operators::Operator::Lt) operator.
438///
439/// ## Example usage
440///
441/// ```
442/// # use vortex_array::arrays::{BoolArray, PrimitiveArray };
443/// # use vortex_array::{IntoArray, ToCanonical};
444/// # use vortex_array::validity::Validity;
445/// # use vortex_buffer::buffer;
446/// # use vortex_array::expr::{root, lit, lt};
447/// let xs = PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::NonNullable);
448/// let result = lt(root(), lit(3)).evaluate(&xs.to_array()).unwrap();
449///
450/// assert_eq!(
451///     result.to_bool().bit_buffer(),
452///     BoolArray::from_iter(vec![true, true, false]).bit_buffer(),
453/// );
454/// ```
455pub fn lt(lhs: Expression, rhs: Expression) -> Expression {
456    Binary
457        .try_new_expr(Operator::Lt, [lhs, rhs])
458        .vortex_expect("Failed to create Lt binary expression")
459}
460
461/// Create a new [`Binary`] using the [`Or`](crate::expr::exprs::operators::Operator::Or) operator.
462///
463/// ## Example usage
464///
465/// ```
466/// # use vortex_array::arrays::BoolArray;
467/// # use vortex_array::{IntoArray, ToCanonical};
468/// # use vortex_array::expr::{root, lit, or};
469/// let xs = BoolArray::from_iter(vec![true, false, true]);
470/// let result = or(root(), lit(false)).evaluate(&xs.to_array()).unwrap();
471///
472/// assert_eq!(
473///     result.to_bool().bit_buffer(),
474///     BoolArray::from_iter(vec![true, false, true]).bit_buffer(),
475/// );
476/// ```
477pub fn or(lhs: Expression, rhs: Expression) -> Expression {
478    Binary
479        .try_new_expr(Operator::Or, [lhs, rhs])
480        .vortex_expect("Failed to create Or binary expression")
481}
482
483/// Collects a list of `or`ed values into a single vortex, expr
484/// [x, y, z] => x or (y or z)
485pub fn or_collect<I>(iter: I) -> Option<Expression>
486where
487    I: IntoIterator<Item = Expression>,
488    I::IntoIter: DoubleEndedIterator<Item = Expression>,
489{
490    let mut iter = iter.into_iter();
491    let first = iter.next_back()?;
492    Some(iter.rfold(first, |acc, elem| or(elem, acc)))
493}
494
495/// Create a new [`Binary`] using the [`And`](crate::expr::exprs::operators::Operator::And) operator.
496///
497/// ## Example usage
498///
499/// ```
500/// # use vortex_array::arrays::BoolArray;
501/// # use vortex_array::{IntoArray, ToCanonical};
502/// # use vortex_array::expr::{and, root, lit};
503/// let xs = BoolArray::from_iter(vec![true, false, true]);
504/// let result = and(root(), lit(true)).evaluate(&xs.to_array()).unwrap();
505///
506/// assert_eq!(
507///     result.to_bool().bit_buffer(),
508///     BoolArray::from_iter(vec![true, false, true]).bit_buffer(),
509/// );
510/// ```
511pub fn and(lhs: Expression, rhs: Expression) -> Expression {
512    Binary
513        .try_new_expr(Operator::And, [lhs, rhs])
514        .vortex_expect("Failed to create And binary expression")
515}
516
517/// Collects a list of `and`ed values into a single vortex, expr
518/// [x, y, z] => x and (y and z)
519pub fn and_collect<I>(iter: I) -> Option<Expression>
520where
521    I: IntoIterator<Item = Expression>,
522    I::IntoIter: DoubleEndedIterator<Item = Expression>,
523{
524    let mut iter = iter.into_iter();
525    let first = iter.next_back()?;
526    Some(iter.rfold(first, |acc, elem| and(elem, acc)))
527}
528
529/// Collects a list of `and`ed values into a single vortex, expr
530/// [x, y, z] => x and (y and z)
531pub fn and_collect_right<I>(iter: I) -> Option<Expression>
532where
533    I: IntoIterator<Item = Expression>,
534{
535    let iter = iter.into_iter();
536    iter.reduce(and)
537}
538
539/// Create a new [`Binary`] using the [`Add`](crate::expr::exprs::operators::Operator::Add) operator.
540///
541/// ## Example usage
542///
543/// ```
544/// # use vortex_array::IntoArray;
545/// # use vortex_array::arrow::IntoArrowArray as _;
546/// # use vortex_buffer::buffer;
547/// # use vortex_array::expr::{checked_add, lit, root};
548/// let xs = buffer![1, 2, 3].into_array();
549/// let result = checked_add(root(), lit(5))
550///     .evaluate(&xs.to_array())
551///     .unwrap();
552///
553/// assert_eq!(
554///     &result.into_arrow_preferred().unwrap(),
555///     &buffer![6, 7, 8]
556///         .into_array()
557///         .into_arrow_preferred()
558///         .unwrap()
559/// );
560/// ```
561pub fn checked_add(lhs: Expression, rhs: Expression) -> Expression {
562    Binary
563        .try_new_expr(Operator::Add, [lhs, rhs])
564        .vortex_expect("Failed to create Add binary expression")
565}
566
567#[cfg(test)]
568mod tests {
569    use vortex_dtype::DType;
570    use vortex_dtype::Nullability;
571
572    use super::and;
573    use super::and_collect;
574    use super::and_collect_right;
575    use super::eq;
576    use super::gt;
577    use super::gt_eq;
578    use super::lt;
579    use super::lt_eq;
580    use super::not_eq;
581    use super::or;
582    use crate::expr::Expression;
583    use crate::expr::exprs::get_item::col;
584    use crate::expr::exprs::literal::lit;
585    use crate::expr::test_harness;
586
587    #[test]
588    fn and_collect_left_assoc() {
589        let values = vec![lit(1), lit(2), lit(3)];
590        assert_eq!(
591            Some(and(lit(1), and(lit(2), lit(3)))),
592            and_collect(values.into_iter())
593        );
594    }
595
596    #[test]
597    fn and_collect_right_assoc() {
598        let values = vec![lit(1), lit(2), lit(3)];
599        assert_eq!(
600            Some(and(and(lit(1), lit(2)), lit(3))),
601            and_collect_right(values.into_iter())
602        );
603    }
604
605    #[test]
606    fn dtype() {
607        let dtype = test_harness::struct_dtype();
608        let bool1: Expression = col("bool1");
609        let bool2: Expression = col("bool2");
610        assert_eq!(
611            and(bool1.clone(), bool2.clone())
612                .return_dtype(&dtype)
613                .unwrap(),
614            DType::Bool(Nullability::NonNullable)
615        );
616        assert_eq!(
617            or(bool1, bool2).return_dtype(&dtype).unwrap(),
618            DType::Bool(Nullability::NonNullable)
619        );
620
621        let col1: Expression = col("col1");
622        let col2: Expression = col("col2");
623
624        assert_eq!(
625            eq(col1.clone(), col2.clone()).return_dtype(&dtype).unwrap(),
626            DType::Bool(Nullability::Nullable)
627        );
628        assert_eq!(
629            not_eq(col1.clone(), col2.clone())
630                .return_dtype(&dtype)
631                .unwrap(),
632            DType::Bool(Nullability::Nullable)
633        );
634        assert_eq!(
635            gt(col1.clone(), col2.clone()).return_dtype(&dtype).unwrap(),
636            DType::Bool(Nullability::Nullable)
637        );
638        assert_eq!(
639            gt_eq(col1.clone(), col2.clone())
640                .return_dtype(&dtype)
641                .unwrap(),
642            DType::Bool(Nullability::Nullable)
643        );
644        assert_eq!(
645            lt(col1.clone(), col2.clone()).return_dtype(&dtype).unwrap(),
646            DType::Bool(Nullability::Nullable)
647        );
648        assert_eq!(
649            lt_eq(col1.clone(), col2.clone())
650                .return_dtype(&dtype)
651                .unwrap(),
652            DType::Bool(Nullability::Nullable)
653        );
654
655        assert_eq!(
656            or(lt(col1.clone(), col2.clone()), not_eq(col1, col2))
657                .return_dtype(&dtype)
658                .unwrap(),
659            DType::Bool(Nullability::Nullable)
660        );
661    }
662
663    #[test]
664    fn test_display_print() {
665        let expr = gt(lit(1), lit(2));
666        assert_eq!(format!("{expr}"), "(1i32 > 2i32)");
667    }
668}