Builds a Matcher that matches a SQL LIKE pattern.
(pattern: &str, case_insensitive: bool)
| 138 | |
| 139 | /// Builds a Matcher that matches a SQL LIKE pattern. |
| 140 | pub fn compile(pattern: &str, case_insensitive: bool) -> Result<Matcher, EvalError> { |
| 141 | // We would like to have a consistent, documented limit to the size of |
| 142 | // supported LIKE patterns. The real limiting factor is the number of states |
| 143 | // that can be handled by the Regex library. In testing, I was able to |
| 144 | // create an adversarial pattern "%a%b%c%d%e..." that started failing around |
| 145 | // 9 KiB, so we chose 8 KiB as the limit. This is consistent with limits |
| 146 | // set by other databases, like SQL Server. |
| 147 | // On the other hand, PostgreSQL does not have a documented limit. |
| 148 | if pattern.len() > 8 << 10 { |
| 149 | return Err(EvalError::LikePatternTooLong); |
| 150 | } |
| 151 | let subpatterns = build_subpatterns(pattern)?; |
| 152 | // `is_match_subpatterns` resolves each `%` (a `many` subpattern) by searching |
| 153 | // for the following suffix and backtracking over every candidate position. A |
| 154 | // single `%` is near-linear, but with two or more the backtracking nests and |
| 155 | // the cost becomes super-linear in the text length — an adversarial pattern |
| 156 | // like `%a%a%a` against a long run of `a`s takes time proportional to |
| 157 | // `len(text)^(number of %)`, which can stall a worker for many minutes. The |
| 158 | // regex engine matches the same patterns in linear time with no backtracking, |
| 159 | // so fall back to it whenever more than one backtracking `%` is present (and |
| 160 | // for the existing case-insensitive / too-many-subpatterns reasons). |
| 161 | // |
| 162 | // A `%` with an *empty* suffix short-circuits in `is_match_subpatterns` |
| 163 | // without any `rfind`/backtracking, and (per `build_subpatterns`) such a `%` |
| 164 | // can only ever be the trailing subpattern. Only `%` subpatterns with a |
| 165 | // non-empty suffix nest the backtracking loops, so we count just those. This |
| 166 | // keeps the common "contains" pattern `%foo%` — which decomposes into one |
| 167 | // non-empty-suffix `%` plus a trailing empty-suffix `%` — on the fast string |
| 168 | // matcher, where a single `rfind` is far cheaper than a forward regex scan. |
| 169 | let backtracking_manys = subpatterns |
| 170 | .iter() |
| 171 | .filter(|s| s.many && !s.suffix.is_empty()) |
| 172 | .count(); |
| 173 | let use_regex = |
| 174 | case_insensitive || subpatterns.len() > MAX_SUBPATTERNS || backtracking_manys > 1; |
| 175 | let matcher_impl = match use_regex { |
| 176 | false => MatcherImpl::String(subpatterns), |
| 177 | true => MatcherImpl::Regex(build_regex(&subpatterns, case_insensitive)?), |
| 178 | }; |
| 179 | Ok(Matcher { |
| 180 | pattern: pattern.into(), |
| 181 | case_insensitive, |
| 182 | matcher_impl, |
| 183 | }) |
| 184 | } |
| 185 | |
| 186 | // The algorithm below is based on the observation that any LIKE pattern can be |
| 187 | // decomposed into multiple parts: |