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    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    /// Compile while keeping each native token buffer under the supplied owner.
62    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;