MCPcopy Create free account
hub / github.com/MaterializeInc/materialize / compile

Function compile

src/expr/src/scalar/like_pattern.rs:140–184  ·  view source on GitHub ↗

Builds a Matcher that matches a SQL LIKE pattern.

(pattern: &str, case_insensitive: bool)

Source from the content-addressed store, hash-verified

138
139/// Builds a Matcher that matches a SQL LIKE pattern.
140pub 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:

Callers 13

bench_ilikeFunction · 0.85
bench_likeFunction · 0.85
test_likeFunction · 0.85
to_char_timestamp_formatFunction · 0.85
test_likeFunction · 0.85
precompile_to_charFunction · 0.85
precompile_is_likeFunction · 0.85

Calls 9

build_subpatternsFunction · 0.85
StringClass · 0.85
build_regexFunction · 0.70
RegexClass · 0.50
lenMethod · 0.45
countMethod · 0.45
filterMethod · 0.45
iterMethod · 0.45
is_emptyMethod · 0.45

Tested by 5

test_likeFunction · 0.68
test_likeFunction · 0.68
test_could_errorFunction · 0.68