blob: 7380c56cdf94de9d92c2d92e290fde71a1906c0a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.cache.query.internal;
import java.util.regex.Pattern;
import org.apache.logging.log4j.Logger;
import org.apache.geode.cache.query.AmbiguousNameException;
import org.apache.geode.cache.query.FunctionDomainException;
import org.apache.geode.cache.query.NameResolutionException;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.SelectResults;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.index.IndexManager;
import org.apache.geode.cache.query.internal.index.IndexProtocol;
import org.apache.geode.cache.query.internal.index.PrimaryKeyIndex;
import org.apache.geode.cache.query.internal.parse.OQLLexerTokenTypes;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.pdx.internal.PdxString;
public class CompiledLike extends CompiledComparison {
private static final Logger logger = LogService.getLogger();
private static final int WILDCARD_PERCENT = 0;
private static final int WILDCARD_UNDERSCORE = 1;
private final Object wildcardTypeKey = new Object();
private final Object wildcardPositionKey = new Object();
private final Object patternLengthKey = new Object();
static final String LOWEST_STRING = "";
private static final char BOUNDARY_CHAR = (char) 255;
private static final char UNDERSCORE = '_';
private static final char PERCENT = '%';
private static final char BACKSLASH = '\\';
private final CompiledValue var;
private final Object isIndexEvaluatedKey = new Object();
private final CompiledValue bindArg;
CompiledLike(CompiledValue var, CompiledValue pattern) {
super(var, pattern, OQLLexerTokenTypes.TOK_EQ);
this.var = var;
this.bindArg = pattern;
}
private int getWildcardPosition(ExecutionContext context) {
return (Integer) context.cacheGet(wildcardPositionKey, -1);
}
private int getWildcardType(ExecutionContext context) {
return (Integer) context.cacheGet(wildcardTypeKey, -1);
}
private int getPatternLength(ExecutionContext context) {
return (Integer) context.cacheGet(patternLengthKey, 0);
}
private boolean getIsIndexEvaluated(ExecutionContext context) {
return (Boolean) context.cacheGet(isIndexEvaluatedKey, false);
}
OrganizedOperands organizeOperands(ExecutionContext context, boolean completeExpansionNeeded,
RuntimeIterator[] indpndntItrs) throws FunctionDomainException, TypeMismatchException,
NameResolutionException, QueryInvocationTargetException {
CompiledComparison[] cvs = getExpandedOperandsWithIndexInfoSetIfAny(context);
Filter filter = null;
if (cvs.length == 1) {
// For the equality condition
filter = cvs[0];
} else {
// 2 or 3 conditions; create junctions
if ((getOperator() == OQLLexerTokenTypes.TOK_NE)
&& (getWildcardPosition(context) == getPatternLength(context) - 1)
&& (getWildcardType(context) == WILDCARD_PERCENT)) {
// negation supported only for trailing %
// GroupJunction is created since the boundary conditions go out of
// range and will be evaluated as false if a RangeJunction was used
// For example, for NOT LIKE a%, the CCs generated would be < A OR >= B
// which would cause the checkForRangeBoundednessAndTrimNotEqualKeyset
// method of RangeJunction to return false
filter = new GroupJunction(OQLLexerTokenTypes.LITERAL_or, indpndntItrs,
completeExpansionNeeded, cvs);
} else {
filter = new RangeJunction(OQLLexerTokenTypes.LITERAL_and, indpndntItrs,
completeExpansionNeeded, cvs);
}
}
OrganizedOperands result = new OrganizedOperands();
result.isSingleFilter = true;
result.filterOperand = filter;
return result;
}
/**
* Expands the CompiledLike operands based on sargability into multiple CompiledComparisons
*
* @return The generated CompiledComparisons
*/
CompiledComparison[] getExpandedOperandsWithIndexInfoSetIfAny(ExecutionContext context)
throws AmbiguousNameException, TypeMismatchException, NameResolutionException,
FunctionDomainException, QueryInvocationTargetException {
String pattern = (String) this.bindArg.evaluate(context);
// check if it is filter evaluatable
CompiledComparison[] cvs = getRangeIfSargable(context, this.var, pattern);
for (CompiledComparison cc : cvs) {
// negation supported only for trailing %
if ((getOperator() == OQLLexerTokenTypes.TOK_NE)
&& (getWildcardPosition(context) == getPatternLength(context) - 1)
&& (getWildcardType(context) == WILDCARD_PERCENT)) {
cc.negate();
}
cc.computeDependencies(context);
// Set the indexinfo for the newly created CCs with the indexinfo of this
// CompiledLike object
IndexInfo[] thisIndexInfo = ((IndexInfo[]) context.cacheGet(this));
if (thisIndexInfo != null && thisIndexInfo.length > 0) {
// set the index key in the indexinfo of the CC since the index key
// in the indexinfo of this object might have been modified in the
// checkIfSargableAndRemoveEscapeChars method
IndexInfo indexInfo =
new IndexInfo(cc.getKey(context), thisIndexInfo[0]._path, thisIndexInfo[0]._index,
thisIndexInfo[0]._matchLevel, thisIndexInfo[0].mapping, cc.getOperator());
context.cachePut(cc, new IndexInfo[] {indexInfo});
}
}
if (IndexManager.testHook != null) {
if (logger.isDebugEnabled()) {
logger.debug("IndexManager TestHook is set in getExpandedOperandsWithIndexInfoSetIfAny.");
}
IndexManager.testHook.hook(12);
}
return cvs;
}
@Override
public SelectResults filterEvaluate(ExecutionContext context, SelectResults intermediateResults,
boolean completeExpansionNeeded, CompiledValue iterOperands, RuntimeIterator[] indpndntItrs,
boolean isIntersection, boolean conditioningNeeded, boolean evaluateProjection)
throws FunctionDomainException, TypeMismatchException, NameResolutionException,
QueryInvocationTargetException {
OrganizedOperands newOperands =
organizeOperands(context, completeExpansionNeeded, indpndntItrs);
assert newOperands.iterateOperand == null;
SelectResults result = intermediateResults;
result = (newOperands.filterOperand).filterEvaluate(context, intermediateResults,
completeExpansionNeeded, iterOperands, indpndntItrs, isIntersection, conditioningNeeded,
evaluateProjection);
return result;
}
@Override
public SelectResults filterEvaluate(ExecutionContext context, SelectResults intermediateResults)
throws FunctionDomainException, TypeMismatchException, NameResolutionException,
QueryInvocationTargetException {
RuntimeIterator grpItr = (RuntimeIterator) QueryUtils
.getCurrentScopeUltimateRuntimeIteratorsIfAny(this, context).iterator().next();
OrganizedOperands newOperands = organizeOperands(context, true, new RuntimeIterator[] {grpItr});
assert newOperands.iterateOperand == null;
SelectResults result = intermediateResults;
result = (newOperands.filterOperand).filterEvaluate(context, intermediateResults);
return result;
}
/**
* Breaks down the like predicate (if sargable) into 2 or 3 CompiledComparisons based on the
* presence of wildcard
*
* @return The generated CompiledComparisons
*/
CompiledComparison[] getRangeIfSargable(ExecutionContext context, CompiledValue var,
String pattern) {
CompiledComparison[] cv = null;
StringBuffer buffer = new StringBuffer(pattern);
// check if the string has a % or _ anywhere
int wildcardPosition = checkIfSargableAndRemoveEscapeChars(context, buffer);
context.cachePut(wildcardPositionKey, wildcardPosition);
int patternLength = buffer.length();
context.cachePut(patternLengthKey, patternLength);
context.cachePut(isIndexEvaluatedKey, true);
// if wildcardPosition is >= 0 means it is sargable
if (wildcardPosition >= 0) {
int len = patternLength;
if (wildcardPosition == 0) {
// wildcard is the leading char
// change the like predicate to >= "" and like
cv = new CompiledComparison[] {new CompiledComparison(var,
new CompiledLiteral(LOWEST_STRING), OQLLexerTokenTypes.TOK_GE), this};
} else {
// the wildcard is not the first char
// delete all chars after the wildchar
for (int k = len - 1; k >= wildcardPosition; k--) {
buffer.deleteCharAt(k);
--len;
}
String lowerBound = buffer.toString();
int upperBoundPosition = len - 1;
char upperBoundChar;
while (true) {
upperBoundChar = (buffer.charAt(upperBoundPosition));
if (upperBoundChar == BOUNDARY_CHAR) {
--upperBoundPosition;
} else {
upperBoundChar = (char) (buffer.charAt(upperBoundPosition) + 1);
break;
}
}
buffer.delete(upperBoundPosition, len);
buffer.append(upperBoundChar);
String upperBound = buffer.toString();
CompiledComparison c1 =
new CompiledComparison(var, new CompiledLiteral(lowerBound), OQLLexerTokenTypes.TOK_GE);
CompiledComparison c2 =
new CompiledComparison(var, new CompiledLiteral(upperBound), OQLLexerTokenTypes.TOK_LT);
// if % is not the last char in the string.
// or the wildchar is _ which could be anywhere
if (len < (patternLength - 1) || getWildcardType(context) == WILDCARD_UNDERSCORE) {
// negation not supported if % is not the last char and also for a _
// anywhere
if (getOperator() == OQLLexerTokenTypes.TOK_NE) {
cv = new CompiledComparison[] {new CompiledComparison(var,
new CompiledLiteral(LOWEST_STRING), OQLLexerTokenTypes.TOK_GE), this};
} else {
// the like predicate is broken into 3 compiled comparisons
cv = new CompiledComparison[] {c1, c2, this};
}
} else {
// % is at the end of the string
// the like predicate is broken down to 2 compile comparisons
cv = new CompiledComparison[] {c1, c2};
}
}
} else {
// not sargable
// Change the like predicate to equality
cv = new CompiledComparison[] {
new CompiledComparison(var, new CompiledLiteral(buffer.toString()), getOperator())};
}
return cv;
}
private String getRegexPattern(String pattern) {
StringBuilder sb = new StringBuilder();
boolean prevMetaChar = false;
int len = pattern.length();
for (int i = 0; i < len; i++) {
char ch = pattern.charAt(i);
switch (ch) {
// meta chars: \ ^ * . + ? ( ) | [ ]
case ']':
case '[':
case '^':
case '*':
case '.':
case '+':
case '?':
case '(':
case ')':
case '|':
case '{':
case '}':
case '\\':
// if ((ch == '\\') && (i+1) < len && (pattern.charAt(i+1) == '_' || pattern.charAt(i+1)
// == '%')) {
if ((ch == '\\')) {
if (!((i + 1) < len && (pattern.charAt(i + 1) == '\\'))) {
break;
}
i++;
}
// Check if subsequent chars are meta chars.
// \Q is used for start of string literal
// \E for end of string literal. E.g. \Q+*\E to escape +*
if (!prevMetaChar) {
sb.append('\\');
sb.append('Q');
prevMetaChar = true;
}
sb.append(ch);
break;
case '_': // replace with .
case '%': // replace with .*
if (prevMetaChar) {
sb.append('\\');
sb.append('E');
prevMetaChar = false;
}
// Check if the % has a valid escape. Backtrack to check for \.
// If the number of \ on back track is odd, then % is escaped.
int numConsecutiveBackSlash = 0;
for (int j = i - 1; j > -1; --j) {
if (pattern.charAt(j) == '\\') {
++numConsecutiveBackSlash;
} else {
break;
}
}
if ((numConsecutiveBackSlash % 2) == 0) {
if (ch == '%') {
sb.append(".*");
// ignore successive '%'
while ((i + 1) < len && pattern.charAt(i + 1) == '%') {
i++;
}
} else {
sb.append(".");
}
} else {
// The percentage or underscore sign is escaped. Hence it is to be un-escaped now
// So remove the backslash
// sb.deleteCharAt(sb.length() - 1);
sb.append(ch);
}
break;
default:
if (prevMetaChar) {
sb.append('\\');
sb.append('E');
prevMetaChar = false;
}
sb.append(ch);
}
}
return sb.toString();
}
/**
* Checks if index can be used for Strings with wildcards. Two wild cards are supported % and _.
* The wildcard could be at any index position of the string.
*
* @return position of wildcard if sargable otherwise -1
*/
int checkIfSargableAndRemoveEscapeChars(ExecutionContext context, StringBuffer buffer) {
int len = buffer.length();
int wildcardPosition = -1;
for (int i = 0; i < len; ++i) {
char ch = buffer.charAt(i);
if (ch == UNDERSCORE) {
context.cachePut(wildcardTypeKey, WILDCARD_UNDERSCORE);
wildcardPosition = i; // the position of the wildcard
break;
} else if (ch == PERCENT) {
context.cachePut(wildcardTypeKey, WILDCARD_PERCENT);
wildcardPosition = i; // the position of the wildcard
break;
} else if (ch == BACKSLASH) {
if (i + 1 < len) {
if (buffer.charAt(i + 1) == PERCENT || buffer.charAt(i + 1) == UNDERSCORE) {
wildcardPosition = -1; // escape the wildcard
}
buffer.deleteCharAt(i); // one \ escapes next
len--;
}
}
}
return wildcardPosition;
}
@Override
public Object evaluate(ExecutionContext context) throws FunctionDomainException,
TypeMismatchException, NameResolutionException, QueryInvocationTargetException {
// reset the isIndexEvaluated flag here since index is not being used here
context.cachePut(isIndexEvaluatedKey, false);
Pattern pattern = (Pattern) context.cacheGet(this.bindArg);
if (pattern == null) {
String strPattern = this.bindArg.evaluate(context).toString(); // handles both Strings and
// PdxStrings
if (strPattern == null) {
throw new UnsupportedOperationException(
"Null values are not supported with LIKE predicate.");
}
pattern = Pattern.compile(getRegexPattern(strPattern), Pattern.MULTILINE | Pattern.DOTALL);
context.cachePut(this.bindArg, pattern);
}
Object value = this.var.evaluate(context);
if (value == null) {
return null;
}
if (!((value instanceof String) || (value instanceof PdxString)
|| (value == QueryService.UNDEFINED))) {
// throw new TypeMismatchException(
// String.format("Unable to compare object of type ' %s ' with object of type ' %s '",
// "java.lang.String", value.getClass().getName()));
if (getOperator() == TOK_NE) {
return true;
}
return false;
}
// Check if LIKE clause is negated (_operator == TOK_NE) in query.
boolean isMatched = pattern.matcher(value.toString()).matches();
if (getOperator() == TOK_NE) {
isMatched = !isMatched;
}
return isMatched;
}
/**
* @since GemFire 6.6
*/
@Override
protected PlanInfo protGetPlanInfo(ExecutionContext context)
throws TypeMismatchException, AmbiguousNameException, NameResolutionException {
/*
* During filterevaluation, CompiledLike is converted to 2 or 3 CompiledComparisons. One of the
* CCs could be a CompiledLike itself. For example If the wildcard is _ or the % is anywhere
* except at the end in the pattern, a GroupJunction is created. For 'ab%cd', the GroupJunction
* would be ">=ab AND < ac AND LIKE ab%cd". The check avoids the re-filterevaluation of this
* CompiledLike.
*/
PlanInfo result = null;
if (getIsIndexEvaluated(context)) {
result = new PlanInfo();
result.evalAsFilter = false;
} else {
result = super.protGetPlanInfo(context);
// CCs created have range conditions which are not supported by PrimaryKey
// index. So disabling filter when PrimaryKey index is used
if (result.indexes.size() > 0 && result.indexes.get(0) instanceof PrimaryKeyIndex) {
result.evalAsFilter = false;
}
}
return result;
}
@Override
public int getType() {
return LIKE;
}
@Override
public boolean isLimitApplicableAtIndexLevel(ExecutionContext context) {
return true;
}
@Override
public boolean isOrderByApplicableAtIndexLevel(ExecutionContext context,
String canonicalizedOrderByClause) throws FunctionDomainException, TypeMismatchException,
NameResolutionException, QueryInvocationTargetException {
if (this.getPlanInfo(context).evalAsFilter) {
PlanInfo pi = this.getPlanInfo(context);
if (pi.indexes.size() == 1) {
IndexProtocol ip = (IndexProtocol) pi.indexes.get(0);
if (ip.getCanonicalizedIndexedExpression().equals(canonicalizedOrderByClause)) {
return true;
}
}
}
return false;
}
}