blob: 07e975ee2fefe3c2231fdf7c16ff19d06727340f [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.drill.exec.store.ischema;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.apache.drill.common.FunctionNames;
import org.apache.drill.exec.expr.fn.impl.RegexpUtil;
import org.apache.drill.exec.expr.fn.impl.RegexpUtil.SqlPatternType;
import org.apache.drill.exec.store.ischema.InfoSchemaFilter.ExprNode.Type;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static org.apache.drill.exec.expr.fn.impl.RegexpUtil.sqlToRegexLike;
@JsonTypeName("info-schema-filter")
public class InfoSchemaFilter {
private final ExprNode exprRoot;
@JsonCreator
public InfoSchemaFilter(@JsonProperty("exprRoot") ExprNode exprRoot) {
this.exprRoot = exprRoot;
}
@JsonProperty("exprRoot")
public ExprNode getExprRoot() {
return exprRoot;
}
// include type-info to be able to deserialize subclasses correctly
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
public static class ExprNode {
@JsonProperty
public Type type;
public ExprNode(Type type) {
this.type = type;
}
public enum Type {
FUNCTION,
FIELD,
CONSTANT
}
}
public static class FunctionExprNode extends ExprNode {
@JsonProperty
public String function;
@JsonProperty
public List<ExprNode> args;
@JsonCreator
public FunctionExprNode(@JsonProperty("function") String function,
@JsonProperty("args") List<ExprNode> args) {
super(Type.FUNCTION);
this.function = function;
this.args = args;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(function);
builder.append("(");
builder.append(Joiner.on(",").join(args));
builder.append(")");
return builder.toString();
}
}
public static class FieldExprNode extends ExprNode {
@JsonProperty
public String field;
@JsonCreator
public FieldExprNode(@JsonProperty("field") String field) {
super(Type.FIELD);
this.field = field;
}
@Override
public String toString() {
return String.format("Field=%s", field);
}
}
public static class ConstantExprNode extends ExprNode {
@JsonProperty
public String value;
@JsonCreator
public ConstantExprNode(@JsonProperty("value") String value) {
super(Type.CONSTANT);
this.value = value;
}
@Override
public String toString() {
return String.format("Literal=%s", value);
}
}
public enum Result {
TRUE,
FALSE,
INCONCLUSIVE
}
/**
* Evaluate the filter for given <COLUMN NAME, VALUE> pairs.
*
* @param recordValues map of field names and their values
* @return evaluation result
*/
@JsonIgnore
public Result evaluate(Map<String, String> recordValues) {
return evaluateHelper(recordValues, false, getExprRoot());
}
/**
* Evaluate the filter for given <COLUMN NAME, VALUE> pairs.
*
* @param recordValues map of field names and their values
* @param prefixMatchesInconclusive whether a prefix match between a schema path and a filter value
* results in Result.INCONCLUSIVE. Used for pruning the schema search
* tree, e.g. "dfs" need not be recursed to find a schema of "cp.default"
* @return evaluation result
*/
@JsonIgnore
public Result evaluate(Map<String, String> recordValues, boolean prefixMatchesInconclusive) {
return evaluateHelper(recordValues, prefixMatchesInconclusive, getExprRoot());
}
private Result evaluateHelper(
Map<String, String> recordValues,
boolean prefixMatchesInconclusive,
ExprNode exprNode
) {
if (exprNode.type == Type.FUNCTION) {
return evaluateHelperFunction(
recordValues,
prefixMatchesInconclusive,
(FunctionExprNode) exprNode
);
}
throw new UnsupportedOperationException(
String.format("Unknown expression type '%s' in InfoSchemaFilter", exprNode.type));
}
private Result evaluateHelperFunction(
Map<String, String> recordValues,
boolean prefixMatchesInconclusive,
FunctionExprNode exprNode
) {
switch (exprNode.function) {
case FunctionNames.LIKE: {
FieldExprNode col = (FieldExprNode) exprNode.args.get(0);
ConstantExprNode pattern = (ConstantExprNode) exprNode.args.get(1);
ConstantExprNode escape = exprNode.args.size() > 2 ? (ConstantExprNode) exprNode.args.get(2) : null;
final String fieldValue = recordValues.get(col.field);
if (fieldValue == null) {
return Result.INCONCLUSIVE;
}
RegexpUtil.SqlPatternInfo spi = escape == null
? sqlToRegexLike(pattern.value)
: sqlToRegexLike(pattern.value, escape.value);
if (Pattern.matches(spi.getJavaPatternString(), fieldValue)) {
// E.g. pattern = 'dfs.%', schema path = 'dfs.tmp'.
// The entire schema path matches.
return Result.TRUE;
}
if (!prefixMatchesInconclusive) {
// E.g. pattern = 'dfs.%', schema path = 'dfs'.
// There may be prefix match but prefixMatchesInconclusive is false.
return Result.FALSE;
}
if ((spi.getPatternType() == SqlPatternType.STARTS_WITH || spi.getPatternType() == SqlPatternType.CONSTANT) &&
!spi.getSimplePatternString().startsWith(fieldValue)) {
// E.g. pattern = 'dfs.%', schema path = 'cp'.
// No match, not even to a prefix.
return Result.FALSE;
}
// E.g. pattern = 'dfs.%', schema path = 'dfs'.
// A prefix matches
return Result.INCONCLUSIVE;
}
case FunctionNames.EQ:
case "not equal": // TODO: Is this name correct?
case "notequal": // TODO: Is this name correct?
case FunctionNames.NE: {
FieldExprNode col = (FieldExprNode) exprNode.args.get(0);
ConstantExprNode arg = (ConstantExprNode) exprNode.args.get(1);
final String value = recordValues.get(col.field);
if (Strings.isNullOrEmpty(value)) {
return Result.INCONCLUSIVE;
}
boolean prefixMatch = arg.value.startsWith(value);
boolean exactMatch = prefixMatch && arg.value.equals(value);
if (exprNode.function.equals(FunctionNames.EQ)) {
// Equality case
if (exactMatch) {
return Result.TRUE;
} else {
return prefixMatchesInconclusive && prefixMatch ? Result.INCONCLUSIVE: Result.FALSE;
}
} else {
// Inequality case
if (exactMatch) {
return Result.FALSE;
} else {
return prefixMatchesInconclusive && prefixMatch ? Result.INCONCLUSIVE : Result.TRUE;
}
}
}
case FunctionNames.OR:
case "booleanor": { // TODO: Is this name correct?
// If at least one arg returns TRUE, then the OR function value is TRUE
// If all args return FALSE, then OR function value is FALSE
// For all other cases, return INCONCLUSIVE
Result result = Result.FALSE;
for(ExprNode arg : exprNode.args) {
Result exprResult = evaluateHelper(recordValues, prefixMatchesInconclusive, arg);
if (exprResult == Result.TRUE) {
return Result.TRUE;
} else if (exprResult == Result.INCONCLUSIVE) {
result = Result.INCONCLUSIVE;
}
}
return result;
}
case FunctionNames.AND:
case "booleanand": { // TODO: Is this name correct?
// If at least one arg returns FALSE, then the AND function value is FALSE
// If at least one arg returns INCONCLUSIVE, then the AND function value is INCONCLUSIVE
// If all args return TRUE, then the AND function value is TRUE
Result result = Result.TRUE;
for(ExprNode arg : exprNode.args) {
Result exprResult = evaluateHelper(recordValues, prefixMatchesInconclusive, arg);
if (exprResult == Result.FALSE) {
return exprResult;
}
if (exprResult == Result.INCONCLUSIVE) {
result = Result.INCONCLUSIVE;
}
}
return result;
}
case "in": {
// This case will probably only ever run if the user submits a manually
// crafted plan because the IN operator is compiled either to a chain
// of boolean ORs, or to a hash join with a relation which uses VALUES
// to generate the list of constants provided to the IN operator in
// the query. See the planner.in_subquery_threshold option.
FieldExprNode col = (FieldExprNode) exprNode.args.get(0);
List<ExprNode> args = exprNode.args.subList(1, exprNode.args.size());
final String fieldValue = recordValues.get(col.field);
if (fieldValue != null) {
for(ExprNode arg: args) {
if (fieldValue.equals(((ConstantExprNode) arg).value)) {
return Result.TRUE;
}
}
return Result.FALSE;
}
return Result.INCONCLUSIVE;
}
}
throw new UnsupportedOperationException(
String.format("Unknown function '%s' in InfoSchemaFilter", exprNode.function));
}
@Override
public String toString() {
return exprRoot.toString();
}
}