1use std::hash::Hash;
5
6use vortex_array::compute::{LikeOptions, like};
7use vortex_array::{ArrayRef, DeserializeMetadata, ProstMetadata};
8use vortex_dtype::DType;
9use vortex_error::{VortexResult, vortex_bail};
10use vortex_proto::expr as pb;
11
12use crate::display::{DisplayAs, DisplayFormat};
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 DisplayAs for LikeExpr {
149 fn fmt_as(&self, df: DisplayFormat, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150 match df {
151 DisplayFormat::Compact => {
152 write!(f, "{} LIKE {}", self.child(), self.pattern())
153 }
154 DisplayFormat::Tree => {
155 write!(f, "Like")
156 }
157 }
158 }
159
160 fn child_names(&self) -> Option<Vec<String>> {
161 Some(vec!["child".to_string(), "pattern".to_string()])
162 }
163}
164
165impl AnalysisExpr for LikeExpr {}
166
167#[cfg(test)]
168mod tests {
169 use vortex_array::ToCanonical;
170 use vortex_array::arrays::BoolArray;
171 use vortex_dtype::{DType, Nullability};
172
173 use crate::{LikeExpr, Scope, get_item, lit, not, root};
174
175 #[test]
176 fn invert_booleans() {
177 let not_expr = not(root());
178 let bools = BoolArray::from_iter([false, true, false, false, true, true]);
179 assert_eq!(
180 not_expr
181 .evaluate(&Scope::new(bools.to_array()))
182 .unwrap()
183 .to_bool()
184 .boolean_buffer()
185 .iter()
186 .collect::<Vec<_>>(),
187 vec![true, false, true, true, false, false]
188 );
189 }
190
191 #[test]
192 fn dtype() {
193 let dtype = DType::Utf8(Nullability::NonNullable);
194 let like_expr = LikeExpr::new(root(), lit("%test%"), false, false);
195 assert_eq!(
196 like_expr.return_dtype(&dtype).unwrap(),
197 DType::Bool(Nullability::NonNullable)
198 );
199 }
200
201 #[test]
202 fn test_display() {
203 let expr = LikeExpr::new(get_item("name", root()), lit("%john%"), false, false);
204 assert_eq!(expr.to_string(), "$.name LIKE \"%john%\"");
205
206 let expr2 = LikeExpr::new(root(), lit("test*"), true, true);
207 assert_eq!(expr2.to_string(), "$ LIKE \"test*\"");
208 }
209}