001: /*
002: * Copyright 2004-2008 H2 Group. Licensed under the H2 License, Version 1.0
003: * (http://h2database.com/html/license.html).
004: * Initial Developer: H2 Group
005: */
006: package org.h2.expression;
007:
008: import java.sql.SQLException;
009: import java.util.regex.Pattern;
010: import java.util.regex.PatternSyntaxException;
011:
012: import org.h2.constant.ErrorCode;
013: import org.h2.engine.Constants;
014: import org.h2.engine.Session;
015: import org.h2.index.IndexCondition;
016: import org.h2.message.Message;
017: import org.h2.table.ColumnResolver;
018: import org.h2.table.TableFilter;
019: import org.h2.util.StringUtils;
020: import org.h2.value.CompareMode;
021: import org.h2.value.Value;
022: import org.h2.value.ValueBoolean;
023: import org.h2.value.ValueNull;
024: import org.h2.value.ValueString;
025:
026: /**
027: * Pattern matching comparison expression: WHERE NAME LIKE ?
028: */
029: public class CompareLike extends Condition {
030:
031: private final CompareMode compareMode;
032: private final boolean regexp;
033: private Expression left;
034: private Expression right;
035: private Expression escape;
036:
037: private boolean isInit;
038: private char[] pattern;
039: private String patternString;
040: private Pattern patternRegexp;
041: private int[] types;
042: private int patternLength;
043: private static final int MATCH = 0, ONE = 1, ANY = 2;
044: private boolean ignoreCase;
045:
046: public CompareLike(CompareMode compareMode, Expression left,
047: Expression right, Expression escape, boolean regexp) {
048: this .compareMode = compareMode;
049: this .regexp = regexp;
050: this .left = left;
051: this .right = right;
052: this .escape = escape;
053: }
054:
055: public String getSQL() {
056: String sql;
057: if (regexp) {
058: sql = left.getSQL() + " REGEXP " + right.getSQL();
059: } else {
060: sql = left.getSQL() + " LIKE " + right.getSQL();
061: if (escape != null) {
062: sql += " ESCAPE " + escape.getSQL();
063: }
064: }
065: return "(" + sql + ")";
066: }
067:
068: public Expression optimize(Session session) throws SQLException {
069: left = left.optimize(session);
070: right = right.optimize(session);
071: if (left.getType() == Value.STRING_IGNORECASE) {
072: ignoreCase = true;
073: }
074: if (left.isValueSet()) {
075: Value l = left.getValue(session);
076: if (l == ValueNull.INSTANCE) {
077: // NULL LIKE something > NULL
078: return ValueExpression.NULL;
079: }
080: }
081: if (escape != null) {
082: escape = escape.optimize(session);
083: }
084: if (right.isValueSet()
085: && (escape == null || escape.isValueSet())) {
086: if (left.isValueSet()) {
087: return ValueExpression.get(getValue(session));
088: }
089: Value r = right.getValue(session);
090: if (r == ValueNull.INSTANCE) {
091: // something LIKE NULL > NULL
092: return ValueExpression.NULL;
093: }
094: Value e = escape == null ? null : escape.getValue(session);
095: if (e == ValueNull.INSTANCE) {
096: return ValueExpression.NULL;
097: }
098: String pattern = r.getString();
099: initPattern(pattern, getEscapeChar(e));
100: if ("%".equals(pattern)) {
101: // optimization for X LIKE '%': convert to X IS NOT NULL
102: return new Comparison(session, Comparison.IS_NOT_NULL,
103: left, null).optimize(session);
104: }
105: if (isFullMatch()) {
106: // optimization for X LIKE 'Hello': convert to X = 'Hello'
107: Value value = ValueString.get(patternString);
108: Expression expr = ValueExpression.get(value);
109: return new Comparison(session, Comparison.EQUAL, left,
110: expr).optimize(session);
111: }
112: isInit = true;
113: }
114: return this ;
115: }
116:
117: private char getEscapeChar(Value e) throws SQLException {
118: if (e == null) {
119: return Constants.DEFAULT_ESCAPE_CHAR;
120: }
121: String es = e.getString();
122: char esc;
123: if (es == null || es.length() == 0) {
124: esc = Constants.DEFAULT_ESCAPE_CHAR;
125: } else {
126: esc = es.charAt(0);
127: }
128: return esc;
129: }
130:
131: public void createIndexConditions(Session session,
132: TableFilter filter) throws SQLException {
133: if (regexp) {
134: return;
135: }
136: if (!(left instanceof ExpressionColumn)) {
137: return;
138: }
139: ExpressionColumn l = (ExpressionColumn) left;
140: if (filter != l.getTableFilter()) {
141: return;
142: }
143: // parameters are always evaluatable, but
144: // we need to check the actual value now
145: // (at prepare time)
146: // otherwise we would need to prepare at execute time,
147: // which is maybe slower (but maybe not in this case!)
148: // TODO optimizer: like: check what other databases do
149: if (!right.isValueSet()) {
150: return;
151: }
152: if (escape != null && !escape.isValueSet()) {
153: return;
154: }
155: String p = right.getValue(session).getString();
156: Value e = escape == null ? null : escape.getValue(session);
157: if (e == ValueNull.INSTANCE) {
158: // should already be optimized
159: throw Message.getInternalError();
160: }
161: initPattern(p, getEscapeChar(e));
162: if (patternLength <= 0 || types[0] != MATCH) {
163: // can't use an index
164: return;
165: }
166: int dataType = l.getColumn().getType();
167: if (dataType != Value.STRING
168: && dataType != Value.STRING_IGNORECASE
169: && dataType != Value.STRING_FIXED) {
170: // column is not a varchar - can't use the index
171: return;
172: }
173: int maxMatch = 0;
174: StringBuffer buff = new StringBuffer();
175: while (maxMatch < patternLength && types[maxMatch] == MATCH) {
176: buff.append(pattern[maxMatch++]);
177: }
178: String begin = buff.toString();
179: if (maxMatch == patternLength) {
180: filter.addIndexCondition(new IndexCondition(
181: Comparison.EQUAL, l, ValueExpression
182: .get(ValueString.get(begin))));
183: } else {
184: // TODO check if this is correct according to Unicode rules (code
185: // points)
186: String end;
187: if (begin.length() > 0) {
188: filter.addIndexCondition(new IndexCondition(
189: Comparison.BIGGER_EQUAL, l, ValueExpression
190: .get(ValueString.get(begin))));
191: char next = begin.charAt(begin.length() - 1);
192: // search the 'next' unicode character (or at least a character
193: // that is higher)
194: for (int i = 1; i < 2000; i++) {
195: end = begin.substring(0, begin.length() - 1)
196: + (char) (next + i);
197: if (compareMode.compareString(begin, end,
198: ignoreCase) == -1) {
199: filter.addIndexCondition(new IndexCondition(
200: Comparison.SMALLER, l, ValueExpression
201: .get(ValueString.get(end))));
202: break;
203: }
204: }
205: }
206: }
207: }
208:
209: public Value getValue(Session session) throws SQLException {
210: Value l = left.getValue(session);
211: if (l == ValueNull.INSTANCE) {
212: return l;
213: }
214: if (!isInit) {
215: Value r = right.getValue(session);
216: if (r == ValueNull.INSTANCE) {
217: return r;
218: }
219: String pattern = r.getString();
220: Value e = escape == null ? null : escape.getValue(session);
221: if (e == ValueNull.INSTANCE) {
222: return ValueNull.INSTANCE;
223: }
224: initPattern(pattern, getEscapeChar(e));
225: }
226: String value = l.getString();
227: boolean result;
228: if (regexp) {
229: // result = patternRegexp.matcher(value).matches();
230: result = patternRegexp.matcher(value).find();
231: } else {
232: result = compareAt(value, 0, 0, value.length());
233: }
234: return ValueBoolean.get(result);
235: }
236:
237: private boolean compare(String s, int pi, int si) {
238: // TODO check if this is correct according to Unicode rules (code points)
239: return compareMode.equalsChars(patternString, pi, s, si,
240: ignoreCase);
241: }
242:
243: private boolean compareAt(String s, int pi, int si, int sLen) {
244: for (; pi < patternLength; pi++) {
245: int type = types[pi];
246: switch (type) {
247: case MATCH:
248: if ((si >= sLen) || !compare(s, pi, si++)) {
249: return false;
250: }
251: break;
252: case ONE:
253: if (si++ >= sLen) {
254: return false;
255: }
256: break;
257: case ANY:
258: if (++pi >= patternLength) {
259: return true;
260: }
261: while (si < sLen) {
262: if (compare(s, pi, si)
263: && compareAt(s, pi, si, sLen)) {
264: return true;
265: }
266: si++;
267: }
268: return false;
269: default:
270: throw Message.getInternalError("type=" + type);
271: }
272: }
273: return si == sLen;
274: }
275:
276: /**
277: * Test if the value matches the pattern.
278: *
279: * @param pattern the pattern
280: * @param value the value
281: * @param escape the escape character
282: * @return true if the value matches
283: */
284: public boolean test(String pattern, String value, char escape)
285: throws SQLException {
286: initPattern(pattern, escape);
287: return compareAt(value, 0, 0, value.length());
288: }
289:
290: private void initPattern(String p, char escape) throws SQLException {
291: if (regexp) {
292: patternString = p;
293: try {
294: if (ignoreCase) {
295: patternRegexp = Pattern.compile(p,
296: Pattern.CASE_INSENSITIVE);
297: } else {
298: patternRegexp = Pattern.compile(p);
299: }
300: } catch (PatternSyntaxException e) {
301: throw Message.getSQLException(
302: ErrorCode.LIKE_ESCAPE_ERROR_1,
303: new String[] { p }, e);
304: }
305: return;
306: }
307: patternLength = 0;
308: if (p == null) {
309: types = null;
310: pattern = null;
311: return;
312: }
313: int len = p.length();
314: pattern = new char[len];
315: types = new int[len];
316: boolean lastAny = false;
317: for (int i = 0; i < len; i++) {
318: char c = p.charAt(i);
319: int type;
320: if (escape == c) {
321: if (i >= len - 1) {
322: throw Message.getSQLException(
323: ErrorCode.LIKE_ESCAPE_ERROR_1, StringUtils
324: .addAsterisk(p, i));
325: }
326: c = p.charAt(++i);
327: if (c != '_' && c != '%' && c != escape) {
328: throw Message.getSQLException(
329: ErrorCode.LIKE_ESCAPE_ERROR_1, StringUtils
330: .addAsterisk(p, i));
331: }
332: type = MATCH;
333: lastAny = false;
334: } else if (c == '%') {
335: if (lastAny) {
336: continue;
337: }
338: type = ANY;
339: lastAny = true;
340: } else if (c == '_') {
341: type = ONE;
342: } else {
343: type = MATCH;
344: lastAny = false;
345: }
346: types[patternLength] = type;
347: pattern[patternLength++] = c;
348: }
349: for (int i = 0; i < patternLength - 1; i++) {
350: if ((types[i] == ANY) && (types[i + 1] == ONE)) {
351: types[i] = ONE;
352: types[i + 1] = ANY;
353: }
354: }
355: patternString = new String(pattern, 0, patternLength);
356: }
357:
358: private boolean isFullMatch() {
359: if (types == null) {
360: return false;
361: }
362: for (int i = 0; i < types.length; i++) {
363: if (types[i] != MATCH) {
364: return false;
365: }
366: }
367: return true;
368: }
369:
370: public void mapColumns(ColumnResolver resolver, int level)
371: throws SQLException {
372: left.mapColumns(resolver, level);
373: right.mapColumns(resolver, level);
374: if (escape != null) {
375: escape.mapColumns(resolver, level);
376: }
377: }
378:
379: public void setEvaluatable(TableFilter tableFilter, boolean b) {
380: left.setEvaluatable(tableFilter, b);
381: right.setEvaluatable(tableFilter, b);
382: if (escape != null) {
383: escape.setEvaluatable(tableFilter, b);
384: }
385: }
386:
387: public void updateAggregate(Session session) throws SQLException {
388: left.updateAggregate(session);
389: right.updateAggregate(session);
390: if (escape != null) {
391: escape.updateAggregate(session);
392: }
393: }
394:
395: public boolean isEverything(ExpressionVisitor visitor) {
396: return left.isEverything(visitor)
397: && right.isEverything(visitor)
398: && (escape == null || escape.isEverything(visitor));
399: }
400:
401: public int getCost() {
402: return left.getCost() + right.getCost() + 3;
403: }
404:
405: }
|