1use const_currying::const_currying;
16use itertools::Itertools;
17use risingwave_expr::function;
18
19use crate::{ExprError, Result};
20
21#[const_currying]
22fn like_impl<const CASE_INSENSITIVE: bool>(
23 s: &str,
24 p: &str,
25 #[maybe_const(consts = [b'\\'])] escape: u8,
26) -> bool {
27 let (mut px, mut sx) = (0, 0);
28 let (mut next_px, mut next_sx) = (0, 0);
29 let (pbytes, sbytes) = (p.as_bytes(), s.as_bytes());
30 while px < pbytes.len() || sx < sbytes.len() {
31 if px < pbytes.len() {
32 let c = pbytes[px];
33 match c {
34 b'_' => {
35 if escape == b'_' {
36 if px > 0 && pbytes[px - 1] != escape {
37 px += 1;
38 continue;
39 }
40 }
41 if sx < sbytes.len() {
42 px += 1;
43 sx += 1;
44 continue;
45 }
46 }
47 b'%' => {
48 next_px = px;
49 next_sx = sx + 1;
50 px += 1;
51 continue;
52 }
53 mut pc => {
54 if ((!CASE_INSENSITIVE && pc == escape)
55 || (CASE_INSENSITIVE && pc.eq_ignore_ascii_case(&escape)))
56 && px + 1 < pbytes.len()
57 {
58 px += 1;
59 pc = pbytes[px];
60 }
61 if sx < sbytes.len()
62 && ((!CASE_INSENSITIVE && sbytes[sx] == pc)
63 || (CASE_INSENSITIVE && sbytes[sx].eq_ignore_ascii_case(&pc)))
64 {
65 px += 1;
66 sx += 1;
67 continue;
68 }
69 }
70 }
71 }
72 if 0 < next_sx && next_sx <= sbytes.len() {
73 px = next_px;
74 sx = next_sx;
75 continue;
76 }
77 return false;
78 }
79 true
80}
81
82#[function("like(varchar, varchar) -> boolean")]
83pub fn like_default(s: &str, p: &str) -> bool {
84 like_impl_escape::<false, b'\\'>(s, p)
85}
86
87#[function("i_like(varchar, varchar) -> boolean")]
88pub fn i_like_default(s: &str, p: &str) -> bool {
89 like_impl_escape::<true, b'\\'>(s, p)
90}
91
92#[function(
93 "like(varchar, varchar, varchar) -> boolean",
94 prebuild = "EscapeChar::from_str($2)?"
95)]
96fn like(s: &str, p: &str, escape: &EscapeChar) -> bool {
97 like_impl::<false>(s, p, escape.0)
98}
99
100#[derive(Copy, Clone, Debug)]
103struct EscapeChar(u8);
104
105impl EscapeChar {
106 fn from_str(escape: &str) -> Result<Self> {
107 Itertools::exactly_one(escape.chars())
108 .ok()
109 .and_then(|c| c.as_ascii().map(|c| c.to_u8()))
110 .ok_or_else(|| ExprError::InvalidParam {
111 name: "escape",
112 reason: "only single ascii character is supported now".into(),
113 })
114 .map(Self)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use risingwave_expr::scalar::like::EscapeChar;
121
122 use super::{i_like_default, like, like_default};
123
124 static CASES: &[(&str, &str, bool, bool)] = &[
125 (r#"ABCDE"#, r#"%abcde%"#, false, false),
126 (r#"Like, expression"#, r#"Like, expression"#, false, true),
127 (r#"Like, expression"#, r#"Like, %"#, false, true),
128 (r#"Like, expression"#, r#"%, expression"#, false, true),
129 (r#"like"#, r#"li%ke"#, false, true),
130 (r#"like"#, r#"l%ik%e"#, false, true),
131 (r#"like"#, r#"%like%"#, false, true),
132 (r#"like"#, r#"l%i%k%e%"#, false, true),
133 (r#"like"#, r#"_%_e"#, false, true),
134 (r#"like"#, r#"l%__"#, false, true),
135 (r#"like"#, r#"_%_%_%_"#, false, true),
136 (r#"abctest"#, r#"__test"#, false, false),
137 (r#"abctest"#, r#"%_test"#, false, true),
138 (r#"aaaaabbb"#, r#"a%a%a%a%a%a%b"#, false, false),
139 (
140 r#"blush thistle blue yellow saddle"#,
141 r#"%yellow%"#,
142 false,
143 true,
144 ),
145 (r#"ABC_123"#, r#"ABC_123"#, false, true),
146 (r#"ABCD123"#, r#"ABC_123"#, false, true),
147 (r#"ABC_123"#, r"ABC\_123", false, true),
148 (r#"ABCD123"#, r"ABC\_123", false, false),
149 (r"ABC\123", r#"ABC_123"#, false, true),
150 (r"ABC\123", r"ABC\\123", false, true),
151 (r"ABC\123", r"ABC\123", false, false),
152 ("apple", r#"App%"#, true, true),
153 ("banana", r#"B%nana"#, true, true),
154 ("apple", r#"B%nana"#, true, false),
155 ("grape", "Gr_P_", true, true),
156 ];
157
158 #[test]
159 fn test_like() {
160 for (target, pattern, case_insensitive, expected) in CASES {
161 let output = if *case_insensitive {
162 i_like_default(target, pattern)
163 } else {
164 like_default(target, pattern)
165 };
166 assert_eq!(
167 output, *expected,
168 "target={}, pattern={}, case_insensitive={}",
169 target, pattern, case_insensitive
170 );
171 }
172 }
173
174 static ESCAPE_CASES: &[(&str, &str, &str, bool)] = &[
175 (r"bear", r"b_ear", r"_", true),
176 (r"be_r", r"b_e__r", r"_", true),
177 (r"be__r", r"b_e___r", r"_", true),
178 (r"be___r", r"b_e____r", r"_", true),
179 (r"be_r", r"__e__r", r"_", false),
180 (r"___r", r"____r", r"_", false),
182 ];
183
184 #[test]
185 fn test_escape_like() {
186 for (target, pattern, escape, expected) in ESCAPE_CASES {
187 let output = like(target, pattern, &EscapeChar::from_str(escape).unwrap());
188 assert_eq!(
189 output, *expected,
190 "target={}, pattern={}, escape={}",
191 target, pattern, escape
192 );
193 }
194 }
195}