blob: 932c7b30bfd20f2ecd05af17e36abde14254018e [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.planner.logical;
import java.util.ArrayList;
import java.util.List;
import org.apache.calcite.rel.logical.LogicalIntersect;
import org.apache.calcite.rel.logical.LogicalMinus;
import com.google.common.collect.Lists;
import org.apache.calcite.rel.logical.LogicalFilter;
import org.apache.calcite.rel.logical.LogicalJoin;
import org.apache.calcite.rex.RexShuttle;
import org.apache.calcite.rex.RexUtil;
import org.apache.drill.common.exceptions.UserException;
import org.apache.drill.common.expression.SchemaPath;
import org.apache.drill.exec.exception.UnsupportedOperatorCollector;
import org.apache.drill.exec.planner.sql.DrillOperatorTable;
import org.apache.drill.exec.planner.sql.parser.DrillCalciteWrapperUtility;
import org.apache.drill.exec.util.ApproximateStringMatcher;
import org.apache.drill.exec.work.foreman.SqlUnsupportedException;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelShuttleImpl;
import org.apache.calcite.rel.logical.LogicalUnion;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.util.NlsString;
/**
* This class rewrites all the project expression that contain convert_to/ convert_from
* to actual implementations.
* Eg: convert_from(EXPR, 'JSON') is rewritten as convert_fromjson(EXPR)
*
* With the actual method name we can find out if the function has a complex
* output type and we will fire/ ignore certain rules (merge project rule) based on this fact.
*/
public class PreProcessLogicalRel extends RelShuttleImpl {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(PreProcessLogicalRel.class);
private final RelDataTypeFactory factory;
private final DrillOperatorTable table;
private final UnsupportedOperatorCollector unsupportedOperatorCollector;
private final UnwrappingExpressionVisitor unwrappingExpressionVisitor;
public static PreProcessLogicalRel createVisitor(RelDataTypeFactory factory, DrillOperatorTable table, RexBuilder rexBuilder) {
return new PreProcessLogicalRel(factory, table, rexBuilder);
}
private PreProcessLogicalRel(RelDataTypeFactory factory, DrillOperatorTable table, RexBuilder rexBuilder) {
super();
this.factory = factory;
this.table = table;
this.unsupportedOperatorCollector = new UnsupportedOperatorCollector();
this.unwrappingExpressionVisitor = new UnwrappingExpressionVisitor(rexBuilder);
}
@Override
public RelNode visit(LogicalProject project) {
final List<RexNode> projExpr = Lists.newArrayList();
for(RexNode rexNode : project.getProjects()) {
projExpr.add(rexNode.accept(unwrappingExpressionVisitor));
}
project = project.copy(project.getTraitSet(),
project.getInput(),
projExpr,
project.getRowType());
List<RexNode> exprList = new ArrayList<>();
boolean rewrite = false;
for (RexNode rex : project.getProjects()) {
RexNode newExpr = rex;
if (rex instanceof RexCall) {
RexCall function = (RexCall) rex;
String functionName = function.getOperator().getName();
int nArgs = function.getOperands().size();
// check if its a convert_from or convert_to function
if (functionName.equalsIgnoreCase("convert_from") || functionName.equalsIgnoreCase("convert_to")) {
String literal;
if (nArgs == 2) {
if (function.getOperands().get(1) instanceof RexLiteral) {
try {
literal = ((NlsString) (((RexLiteral) function.getOperands().get(1)).getValue())).getValue();
} catch (final ClassCastException e) {
// Caused by user entering a value with a non-string literal
throw getConvertFunctionInvalidTypeException(function);
}
} else {
// caused by user entering a non-literal
throw getConvertFunctionInvalidTypeException(function);
}
} else {
// Second operand is missing
throw UserException.parseError()
.message("'%s' expects a string literal as a second argument.", functionName)
.build(logger);
}
RexBuilder builder = new RexBuilder(factory);
// construct the new function name based on the input argument
String newFunctionName = functionName + literal;
// Look up the new function name in the drill operator table
List<SqlOperator> operatorList = table.getSqlOperator(newFunctionName);
if (operatorList.size() == 0) {
// User typed in an invalid type name
throw getConvertFunctionException(functionName, literal);
}
SqlFunction newFunction = null;
// Find the SqlFunction with the correct args
for (SqlOperator op : operatorList) {
if (op.getOperandTypeChecker().getOperandCountRange().isValidCount(nArgs - 1)) {
newFunction = (SqlFunction) op;
break;
}
}
if (newFunction == null) {
// we are here because we found some dummy convert function. (See DummyConvertFrom and DummyConvertTo)
throw getConvertFunctionException(functionName, literal);
}
// create the new expression to be used in the rewritten project
newExpr = builder.makeCall(newFunction, function.getOperands().subList(0, 1));
rewrite = true;
}
}
exprList.add(newExpr);
}
if (rewrite) {
LogicalProject newProject = project.copy(project.getTraitSet(), project.getInput(0), exprList, project.getRowType());
return visitChild(newProject, 0, project.getInput());
}
return visitChild(project, 0, project.getInput());
}
@Override
public RelNode visit(LogicalFilter filter) {
final RexNode condition = filter.getCondition().accept(unwrappingExpressionVisitor);
filter = filter.copy(
filter.getTraitSet(),
filter.getInput(),
condition);
return visitChild(filter, 0, filter.getInput());
}
@Override
public RelNode visit(LogicalJoin join) {
final RexNode conditionExpr = join.getCondition().accept(unwrappingExpressionVisitor);
join = join.copy(join.getTraitSet(),
conditionExpr,
join.getLeft(),
join.getRight(),
join.getJoinType(),
join.isSemiJoinDone());
return visitChildren(join);
}
@Override
public RelNode visit(LogicalUnion union) {
for (RelNode child : union.getInputs()) {
for (RelDataTypeField dataField : child.getRowType().getFieldList()) {
if (dataField.getName().contains(SchemaPath.DYNAMIC_STAR)) {
unsupportedOperatorCollector.setException(SqlUnsupportedException.ExceptionType.RELATIONAL,
"Union-All over schema-less tables must specify the columns explicitly\n" +
"See Apache Drill JIRA: DRILL-2414");
throw new UnsupportedOperationException();
}
}
}
return visitChildren(union);
}
@Override
public RelNode visit(LogicalIntersect intersect) {
for (RelNode child : intersect.getInputs()) {
for (RelDataTypeField dataField : child.getRowType().getFieldList()) {
if (dataField.getName().contains(SchemaPath.DYNAMIC_STAR)) {
unsupportedOperatorCollector.setException(SqlUnsupportedException.ExceptionType.RELATIONAL,
"Intersect(All) over schema-less tables must specify the columns explicitly\n");
throw new UnsupportedOperationException();
}
}
}
return visitChildren(intersect);
}
@Override
public RelNode visit(LogicalMinus minus) {
for (RelNode child : minus.getInputs()) {
for (RelDataTypeField dataField : child.getRowType().getFieldList()) {
if (dataField.getName().contains(SchemaPath.DYNAMIC_STAR)) {
unsupportedOperatorCollector.setException(SqlUnsupportedException.ExceptionType.RELATIONAL,
"Except(All) over schema-less tables must specify the columns explicitly\n");
throw new UnsupportedOperationException();
}
}
}
return visitChildren(minus);
}
private UserException getConvertFunctionInvalidTypeException(final RexCall function) {
// Caused by user entering a value with a numeric type
final String functionName = function.getOperator().getName();
final String typeName = function.getOperands().get(1).getType().getFullTypeString();
return UserException.parseError()
.message("Invalid type %s passed as second argument to function '%s'. " +
"The function expects a literal argument.",
typeName,
functionName)
.build(logger);
}
private UserException getConvertFunctionException(final String functionName, final String typeName) {
final String newFunctionName = functionName + typeName;
final String typeNameToPrint = typeName.length()==0 ? "<empty_string>" : typeName;
final UserException.Builder exceptionBuilder = UserException.unsupportedError()
.message("%s does not support conversion %s type '%s'.", functionName, functionName.substring(8).toLowerCase(), typeNameToPrint);
// Build a nice error message
if (typeName.length()>0) {
List<String> ops = new ArrayList<>();
for (SqlOperator op : table.getOperatorList()) {
ops.add(op.getName());
}
final String bestMatch = ApproximateStringMatcher.getBestMatch(ops, newFunctionName);
if (bestMatch != null && bestMatch.length() > functionName.length() && bestMatch.toLowerCase().startsWith("convert")) {
final StringBuilder s = new StringBuilder("Did you mean ")
.append(bestMatch.substring(functionName.length()))
.append("?");
exceptionBuilder.addContext(s.toString());
}
}
return exceptionBuilder.build(logger);
}
public void convertException() throws SqlUnsupportedException {
unsupportedOperatorCollector.convertException();
}
private static class UnwrappingExpressionVisitor extends RexShuttle {
private final RexBuilder rexBuilder;
private UnwrappingExpressionVisitor(RexBuilder rexBuilder) {
this.rexBuilder = rexBuilder;
}
@Override
public RexNode visitCall(final RexCall call) {
final List<RexNode> clonedOperands = visitList(call.operands, new boolean[]{true});
final SqlOperator sqlOperator = DrillCalciteWrapperUtility.extractSqlOperatorFromWrapper(call.getOperator());
return RexUtil.flatten(rexBuilder,
rexBuilder.makeCall(
call.getType(),
sqlOperator,
clonedOperands));
}
}
}