uqa_sql/expr/scalar_helpers/
like_pattern.rs1use super::casing;
10use crate::{
11 error::{Result, SQLError},
12 expr::conversion::value_to_string_with_control,
13};
14use uqa_core::{
15 memory::{Produced, ProductionControl, ProductionVec},
16 Value,
17};
18
19pub struct CompiledLikePattern {
21 case_insensitive: bool,
22 pattern_chars: Produced<Vec<LikePatternToken<char>>>,
23 pattern_ascii: Option<Produced<Vec<LikePatternToken<u8>>>>,
24}
25
26#[derive(Clone, Copy, PartialEq, Eq)]
27enum LikePatternToken<T> {
28 Literal(T),
29 AnyOne,
30 AnyMany,
31 DanglingEscape,
32}
33
34impl CompiledLikePattern {
35 #[must_use]
36 pub fn new(pattern: &str, case_insensitive: bool) -> Self {
37 Self::with_escape(pattern, case_insensitive, None)
38 .expect("the default LIKE escape is exactly one character")
39 }
40
41 pub fn from_value(pattern: &Value, case_insensitive: bool) -> Result<Self> {
42 Ok(Self::new(
43 &crate::expr::value_to_string(pattern)?,
44 case_insensitive,
45 ))
46 }
47
48 pub fn with_escape(
49 pattern: &str,
50 case_insensitive: bool,
51 escape: Option<&str>,
52 ) -> Result<Self> {
53 Self::with_escape_with_control(
54 pattern,
55 case_insensitive,
56 escape,
57 &ProductionControl::uncontrolled(),
58 )
59 }
60
61 pub fn with_escape_with_control(
63 pattern: &str,
64 case_insensitive: bool,
65 escape: Option<&str>,
66 control: &ProductionControl<'_>,
67 ) -> Result<Self> {
68 control.check()?;
69 let escape = escape_character(escape)?;
70 let pattern_chars = compile(pattern, case_insensitive, escape, control)?;
71 let mut ascii = ProductionVec::new(*control);
72 let mut all_ascii = true;
73 for token in pattern_chars.iter() {
74 control.check()?;
75 let token = match token {
76 LikePatternToken::Literal(character) if character.is_ascii() => {
77 LikePatternToken::Literal(*character as u8)
78 }
79 LikePatternToken::Literal(_) => {
80 all_ascii = false;
81 break;
82 }
83 LikePatternToken::AnyOne => LikePatternToken::AnyOne,
84 LikePatternToken::AnyMany => LikePatternToken::AnyMany,
85 LikePatternToken::DanglingEscape => LikePatternToken::DanglingEscape,
86 };
87 ascii.push_copy(token)?;
88 }
89 let pattern_ascii = if all_ascii {
90 Some(ascii.finish()?)
91 } else {
92 None
93 };
94 Ok(Self {
95 case_insensitive,
96 pattern_chars,
97 pattern_ascii,
98 })
99 }
100
101 #[must_use]
102 pub fn is_match(&self, haystack: &str) -> bool {
103 self.try_is_match(haystack).unwrap_or(false)
104 }
105
106 pub fn try_is_match(&self, haystack: &str) -> Result<bool> {
107 self.try_is_match_with_control(haystack, &ProductionControl::uncontrolled())
108 }
109
110 pub fn try_is_match_with_control(
111 &self,
112 haystack: &str,
113 control: &ProductionControl<'_>,
114 ) -> Result<bool> {
115 control.check()?;
116 let normalized = if self.case_insensitive {
117 Some(casing::lowercase(haystack, control)?)
118 } else {
119 None
120 };
121 let haystack = normalized.as_deref().map_or(haystack, String::as_str);
122 if let Some(pattern) = self
123 .pattern_ascii
124 .as_deref()
125 .filter(|_| haystack.is_ascii())
126 {
127 return wildcard_match(haystack.as_bytes(), pattern, control);
128 }
129 let mut characters = ProductionVec::new(*control);
130 for character in haystack.chars() {
131 characters.push_copy(character)?;
132 }
133 wildcard_match(&characters, &self.pattern_chars, control)
134 }
135
136 #[must_use]
137 pub fn matches_value(&self, haystack: &Value) -> bool {
138 self.try_matches_value(haystack).unwrap_or(false)
139 }
140
141 pub fn try_matches_value(&self, haystack: &Value) -> Result<bool> {
142 self.try_matches_value_with_control(haystack, &ProductionControl::uncontrolled())
143 }
144
145 pub fn try_matches_value_with_control(
146 &self,
147 haystack: &Value,
148 control: &ProductionControl<'_>,
149 ) -> Result<bool> {
150 match haystack {
151 Value::Str(text) => self.try_is_match_with_control(text, control),
152 Value::FixedChar(text) => {
153 self.try_is_match_with_control(text.trim_end_matches(' '), control)
154 }
155 Value::Null => self.try_is_match_with_control("", control),
156 other => self
157 .try_is_match_with_control(&value_to_string_with_control(other, control)?, control),
158 }
159 }
160}
161
162pub(super) fn escape_character(escape: Option<&str>) -> Result<Option<char>> {
163 let Some(escape) = escape else {
164 return Ok(Some('\\'));
165 };
166 let mut characters = escape.chars();
167 let first = characters.next();
168 if characters.next().is_some() {
169 return Err(SQLError::Routine {
170 sqlstate: "22025".into(),
171 message: "invalid escape string".into(),
172 });
173 }
174 Ok(first)
175}
176
177fn compile(
178 pattern: &str,
179 insensitive: bool,
180 escape: Option<char>,
181 control: &ProductionControl<'_>,
182) -> Result<Produced<Vec<LikePatternToken<char>>>> {
183 let mut output = ProductionVec::new(*control);
184 let mut characters = pattern.chars();
185 while let Some(character) = characters.next() {
186 control.check()?;
187 if escape == Some(character) {
188 let Some(literal) = characters.next() else {
189 output.push_copy(LikePatternToken::DanglingEscape)?;
190 break;
191 };
192 push_literal(&mut output, literal, insensitive)?;
193 continue;
194 }
195 match character {
196 '%' => output.push_copy(LikePatternToken::AnyMany)?,
197 '_' => output.push_copy(LikePatternToken::AnyOne)?,
198 literal => push_literal(&mut output, literal, insensitive)?,
199 }
200 }
201 Ok(output.finish()?)
202}
203
204fn push_literal(
205 output: &mut ProductionVec<'_, LikePatternToken<char>>,
206 literal: char,
207 insensitive: bool,
208) -> Result<()> {
209 if insensitive {
210 for character in literal.to_lowercase() {
211 output.push_copy(LikePatternToken::Literal(character))?;
212 }
213 } else {
214 output.push_copy(LikePatternToken::Literal(literal))?;
215 }
216 Ok(())
217}
218
219fn wildcard_match<T: Copy + Eq>(
220 haystack: &[T],
221 pattern: &[LikePatternToken<T>],
222 control: &ProductionControl<'_>,
223) -> Result<bool> {
224 let mut haystack_index = 0;
225 let mut pattern_index = 0;
226 let mut star: Option<(usize, usize)> = None;
227 while haystack_index < haystack.len() {
228 control.check()?;
229 match pattern.get(pattern_index) {
230 Some(LikePatternToken::Literal(literal)) if *literal == haystack[haystack_index] => {
231 haystack_index += 1;
232 pattern_index += 1;
233 }
234 Some(LikePatternToken::AnyOne) => {
235 haystack_index += 1;
236 pattern_index += 1;
237 }
238 Some(LikePatternToken::AnyMany) => {
239 star = Some((pattern_index, haystack_index));
240 pattern_index += 1;
241 }
242 Some(LikePatternToken::DanglingEscape) => {
243 return Err(SQLError::Routine {
244 sqlstate: "22025".into(),
245 message: "LIKE pattern must not end with escape character".into(),
246 });
247 }
248 _ => {
249 if let Some((star_pattern, star_haystack)) = star {
250 pattern_index = star_pattern + 1;
251 haystack_index = star_haystack + 1;
252 star = Some((star_pattern, haystack_index));
253 } else {
254 return Ok(false);
255 }
256 }
257 }
258 }
259 while matches!(pattern.get(pattern_index), Some(LikePatternToken::AnyMany)) {
260 control.check()?;
261 pattern_index += 1;
262 }
263 Ok(pattern_index == pattern.len())
264}
265
266#[cfg(test)]
267mod tests;