vortex_expr/exprs/
like.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::hash::Hash;
6
7use vortex_array::compute::{LikeOptions, like};
8use vortex_array::{ArrayRef, DeserializeMetadata, ProstMetadata};
9use vortex_dtype::DType;
10use vortex_error::{VortexResult, vortex_bail};
11use vortex_proto::expr as pb;
12
13use crate::{AnalysisExpr, ExprEncodingRef, ExprId, ExprRef, IntoExpr, Scope, VTable, vtable};
14
15vtable!(Like);
16
17#[allow(clippy::derived_hash_with_manual_eq)]
18#[derive(Clone, Debug, Hash, Eq)]
19pub struct LikeExpr {
20    child: ExprRef,
21    pattern: ExprRef,
22    negated: bool,
23    case_insensitive: bool,
24}
25
26impl PartialEq for LikeExpr {
27    fn eq(&self, other: &Self) -> bool {
28        self.child.eq(&other.child)
29            && self.pattern.eq(&other.pattern)
30            && self.negated == other.negated
31            && self.case_insensitive == other.case_insensitive
32    }
33}
34
35pub struct LikeExprEncoding;
36
37impl VTable for LikeVTable {
38    type Expr = LikeExpr;
39    type Encoding = LikeExprEncoding;
40    type Metadata = ProstMetadata<pb::LikeOpts>;
41
42    fn id(_encoding: &Self::Encoding) -> ExprId {
43        ExprId::new_ref("like")
44    }
45
46    fn encoding(_expr: &Self::Expr) -> ExprEncodingRef {
47        ExprEncodingRef::new_ref(LikeExprEncoding.as_ref())
48    }
49
50    fn metadata(expr: &Self::Expr) -> Option<Self::Metadata> {
51        Some(ProstMetadata(pb::LikeOpts {
52            negated: expr.negated,
53            case_insensitive: expr.case_insensitive,
54        }))
55    }
56
57    fn children(expr: &Self::Expr) -> Vec<&ExprRef> {
58        vec![&expr.child, &expr.pattern]
59    }
60
61    fn with_children(expr: &Self::Expr, children: Vec<ExprRef>) -> VortexResult<Self::Expr> {
62        Ok(LikeExpr::new(
63            children[0].clone(),
64            children[1].clone(),
65            expr.negated,
66            expr.case_insensitive,
67        ))
68    }
69
70    fn build(
71        _encoding: &Self::Encoding,
72        metadata: &<Self::Metadata as DeserializeMetadata>::Output,
73        children: Vec<ExprRef>,
74    ) -> VortexResult<Self::Expr> {
75        if children.len() != 2 {
76            vortex_bail!(
77                "Like expression must have exactly 2 children, got {}",
78                children.len()
79            );
80        }
81
82        Ok(LikeExpr::new(
83            children[0].clone(),
84            children[1].clone(),
85            metadata.negated,
86            metadata.case_insensitive,
87        ))
88    }
89
90    fn evaluate(expr: &Self::Expr, scope: &Scope) -> VortexResult<ArrayRef> {
91        let child = expr.child().unchecked_evaluate(scope)?;
92        let pattern = expr.pattern().unchecked_evaluate(scope)?;
93        like(
94            &child,
95            &pattern,
96            LikeOptions {
97                negated: expr.negated,
98                case_insensitive: expr.case_insensitive,
99            },
100        )
101    }
102
103    fn return_dtype(expr: &Self::Expr, scope: &DType) -> VortexResult<DType> {
104        let input = expr.child().return_dtype(scope)?;
105        let pattern = expr.pattern().return_dtype(scope)?;
106        Ok(DType::Bool(
107            (input.is_nullable() || pattern.is_nullable()).into(),
108        ))
109    }
110}
111
112impl LikeExpr {
113    pub fn new(child: ExprRef, pattern: ExprRef, negated: bool, case_insensitive: bool) -> Self {
114        Self {
115            child,
116            pattern,
117            negated,
118            case_insensitive,
119        }
120    }
121
122    pub fn new_expr(
123        child: ExprRef,
124        pattern: ExprRef,
125        negated: bool,
126        case_insensitive: bool,
127    ) -> ExprRef {
128        Self::new(child, pattern, negated, case_insensitive).into_expr()
129    }
130
131    pub fn child(&self) -> &ExprRef {
132        &self.child
133    }
134
135    pub fn pattern(&self) -> &ExprRef {
136        &self.pattern
137    }
138
139    pub fn negated(&self) -> bool {
140        self.negated
141    }
142
143    pub fn case_insensitive(&self) -> bool {
144        self.case_insensitive
145    }
146}
147
148impl Display for LikeExpr {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        write!(f, "{} LIKE {}", self.child(), self.pattern())
151    }
152}
153
154impl AnalysisExpr for LikeExpr {}
155
156#[cfg(test)]
157mod tests {
158    use vortex_array::ToCanonical;
159    use vortex_array::arrays::BoolArray;
160    use vortex_dtype::{DType, Nullability};
161
162    use crate::{LikeExpr, Scope, lit, not, root};
163
164    #[test]
165    fn invert_booleans() {
166        let not_expr = not(root());
167        let bools = BoolArray::from_iter([false, true, false, false, true, true]);
168        assert_eq!(
169            not_expr
170                .evaluate(&Scope::new(bools.to_array()))
171                .unwrap()
172                .to_bool()
173                .unwrap()
174                .boolean_buffer()
175                .iter()
176                .collect::<Vec<_>>(),
177            vec![true, false, true, true, false, false]
178        );
179    }
180
181    #[test]
182    fn dtype() {
183        let dtype = DType::Utf8(Nullability::NonNullable);
184        let like_expr = LikeExpr::new(root(), lit("%test%"), false, false);
185        assert_eq!(
186            like_expr.return_dtype(&dtype).unwrap(),
187            DType::Bool(Nullability::NonNullable)
188        );
189    }
190}