1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.expression; |
7 | |
8 | import java.util.regex.Pattern; |
9 | import java.util.regex.PatternSyntaxException; |
10 | |
11 | import org.h2.api.ErrorCode; |
12 | import org.h2.engine.Database; |
13 | import org.h2.engine.Session; |
14 | import org.h2.index.IndexCondition; |
15 | import org.h2.message.DbException; |
16 | import org.h2.table.ColumnResolver; |
17 | import org.h2.table.TableFilter; |
18 | import org.h2.value.CompareMode; |
19 | import org.h2.value.Value; |
20 | import org.h2.value.ValueBoolean; |
21 | import org.h2.value.ValueNull; |
22 | import org.h2.value.ValueString; |
23 | |
24 | /** |
25 | * Pattern matching comparison expression: WHERE NAME LIKE ? |
26 | */ |
27 | public class CompareLike extends Condition { |
28 | |
29 | private static final int MATCH = 0, ONE = 1, ANY = 2; |
30 | |
31 | private final CompareMode compareMode; |
32 | private final String defaultEscape; |
33 | private Expression left; |
34 | private Expression right; |
35 | private Expression escape; |
36 | |
37 | private boolean isInit; |
38 | |
39 | private char[] patternChars; |
40 | private String patternString; |
41 | private int[] patternTypes; |
42 | private int patternLength; |
43 | |
44 | private final boolean regexp; |
45 | private Pattern patternRegexp; |
46 | |
47 | private boolean ignoreCase; |
48 | private boolean fastCompare; |
49 | private boolean invalidPattern; |
50 | |
51 | public CompareLike(Database db, Expression left, Expression right, |
52 | Expression escape, boolean regexp) { |
53 | this(db.getCompareMode(), db.getSettings().defaultEscape, left, right, |
54 | escape, regexp); |
55 | } |
56 | |
57 | public CompareLike(CompareMode compareMode, String defaultEscape, |
58 | Expression left, Expression right, Expression escape, boolean regexp) { |
59 | this.compareMode = compareMode; |
60 | this.defaultEscape = defaultEscape; |
61 | this.regexp = regexp; |
62 | this.left = left; |
63 | this.right = right; |
64 | this.escape = escape; |
65 | } |
66 | |
67 | private static Character getEscapeChar(String s) { |
68 | return s == null || s.length() == 0 ? null : s.charAt(0); |
69 | } |
70 | |
71 | @Override |
72 | public String getSQL() { |
73 | String sql; |
74 | if (regexp) { |
75 | sql = left.getSQL() + " REGEXP " + right.getSQL(); |
76 | } else { |
77 | sql = left.getSQL() + " LIKE " + right.getSQL(); |
78 | if (escape != null) { |
79 | sql += " ESCAPE " + escape.getSQL(); |
80 | } |
81 | } |
82 | return "(" + sql + ")"; |
83 | } |
84 | |
85 | @Override |
86 | public Expression optimize(Session session) { |
87 | left = left.optimize(session); |
88 | right = right.optimize(session); |
89 | if (left.getType() == Value.STRING_IGNORECASE) { |
90 | ignoreCase = true; |
91 | } |
92 | if (left.isValueSet()) { |
93 | Value l = left.getValue(session); |
94 | if (l == ValueNull.INSTANCE) { |
95 | // NULL LIKE something > NULL |
96 | return ValueExpression.getNull(); |
97 | } |
98 | } |
99 | if (escape != null) { |
100 | escape = escape.optimize(session); |
101 | } |
102 | if (right.isValueSet() && (escape == null || escape.isValueSet())) { |
103 | if (left.isValueSet()) { |
104 | return ValueExpression.get(getValue(session)); |
105 | } |
106 | Value r = right.getValue(session); |
107 | if (r == ValueNull.INSTANCE) { |
108 | // something LIKE NULL > NULL |
109 | return ValueExpression.getNull(); |
110 | } |
111 | Value e = escape == null ? null : escape.getValue(session); |
112 | if (e == ValueNull.INSTANCE) { |
113 | return ValueExpression.getNull(); |
114 | } |
115 | String p = r.getString(); |
116 | initPattern(p, getEscapeChar(e)); |
117 | if (invalidPattern) { |
118 | return ValueExpression.getNull(); |
119 | } |
120 | if ("%".equals(p)) { |
121 | // optimization for X LIKE '%': convert to X IS NOT NULL |
122 | return new Comparison(session, |
123 | Comparison.IS_NOT_NULL, left, null).optimize(session); |
124 | } |
125 | if (isFullMatch()) { |
126 | // optimization for X LIKE 'Hello': convert to X = 'Hello' |
127 | Value value = ValueString.get(patternString); |
128 | Expression expr = ValueExpression.get(value); |
129 | return new Comparison(session, |
130 | Comparison.EQUAL, left, expr).optimize(session); |
131 | } |
132 | isInit = true; |
133 | } |
134 | return this; |
135 | } |
136 | |
137 | private Character getEscapeChar(Value e) { |
138 | if (e == null) { |
139 | return getEscapeChar(defaultEscape); |
140 | } |
141 | String es = e.getString(); |
142 | Character esc; |
143 | if (es == null) { |
144 | esc = getEscapeChar(defaultEscape); |
145 | } else if (es.length() == 0) { |
146 | esc = null; |
147 | } else if (es.length() > 1) { |
148 | throw DbException.get(ErrorCode.LIKE_ESCAPE_ERROR_1, es); |
149 | } else { |
150 | esc = es.charAt(0); |
151 | } |
152 | return esc; |
153 | } |
154 | |
155 | @Override |
156 | public void createIndexConditions(Session session, TableFilter filter) { |
157 | if (regexp) { |
158 | return; |
159 | } |
160 | if (!(left instanceof ExpressionColumn)) { |
161 | return; |
162 | } |
163 | ExpressionColumn l = (ExpressionColumn) left; |
164 | if (filter != l.getTableFilter()) { |
165 | return; |
166 | } |
167 | // parameters are always evaluatable, but |
168 | // we need to check if the value is set |
169 | // (at prepare time) |
170 | // otherwise we would need to prepare at execute time, |
171 | // which may be slower (possibly not in this case) |
172 | if (!right.isEverything(ExpressionVisitor.INDEPENDENT_VISITOR)) { |
173 | return; |
174 | } |
175 | if (escape != null && |
176 | !escape.isEverything(ExpressionVisitor.INDEPENDENT_VISITOR)) { |
177 | return; |
178 | } |
179 | String p = right.getValue(session).getString(); |
180 | Value e = escape == null ? null : escape.getValue(session); |
181 | if (e == ValueNull.INSTANCE) { |
182 | // should already be optimized |
183 | DbException.throwInternalError(); |
184 | } |
185 | initPattern(p, getEscapeChar(e)); |
186 | if (invalidPattern) { |
187 | return; |
188 | } |
189 | if (patternLength <= 0 || patternTypes[0] != MATCH) { |
190 | // can't use an index |
191 | return; |
192 | } |
193 | int dataType = l.getColumn().getType(); |
194 | if (dataType != Value.STRING && dataType != Value.STRING_IGNORECASE && |
195 | dataType != Value.STRING_FIXED) { |
196 | // column is not a varchar - can't use the index |
197 | return; |
198 | } |
199 | int maxMatch = 0; |
200 | StringBuilder buff = new StringBuilder(); |
201 | while (maxMatch < patternLength && patternTypes[maxMatch] == MATCH) { |
202 | buff.append(patternChars[maxMatch++]); |
203 | } |
204 | String begin = buff.toString(); |
205 | if (maxMatch == patternLength) { |
206 | filter.addIndexCondition(IndexCondition.get(Comparison.EQUAL, l, |
207 | ValueExpression.get(ValueString.get(begin)))); |
208 | } else { |
209 | // TODO check if this is correct according to Unicode rules |
210 | // (code points) |
211 | String end; |
212 | if (begin.length() > 0) { |
213 | filter.addIndexCondition(IndexCondition.get( |
214 | Comparison.BIGGER_EQUAL, l, |
215 | ValueExpression.get(ValueString.get(begin)))); |
216 | char next = begin.charAt(begin.length() - 1); |
217 | // search the 'next' unicode character (or at least a character |
218 | // that is higher) |
219 | for (int i = 1; i < 2000; i++) { |
220 | end = begin.substring(0, begin.length() - 1) + (char) (next + i); |
221 | if (compareMode.compareString(begin, end, ignoreCase) == -1) { |
222 | filter.addIndexCondition(IndexCondition.get( |
223 | Comparison.SMALLER, l, |
224 | ValueExpression.get(ValueString.get(end)))); |
225 | break; |
226 | } |
227 | } |
228 | } |
229 | } |
230 | } |
231 | |
232 | @Override |
233 | public Value getValue(Session session) { |
234 | Value l = left.getValue(session); |
235 | if (l == ValueNull.INSTANCE) { |
236 | return l; |
237 | } |
238 | if (!isInit) { |
239 | Value r = right.getValue(session); |
240 | if (r == ValueNull.INSTANCE) { |
241 | return r; |
242 | } |
243 | String p = r.getString(); |
244 | Value e = escape == null ? null : escape.getValue(session); |
245 | if (e == ValueNull.INSTANCE) { |
246 | return ValueNull.INSTANCE; |
247 | } |
248 | initPattern(p, getEscapeChar(e)); |
249 | } |
250 | if (invalidPattern) { |
251 | return ValueNull.INSTANCE; |
252 | } |
253 | String value = l.getString(); |
254 | boolean result; |
255 | if (regexp) { |
256 | // result = patternRegexp.matcher(value).matches(); |
257 | result = patternRegexp.matcher(value).find(); |
258 | } else { |
259 | result = compareAt(value, 0, 0, value.length(), patternChars, patternTypes); |
260 | } |
261 | return ValueBoolean.get(result); |
262 | } |
263 | |
264 | private boolean compare(char[] pattern, String s, int pi, int si) { |
265 | return pattern[pi] == s.charAt(si) || |
266 | (!fastCompare && compareMode.equalsChars(patternString, pi, s, |
267 | si, ignoreCase)); |
268 | } |
269 | |
270 | private boolean compareAt(String s, int pi, int si, int sLen, |
271 | char[] pattern, int[] types) { |
272 | for (; pi < patternLength; pi++) { |
273 | switch (types[pi]) { |
274 | case MATCH: |
275 | if ((si >= sLen) || !compare(pattern, s, pi, si++)) { |
276 | return false; |
277 | } |
278 | break; |
279 | case ONE: |
280 | if (si++ >= sLen) { |
281 | return false; |
282 | } |
283 | break; |
284 | case ANY: |
285 | if (++pi >= patternLength) { |
286 | return true; |
287 | } |
288 | while (si < sLen) { |
289 | if (compare(pattern, s, pi, si) && |
290 | compareAt(s, pi, si, sLen, pattern, types)) { |
291 | return true; |
292 | } |
293 | si++; |
294 | } |
295 | return false; |
296 | default: |
297 | DbException.throwInternalError(); |
298 | } |
299 | } |
300 | return si == sLen; |
301 | } |
302 | |
303 | /** |
304 | * Test if the value matches the pattern. |
305 | * |
306 | * @param testPattern the pattern |
307 | * @param value the value |
308 | * @param escapeChar the escape character |
309 | * @return true if the value matches |
310 | */ |
311 | public boolean test(String testPattern, String value, char escapeChar) { |
312 | initPattern(testPattern, escapeChar); |
313 | if (invalidPattern) { |
314 | return false; |
315 | } |
316 | return compareAt(value, 0, 0, value.length(), patternChars, patternTypes); |
317 | } |
318 | |
319 | private void initPattern(String p, Character escapeChar) { |
320 | if (compareMode.getName().equals(CompareMode.OFF) && !ignoreCase) { |
321 | fastCompare = true; |
322 | } |
323 | if (regexp) { |
324 | patternString = p; |
325 | try { |
326 | if (ignoreCase) { |
327 | patternRegexp = Pattern.compile(p, Pattern.CASE_INSENSITIVE); |
328 | } else { |
329 | patternRegexp = Pattern.compile(p); |
330 | } |
331 | } catch (PatternSyntaxException e) { |
332 | throw DbException.get(ErrorCode.LIKE_ESCAPE_ERROR_1, e, p); |
333 | } |
334 | return; |
335 | } |
336 | patternLength = 0; |
337 | if (p == null) { |
338 | patternTypes = null; |
339 | patternChars = null; |
340 | return; |
341 | } |
342 | int len = p.length(); |
343 | patternChars = new char[len]; |
344 | patternTypes = new int[len]; |
345 | boolean lastAny = false; |
346 | for (int i = 0; i < len; i++) { |
347 | char c = p.charAt(i); |
348 | int type; |
349 | if (escapeChar != null && escapeChar == c) { |
350 | if (i >= len - 1) { |
351 | invalidPattern = true; |
352 | return; |
353 | } |
354 | c = p.charAt(++i); |
355 | type = MATCH; |
356 | lastAny = false; |
357 | } else if (c == '%') { |
358 | if (lastAny) { |
359 | continue; |
360 | } |
361 | type = ANY; |
362 | lastAny = true; |
363 | } else if (c == '_') { |
364 | type = ONE; |
365 | } else { |
366 | type = MATCH; |
367 | lastAny = false; |
368 | } |
369 | patternTypes[patternLength] = type; |
370 | patternChars[patternLength++] = c; |
371 | } |
372 | for (int i = 0; i < patternLength - 1; i++) { |
373 | if ((patternTypes[i] == ANY) && (patternTypes[i + 1] == ONE)) { |
374 | patternTypes[i] = ONE; |
375 | patternTypes[i + 1] = ANY; |
376 | } |
377 | } |
378 | patternString = new String(patternChars, 0, patternLength); |
379 | } |
380 | |
381 | private boolean isFullMatch() { |
382 | if (patternTypes == null) { |
383 | return false; |
384 | } |
385 | for (int type : patternTypes) { |
386 | if (type != MATCH) { |
387 | return false; |
388 | } |
389 | } |
390 | return true; |
391 | } |
392 | |
393 | @Override |
394 | public void mapColumns(ColumnResolver resolver, int level) { |
395 | left.mapColumns(resolver, level); |
396 | right.mapColumns(resolver, level); |
397 | if (escape != null) { |
398 | escape.mapColumns(resolver, level); |
399 | } |
400 | } |
401 | |
402 | @Override |
403 | public void setEvaluatable(TableFilter tableFilter, boolean b) { |
404 | left.setEvaluatable(tableFilter, b); |
405 | right.setEvaluatable(tableFilter, b); |
406 | if (escape != null) { |
407 | escape.setEvaluatable(tableFilter, b); |
408 | } |
409 | } |
410 | |
411 | @Override |
412 | public void updateAggregate(Session session) { |
413 | left.updateAggregate(session); |
414 | right.updateAggregate(session); |
415 | if (escape != null) { |
416 | escape.updateAggregate(session); |
417 | } |
418 | } |
419 | |
420 | @Override |
421 | public boolean isEverything(ExpressionVisitor visitor) { |
422 | return left.isEverything(visitor) && right.isEverything(visitor) |
423 | && (escape == null || escape.isEverything(visitor)); |
424 | } |
425 | |
426 | @Override |
427 | public int getCost() { |
428 | return left.getCost() + right.getCost() + 3; |
429 | } |
430 | |
431 | } |