EMMA Coverage Report (generated Sun Mar 01 22:06:14 CET 2015)
[all classes][org.h2.expression]

COVERAGE SUMMARY FOR SOURCE FILE [CompareLike.java]

nameclass, %method, %block, %line, %
CompareLike.java100% (1/1)100% (18/18)95%  (925/976)92%  (188.7/206)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class CompareLike100% (1/1)100% (18/18)95%  (925/976)92%  (188.7/206)
updateAggregate (Session): void 100% (1/1)75%  (12/16)80%  (4/5)
getEscapeChar (Value): Character 100% (1/1)76%  (28/37)82%  (9/11)
getEscapeChar (String): Character 100% (1/1)83%  (10/12)83%  (0.8/1)
test (String, String, char): boolean 100% (1/1)91%  (20/22)75%  (3/4)
createIndexConditions (Session, TableFilter): void 100% (1/1)92%  (174/189)89%  (34.9/39)
getValue (Session): Value 100% (1/1)95%  (76/80)89%  (17/19)
optimize (Session): Expression 100% (1/1)96%  (137/143)90%  (28/31)
initPattern (String, Character): void 100% (1/1)96%  (183/190)93%  (43/46)
compareAt (String, int, int, int, char [], int []): boolean 100% (1/1)97%  (68/70)93%  (14/15)
CompareLike (CompareMode, String, Expression, Expression, Expression, boolean... 100% (1/1)100% (21/21)100% (8/8)
CompareLike (Database, Expression, Expression, Expression, boolean): void 100% (1/1)100% (12/12)100% (2/2)
compare (char [], String, int, int): boolean 100% (1/1)100% (25/25)100% (1/1)
getCost (): int 100% (1/1)100% (10/10)100% (1/1)
getSQL (): String 100% (1/1)100% (61/61)100% (6/6)
isEverything (ExpressionVisitor): boolean 100% (1/1)100% (22/22)100% (1/1)
isFullMatch (): boolean 100% (1/1)100% (28/28)100% (6/6)
mapColumns (ColumnResolver, int): void 100% (1/1)100% (19/19)100% (5/5)
setEvaluatable (TableFilter, boolean): void 100% (1/1)100% (19/19)100% (5/5)

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 */
6package org.h2.expression;
7 
8import java.util.regex.Pattern;
9import java.util.regex.PatternSyntaxException;
10 
11import org.h2.api.ErrorCode;
12import org.h2.engine.Database;
13import org.h2.engine.Session;
14import org.h2.index.IndexCondition;
15import org.h2.message.DbException;
16import org.h2.table.ColumnResolver;
17import org.h2.table.TableFilter;
18import org.h2.value.CompareMode;
19import org.h2.value.Value;
20import org.h2.value.ValueBoolean;
21import org.h2.value.ValueNull;
22import org.h2.value.ValueString;
23 
24/**
25 * Pattern matching comparison expression: WHERE NAME LIKE ?
26 */
27public 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}

[all classes][org.h2.expression]
EMMA 2.0.5312 (C) Vladimir Roubtsov