1use std::borrow::Cow;
16
17use const_currying::const_currying;
18use itertools::Itertools;
19use risingwave_common::util::recursive::{self, Recurse};
20use risingwave_expr::function;
21
22use crate::{ExprError, Result};
23
24fn normalize_pattern(p: &str, escape: Option<u8>) -> Cow<'_, [u8]> {
30 if escape == Some(b'\\') {
31 return Cow::Borrowed(p.as_bytes());
32 }
33
34 let mut normalized = Vec::with_capacity(p.len());
35 let mut after_escape = false;
36
37 for c in p.bytes() {
38 if Some(c) == escape && !after_escape {
39 normalized.push(b'\\');
40 after_escape = true;
41 } else if c == b'\\' {
42 normalized.push(b'\\');
43 if !after_escape {
44 normalized.push(b'\\');
45 }
46 after_escape = false;
47 } else {
48 normalized.push(c);
49 after_escape = false;
50 }
51 }
52
53 Cow::Owned(normalized)
54}
55
56fn like_impl_inner<const CASE_INSENSITIVE: bool>(
57 s: &str,
58 p: &str,
59 escape: Option<u8>,
60) -> Result<bool> {
61 let pattern = normalize_pattern(p, escape);
62 Ok(match_text::<CASE_INSENSITIVE>(s.as_bytes(), pattern.as_ref())?.into_bool())
63}
64
65#[const_currying]
66fn like_impl<const CASE_INSENSITIVE: bool>(
67 s: &str,
68 p: &str,
69 #[maybe_const(consts = [b'\\'])] escape: u8,
70) -> Result<bool> {
71 like_impl_inner::<CASE_INSENSITIVE>(s, p, Some(escape))
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum MatchResult {
76 True,
77 False,
78 Abort,
79}
80
81const MAX_RECURSION_DEPTH: usize = 4096;
85
86impl MatchResult {
87 #[inline]
88 fn into_bool(self) -> bool {
89 matches!(self, MatchResult::True)
90 }
91}
92
93#[inline]
94fn normalize<const CASE_INSENSITIVE: bool>(b: u8) -> u8 {
95 if CASE_INSENSITIVE {
96 b.to_ascii_lowercase()
97 } else {
98 b
99 }
100}
101
102#[inline]
103fn byte_eq<const CASE_INSENSITIVE: bool>(a: u8, b: u8) -> bool {
104 normalize::<CASE_INSENSITIVE>(a) == normalize::<CASE_INSENSITIVE>(b)
105}
106
107pub fn match_text<const CASE_INSENSITIVE: bool>(
108 text: &[u8],
109 pattern: &[u8],
110) -> Result<MatchResult, ExprError> {
111 recursive::tracker!().recurse(|tracker| {
112 if tracker.depth() > MAX_RECURSION_DEPTH {
113 return Err(ExprError::InvalidParam {
114 name: "pattern",
115 reason: "stack depth limit exceeded".into(),
116 });
117 }
118
119 match_text_inner::<CASE_INSENSITIVE>(text, pattern)
120 })
121}
122
123fn match_text_inner<const CASE_INSENSITIVE: bool>(
124 mut text: &[u8],
125 mut pattern: &[u8],
126) -> Result<MatchResult, ExprError> {
127 if pattern == b"%" {
129 return Ok(MatchResult::True);
130 }
131
132 while let (Some((&p, rest_pattern)), Some((&t, rest_text))) =
133 (pattern.split_first(), text.split_first())
134 {
135 match p {
136 b'%' => {
137 pattern = rest_pattern;
138
139 while let Some((&p, rest_pattern)) = pattern.split_first() {
140 match p {
141 b'%' => {
142 pattern = rest_pattern;
144 }
145 b'_' => {
146 let Some((_, rest_text)) = text.split_first() else {
147 return Ok(MatchResult::Abort);
149 };
150
151 text = rest_text;
152 pattern = rest_pattern;
153 }
154 _ => break, }
156 }
157
158 let Some((&first, _)) = pattern.split_first() else {
161 return Ok(MatchResult::True);
162 };
163
164 let first = if first == b'\\' {
166 let Some(&escaped) = pattern.get(1) else {
167 return Err(ExprError::InvalidParam {
168 name: "pattern",
169 reason: "LIKE pattern must not end with escape character".into(),
170 });
171 };
172 escaped
173 } else {
174 first
175 };
176
177 while let Some((&t, rest_text)) = text.split_first() {
178 if byte_eq::<CASE_INSENSITIVE>(t, first) {
179 let matched = match_text::<CASE_INSENSITIVE>(text, pattern)?;
180
181 if matched != MatchResult::False {
182 return Ok(matched); }
184 }
185
186 text = rest_text;
187 }
188
189 return Ok(MatchResult::Abort);
192 }
193 b'_' => {
194 text = rest_text;
195 pattern = rest_pattern;
196 }
197 b'\\' => {
198 let Some((&escaped, rest_pattern)) = rest_pattern.split_first() else {
199 return Err(ExprError::InvalidParam {
200 name: "pattern",
201 reason: "LIKE pattern must not end with escape character".into(),
202 });
203 };
204 if !byte_eq::<CASE_INSENSITIVE>(t, escaped) {
205 return Ok(MatchResult::False);
206 }
207 text = rest_text;
208 pattern = rest_pattern;
209 }
210 literal => {
211 if !byte_eq::<CASE_INSENSITIVE>(t, literal) {
212 return Ok(MatchResult::False);
214 }
215
216 text = rest_text;
217 pattern = rest_pattern;
218 }
219 }
220 }
221
222 if !text.is_empty() {
223 return Ok(MatchResult::False);
225 }
226
227 while pattern.first() == Some(&b'%') {
231 pattern = &pattern[1..];
232 }
233
234 if pattern.is_empty() {
237 Ok(MatchResult::True)
238 } else {
239 Ok(MatchResult::Abort)
240 }
241}
242
243#[function("like(varchar, varchar) -> boolean")]
244pub fn like_default(s: &str, p: &str) -> Result<bool> {
245 like_impl_escape::<false, b'\\'>(s, p)
246}
247
248#[function("i_like(varchar, varchar) -> boolean")]
249pub fn i_like_default(s: &str, p: &str) -> Result<bool> {
250 like_impl_escape::<true, b'\\'>(s, p)
251}
252
253#[function(
254 "like(varchar, varchar, varchar) -> boolean",
255 prebuild = "EscapeChar::from_str($2)?"
256)]
257fn like(s: &str, p: &str, escape: &EscapeChar) -> Result<bool> {
258 like_impl_inner::<false>(s, p, escape.0)
259}
260
261#[function(
262 "i_like(varchar, varchar, varchar) -> boolean",
263 prebuild = "EscapeChar::from_str($2)?"
264)]
265fn i_like(s: &str, p: &str, escape: &EscapeChar) -> Result<bool> {
266 like_impl_inner::<true>(s, p, escape.0)
267}
268
269#[derive(Copy, Clone, Debug)]
271struct EscapeChar(Option<u8>);
272
273impl EscapeChar {
274 fn from_str(escape: &str) -> Result<Self> {
275 if escape.is_empty() {
276 return Ok(Self(None));
277 }
278
279 Itertools::exactly_one(escape.chars())
280 .ok()
281 .and_then(|c| c.as_ascii().map(|c| c.to_u8()))
282 .map(Some)
283 .ok_or_else(|| ExprError::InvalidParam {
284 name: "escape",
285 reason: "only empty or single ascii character is supported now".into(),
286 })
287 .map(Self)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use risingwave_expr::scalar::like::EscapeChar;
294
295 use super::{
296 MAX_RECURSION_DEPTH, i_like, i_like_default, like, like_default, normalize_pattern,
297 };
298
299 static CASES: &[(&str, &str, bool, bool)] = &[
300 (r#"ABCDE"#, r#"%abcde%"#, false, false),
301 (r#"Like, expression"#, r#"Like, expression"#, false, true),
302 (r#"Like, expression"#, r#"Like, %"#, false, true),
303 (r#"Like, expression"#, r#"%, expression"#, false, true),
304 (r#"like"#, r#"li%ke"#, false, true),
305 (r#"like"#, r#"l%ik%e"#, false, true),
306 (r#"like"#, r#"%like%"#, false, true),
307 (r#"like"#, r#"l%i%k%e%"#, false, true),
308 (r#"like"#, r#"_%_e"#, false, true),
309 (r#"like"#, r#"l%__"#, false, true),
310 (r#"like"#, r#"_%_%_%_"#, false, true),
311 (r#"abctest"#, r#"__test"#, false, false),
312 (r#"abctest"#, r#"%_test"#, false, true),
313 (r#"aaaaabbb"#, r#"a%a%a%a%a%a%b"#, false, false),
314 (
315 r#"blush thistle blue yellow saddle"#,
316 r#"%yellow%"#,
317 false,
318 true,
319 ),
320 (r#"ABC_123"#, r#"ABC_123"#, false, true),
321 (r#"ABCD123"#, r#"ABC_123"#, false, true),
322 (r#"ABC_123"#, r"ABC\_123", false, true),
323 (r#"ABCD123"#, r"ABC\_123", false, false),
324 (r"ABC\123", r#"ABC_123"#, false, true),
325 (r"ABC\123", r"ABC\\123", false, true),
326 (r"ABC\123", r"ABC\123", false, false),
327 ("apple", r#"App%"#, true, true),
328 ("banana", r#"B%nana"#, true, true),
329 ("apple", r#"B%nana"#, true, false),
330 ("grape", "Gr_P_", true, true),
331 ];
332
333 #[test]
334 fn test_like() {
335 for (target, pattern, case_insensitive, expected) in CASES {
336 let output = if *case_insensitive {
337 i_like_default(target, pattern)
338 } else {
339 like_default(target, pattern)
340 };
341 assert!(output.is_ok());
342 assert_eq!(
343 output.unwrap(),
344 *expected,
345 "target={}, pattern={}, case_insensitive={}",
346 target,
347 pattern,
348 case_insensitive
349 );
350 }
351 }
352
353 fn assert_normalized_pattern(pattern: &str, escape: &str, expected: &str) {
354 let escape = EscapeChar::from_str(escape).unwrap();
355 let normalized = normalize_pattern(pattern, escape.0);
356 assert_eq!(
357 normalized,
358 expected.as_bytes(),
359 "pattern={pattern}, escape={escape:?}"
360 );
361 }
362
363 #[test]
364 fn test_normalize_pattern() {
365 let testcases = [
366 (r"ABC\_123", r"\", r"ABC\_123"),
368 (r"ABC\\123", r"\", r"ABC\\123"),
369 ("h%", "#", "h%"),
371 ("ind_o", "$", "ind_o"),
372 ("h#%", "#", r"h\%"),
374 ("h#%%", "#", r"h\%%"),
375 ("i$_d_o", "$", r"i\_d_o"),
376 ("i$_d%o", "$", r"i\_d%o"),
377 ("m%aca", "%", r"m\aca"),
379 ("m%a%%a", "%", r"m\a\%a"),
380 ("b_ear", "_", r"b\ear"),
381 ("b_e__r", "_", r"b\e\_r"),
382 ("__e__r", "_", r"\_e\_r"),
383 ("____r", "_", r"\_\_r"),
384 ("a_c", "", "a_c"),
386 (r"a\_c", "", r"a\\_c"),
387 ];
388
389 for (pattern, escape, expected) in testcases {
390 assert_normalized_pattern(pattern, escape, expected);
391 }
392 }
393
394 #[test]
395 fn test_normalize_pattern_ends_with_escape() {
396 let testcases = [
397 (r"abc\", r"\", r"abc\"),
398 ("h#", "#", r"h\"),
399 ("_____", "_", r"\_\_\"),
400 ("m%", "%", r"m\"),
401 ];
402
403 for (pattern, escape, expected) in testcases {
404 assert_normalized_pattern(pattern, escape, expected);
405 }
406
407 assert_normalized_pattern(r"abc\", "", r"abc\\");
408 }
409
410 static ESCAPE_CASES: &[(&str, &str, &str, bool)] = &[
411 (r"bear", r"b_ear", r"_", true),
412 (r"be_r", r"b_e__r", r"_", true),
413 (r"be__r", r"b_e___r", r"_", false),
414 (r"be__r", r"b_e____r", r"_", true),
415 (r"be___r", r"b_e_____r", r"_", false),
416 (r"be___r", r"b_e______r", r"_", true),
417 (r"be_r", r"__e__r", r"_", false),
418 (r"___r", r"____r", r"_", false),
419 (r"__r", r"____r", r"_", true),
420 (r"maca", r"m%aca", r"%", true),
421 (r"ma%a", r"m%a%%a", r"%", true),
422 (r"abc", r"a_c", r"", true),
423 (r"a_c", r"a\_c", r"", false),
424 (r"a\_c", r"a\_c", r"", true),
425 (r"abc", r"abc\", r"\", false),
426 (r"h", r"h#", "#", false),
427 (r"__", r"_____", "_", false),
428 (r"m", r"m%", "%", false),
429 ];
430
431 #[test]
432 fn test_escape_like() {
433 for (target, pattern, escape, expected) in ESCAPE_CASES {
434 let output = like(target, pattern, &EscapeChar::from_str(escape).unwrap());
435 assert!(output.is_ok());
436 assert_eq!(
437 output.unwrap(),
438 *expected,
439 "target={}, pattern={}, escape={}",
440 target,
441 pattern,
442 escape
443 );
444 }
445 }
446
447 #[test]
448 fn test_like_pattern_ends_with_escape() {
449 let escape = EscapeChar::from_str("_").unwrap();
450 assert!(like("____", "_____", &escape).is_err());
451 assert!(i_like("____", "_____", &escape).is_err());
452 }
453
454 #[test]
455 fn test_like_recursion_depth_limit() {
456 let text = "a".repeat(MAX_RECURSION_DEPTH - 1);
457 let pattern = "%a".repeat(MAX_RECURSION_DEPTH - 1);
458 assert!(like_default(&text, &pattern).unwrap());
459
460 let text = "a".repeat(MAX_RECURSION_DEPTH);
461 let pattern = "%a".repeat(MAX_RECURSION_DEPTH);
462
463 assert!(like_default(&text, &pattern).is_err());
464 }
465}