Skip to main content

uqa_sql/expr/scalar_helpers/
like_pattern.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! LIKE compilation and wildcard matching share admitted and ordinary owners.
8
9use 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
19/// A LIKE/ILIKE pattern compiled once for repeated evaluation. ASCII values use a byte matcher; Unicode retains character-oriented `_` semantics and contextual lowercase rules.
20pub struct CompiledLikePattern {
21    case_insensitive: bool,
22    binary: bool,
23    pattern_chars: Produced<Vec<LikePatternToken<char>>>,
24    pattern_ascii: Option<Produced<Vec<LikePatternToken<u8>>>>,
25}
26
27#[derive(Clone, Copy, PartialEq, Eq)]
28enum LikePatternToken<T> {
29    Literal(T),
30    AnyOne,
31    AnyMany,
32    DanglingEscape,
33}
34
35impl CompiledLikePattern {
36    #[must_use]
37    pub fn new(pattern: &str, case_insensitive: bool) -> Self {
38        Self::with_escape(pattern, case_insensitive, None)
39            .expect("the default LIKE escape is exactly one character")
40    }
41
42    pub fn from_value(pattern: &Value, case_insensitive: bool) -> Result<Self> {
43        Self::from_value_with_escape_with_control(
44            pattern,
45            case_insensitive,
46            None,
47            &ProductionControl::uncontrolled(),
48        )
49    }
50
51    /// Compile the selected text or bytea pattern without converting binary data to display text.
52    pub fn from_value_with_escape_with_control(
53        pattern: &Value,
54        case_insensitive: bool,
55        escape: Option<&Value>,
56        control: &ProductionControl<'_>,
57    ) -> Result<Self> {
58        if let Some(pattern) = crate::expr::datums::binary_payload(pattern, control)? {
59            if case_insensitive {
60                return Err(SQLError::Internal("bytea has no ILIKE operator".into()));
61            }
62            let escape = escape
63                .map(|value| crate::expr::datums::binary_payload(value, control))
64                .transpose()?;
65            let escape = match escape.as_ref() {
66                None => Some(b'\\'),
67                Some(Some(bytes)) => match bytes.bytes() {
68                    [] => None,
69                    [byte] => Some(*byte),
70                    _ => return Err(invalid_escape()),
71                },
72                Some(None) => {
73                    return Err(SQLError::Internal(
74                        "bytea escape has an invalid carrier".into(),
75                    ))
76                }
77            };
78            let mut tokens = ProductionVec::new(*control);
79            let mut bytes = pattern.bytes().iter().copied();
80            while let Some(byte) = bytes.next() {
81                control.check()?;
82                let token = if escape == Some(byte) {
83                    bytes
84                        .next()
85                        .map_or(LikePatternToken::DanglingEscape, LikePatternToken::Literal)
86                } else {
87                    match byte {
88                        b'%' => LikePatternToken::AnyMany,
89                        b'_' => LikePatternToken::AnyOne,
90                        other => LikePatternToken::Literal(other),
91                    }
92                };
93                tokens.push_copy(token)?;
94            }
95            return Ok(Self {
96                case_insensitive,
97                binary: true,
98                pattern_chars: ProductionVec::new(*control).finish()?,
99                pattern_ascii: Some(tokens.finish()?),
100            });
101        }
102        let pattern = value_to_string_with_control(pattern, control)?;
103        let escape = escape
104            .map(|value| value_to_string_with_control(value, control))
105            .transpose()?;
106        Self::with_escape_with_control(
107            &pattern,
108            case_insensitive,
109            escape.as_deref().map(String::as_str),
110            control,
111        )
112    }
113
114    pub fn with_escape(
115        pattern: &str,
116        case_insensitive: bool,
117        escape: Option<&str>,
118    ) -> Result<Self> {
119        Self::with_escape_with_control(
120            pattern,
121            case_insensitive,
122            escape,
123            &ProductionControl::uncontrolled(),
124        )
125    }
126
127    /// Compile while keeping each native token buffer under the supplied owner.
128    pub fn with_escape_with_control(
129        pattern: &str,
130        case_insensitive: bool,
131        escape: Option<&str>,
132        control: &ProductionControl<'_>,
133    ) -> Result<Self> {
134        control.check()?;
135        let escape = escape_character(escape)?;
136        let pattern_chars = compile(pattern, case_insensitive, escape, control)?;
137        let mut ascii = ProductionVec::new(*control);
138        let mut all_ascii = true;
139        for token in pattern_chars.iter() {
140            control.check()?;
141            let token = match token {
142                LikePatternToken::Literal(character) if character.is_ascii() => {
143                    LikePatternToken::Literal(*character as u8)
144                }
145                LikePatternToken::Literal(_) => {
146                    all_ascii = false;
147                    break;
148                }
149                LikePatternToken::AnyOne => LikePatternToken::AnyOne,
150                LikePatternToken::AnyMany => LikePatternToken::AnyMany,
151                LikePatternToken::DanglingEscape => LikePatternToken::DanglingEscape,
152            };
153            ascii.push_copy(token)?;
154        }
155        let pattern_ascii = if all_ascii {
156            Some(ascii.finish()?)
157        } else {
158            None
159        };
160        Ok(Self {
161            case_insensitive,
162            binary: false,
163            pattern_chars,
164            pattern_ascii,
165        })
166    }
167
168    #[must_use]
169    pub fn is_match(&self, haystack: &str) -> bool {
170        self.try_is_match(haystack).unwrap_or(false)
171    }
172
173    pub fn try_is_match(&self, haystack: &str) -> Result<bool> {
174        self.try_is_match_with_control(haystack, &ProductionControl::uncontrolled())
175    }
176
177    pub fn try_is_match_with_control(
178        &self,
179        haystack: &str,
180        control: &ProductionControl<'_>,
181    ) -> Result<bool> {
182        control.check()?;
183        let normalized = if self.case_insensitive {
184            Some(casing::lowercase(haystack, control)?)
185        } else {
186            None
187        };
188        let haystack = normalized.as_deref().map_or(haystack, String::as_str);
189        if let Some(pattern) = self
190            .pattern_ascii
191            .as_deref()
192            .filter(|_| self.binary || haystack.is_ascii())
193        {
194            return wildcard_match(haystack.as_bytes(), pattern, control);
195        }
196        let mut characters = ProductionVec::new(*control);
197        for character in haystack.chars() {
198            characters.push_copy(character)?;
199        }
200        wildcard_match(&characters, &self.pattern_chars, control)
201    }
202
203    #[must_use]
204    pub fn matches_value(&self, haystack: &Value) -> bool {
205        self.try_matches_value(haystack).unwrap_or(false)
206    }
207
208    pub fn try_matches_value(&self, haystack: &Value) -> Result<bool> {
209        self.try_matches_value_with_control(haystack, &ProductionControl::uncontrolled())
210    }
211
212    pub fn try_matches_value_with_control(
213        &self,
214        haystack: &Value,
215        control: &ProductionControl<'_>,
216    ) -> Result<bool> {
217        if self.binary {
218            let bytes =
219                crate::expr::datums::binary_payload(haystack, control)?.ok_or_else(|| {
220                    SQLError::Internal("bytea LIKE source has an invalid carrier".into())
221                })?;
222            return wildcard_match(
223                bytes.bytes(),
224                self.pattern_ascii
225                    .as_deref()
226                    .expect("binary pattern tokens"),
227                control,
228            );
229        }
230        match haystack {
231            Value::Datum(datum) => self.try_matches_value_with_control(
232                &*crate::expr::datums::read_with_control(datum, control)?,
233                control,
234            ),
235            Value::Str(text) | Value::FixedChar(text) => {
236                self.try_is_match_with_control(text, control)
237            }
238            Value::Null => self.try_is_match_with_control("", control),
239            other => self
240                .try_is_match_with_control(&value_to_string_with_control(other, control)?, control),
241        }
242    }
243}
244
245pub(in crate::expr) fn escape_character(escape: Option<&str>) -> Result<Option<char>> {
246    let Some(escape) = escape else {
247        return Ok(Some('\\'));
248    };
249    let mut characters = escape.chars();
250    let first = characters.next();
251    if characters.next().is_some() {
252        return Err(invalid_escape());
253    }
254    Ok(first)
255}
256
257pub(in crate::expr) fn invalid_escape() -> SQLError {
258    SQLError::Diagnostic {
259        sqlstate: "22025".into(),
260        message: "invalid escape string".into(),
261        detail: None,
262        hint: Some("Escape string must be empty or one character.".into()),
263    }
264}
265
266fn compile(
267    pattern: &str,
268    insensitive: bool,
269    escape: Option<char>,
270    control: &ProductionControl<'_>,
271) -> Result<Produced<Vec<LikePatternToken<char>>>> {
272    let mut output = ProductionVec::new(*control);
273    let mut characters = pattern.chars();
274    while let Some(character) = characters.next() {
275        control.check()?;
276        if escape == Some(character) {
277            let Some(literal) = characters.next() else {
278                output.push_copy(LikePatternToken::DanglingEscape)?;
279                break;
280            };
281            push_literal(&mut output, literal, insensitive)?;
282            continue;
283        }
284        match character {
285            '%' => output.push_copy(LikePatternToken::AnyMany)?,
286            '_' => output.push_copy(LikePatternToken::AnyOne)?,
287            literal => push_literal(&mut output, literal, insensitive)?,
288        }
289    }
290    Ok(output.finish()?)
291}
292
293fn push_literal(
294    output: &mut ProductionVec<'_, LikePatternToken<char>>,
295    literal: char,
296    insensitive: bool,
297) -> Result<()> {
298    if insensitive {
299        for character in literal.to_lowercase() {
300            output.push_copy(LikePatternToken::Literal(character))?;
301        }
302    } else {
303        output.push_copy(LikePatternToken::Literal(literal))?;
304    }
305    Ok(())
306}
307
308fn wildcard_match<T: Copy + Eq>(
309    haystack: &[T],
310    pattern: &[LikePatternToken<T>],
311    control: &ProductionControl<'_>,
312) -> Result<bool> {
313    let mut haystack_index = 0;
314    let mut pattern_index = 0;
315    let mut star: Option<(usize, usize)> = None;
316    while haystack_index < haystack.len() {
317        control.check()?;
318        match pattern.get(pattern_index) {
319            Some(LikePatternToken::Literal(literal)) if *literal == haystack[haystack_index] => {
320                haystack_index += 1;
321                pattern_index += 1;
322            }
323            Some(LikePatternToken::AnyOne) => {
324                haystack_index += 1;
325                pattern_index += 1;
326            }
327            Some(LikePatternToken::AnyMany) => {
328                star = Some((pattern_index, haystack_index));
329                pattern_index += 1;
330            }
331            Some(LikePatternToken::DanglingEscape) => {
332                return Err(SQLError::Routine {
333                    sqlstate: "22025".into(),
334                    message: "LIKE pattern must not end with escape character".into(),
335                });
336            }
337            _ => {
338                if let Some((star_pattern, star_haystack)) = star {
339                    pattern_index = star_pattern + 1;
340                    haystack_index = star_haystack + 1;
341                    star = Some((star_pattern, haystack_index));
342                } else {
343                    return Ok(false);
344                }
345            }
346        }
347    }
348    while matches!(pattern.get(pattern_index), Some(LikePatternToken::AnyMany)) {
349        control.check()?;
350        pattern_index += 1;
351    }
352    Ok(pattern_index == pattern.len())
353}
354
355#[cfg(test)]
356mod tests;