Human-readable and actionable SQL error messages (#11911)

This PR does two things

1. It adds the capability to surface missing features in SQL to users - The calcite planner will explore through multiple rules to convert a logical SQL query to a druid native query. Some rules change the shape of the query itself, optimize it and some rules are responsible for translating the query into a druid native query. These are DruidQueryRule, DruidOuterQueryRule, DruidJoinRule, DruidUnionDataSourceRule, DruidUnionRule etc. These rules will look at SQL and will do the necessary transformation. But if the rule can't transform the query, it returns back the control to the calcite planner without recording why was it not able to transform. E.g. there is a join query with a non-equal join condition. DruidJoinRule will look at the condition, see that it is not supported, and return back the control. The reason can be that a query can be planned in many different ways so if one rule can't parse it, the query may still be parseable by other rules. In this PR, we are intercepting these gaps and passing them back to the user if the query could not be planned at all.

2. The said capability has been used to generate actionable errors for some common unsupported SQL features. However, not all possible errors are covered and we can keep adding more in the future.
This commit is contained in:
Abhishek Agarwal 2021-12-07 09:44:08 +05:30 committed by GitHub
parent 34a3d45737
commit 834aae096a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 366 additions and 177 deletions

View File

@ -19,17 +19,27 @@
package org.apache.druid.utils;
import javax.annotation.Nullable;
public final class Throwables
{
public static boolean isThrowable(Throwable t, Class<? extends Throwable> searchFor)
/**
* searches for a throwable in the cause chain that is of same type as {@param searchFor}. The class of returned
* throwable will either be same as {@param searchFor} or a sub-class of {@param searchFor}.
* @param t - the throwable in which to search
* @param searchFor - Class of throwable to search for
* @return null if not found otherwise the cause exception that is of same type as searchFor
*/
@Nullable
public static Throwable getCauseOfType(Throwable t, Class<? extends Throwable> searchFor)
{
if (t.getClass().isAssignableFrom(searchFor)) {
return true;
if (searchFor.isAssignableFrom(t.getClass())) {
return t;
} else {
if (t.getCause() != null) {
return isThrowable(t.getCause(), searchFor);
return getCauseOfType(t.getCause(), searchFor);
} else {
return false;
return null;
}
}
}

View File

@ -19,30 +19,42 @@
package org.apache.druid.utils;
import org.apache.druid.java.util.common.ISE;
import org.junit.Assert;
import org.junit.Test;
public class ThrowablesTest
{
@Test
public void testIsThrowableItself()
public void testGetCauseOfType_Itself()
{
Assert.assertTrue(Throwables.isThrowable(new NoClassDefFoundError(), NoClassDefFoundError.class));
Throwable th = new NoClassDefFoundError();
Assert.assertSame(th, Throwables.getCauseOfType(th, NoClassDefFoundError.class));
}
@Test
public void testIsThrowableNestedThrowable()
public void testGetCauseOfType_NestedThrowable()
{
Assert.assertTrue(
Throwables.isThrowable(new RuntimeException(new NoClassDefFoundError()), NoClassDefFoundError.class)
Throwable th = new NoClassDefFoundError();
Assert.assertSame(th,
Throwables.getCauseOfType(new RuntimeException(th), NoClassDefFoundError.class)
);
}
@Test
public void testIsThrowableNonTarget()
public void testGetCauseOfType_NestedThrowableSubclass()
{
Assert.assertFalse(
Throwables.isThrowable(new RuntimeException(new ClassNotFoundException()), NoClassDefFoundError.class)
Throwable th = new ISE("something");
Assert.assertSame(th,
Throwables.getCauseOfType(new RuntimeException(th), IllegalStateException.class)
);
}
@Test
public void testGetCauseOfType_NonTarget()
{
Assert.assertNull(
Throwables.getCauseOfType(new RuntimeException(new ClassNotFoundException()), NoClassDefFoundError.class)
);
}
}

View File

@ -318,9 +318,9 @@ public class QueryLifecycle
if (e != null) {
statsMap.put("exception", e.toString());
if (QueryContexts.isDebug(baseQuery)) {
log.error(e, "Exception while processing queryId [%s]", baseQuery.getId());
log.warn(e, "Exception while processing queryId [%s]", baseQuery.getId());
} else {
log.noStackTrace().error(e, "Exception while processing queryId [%s]", baseQuery.getId());
log.noStackTrace().warn(e, "Exception while processing queryId [%s]", baseQuery.getId());
}
if (e instanceof QueryInterruptedException || e instanceof QueryTimeoutException) {
// Mimic behavior from QueryResource, where this code was originally taken from.

View File

@ -37,7 +37,6 @@ import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.Optionality;
import org.apache.druid.java.util.common.HumanReadableBytes;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.aggregation.ExpressionLambdaAggregatorFactory;
@ -50,9 +49,11 @@ import org.apache.druid.sql.calcite.expression.DruidExpression;
import org.apache.druid.sql.calcite.expression.Expressions;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.apache.druid.sql.calcite.rel.VirtualColumnRegistry;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@ -184,7 +185,7 @@ public class ArraySqlAggregator implements SqlAggregator
{
RelDataType type = sqlOperatorBinding.getOperandType(0);
if (SqlTypeUtil.isArray(type)) {
throw new ISE("Cannot use ARRAY_AGG on array inputs %s", type);
throw new UnsupportedSQLQueryException("Cannot use ARRAY_AGG on array inputs %s", type);
}
return Calcites.createSqlArrayTypeWithNullability(
sqlOperatorBinding.getTypeFactory(),

View File

@ -60,9 +60,11 @@ import org.apache.druid.sql.calcite.expression.DruidExpression;
import org.apache.druid.sql.calcite.expression.Expressions;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.apache.druid.sql.calcite.rel.VirtualColumnRegistry;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -90,7 +92,7 @@ public class EarliestLatestAnySqlAggregator implements SqlAggregator
case COMPLEX:
return new StringFirstAggregatorFactory(name, fieldName, timeColumn, maxStringBytes);
default:
throw new ISE("Cannot build EARLIEST aggregatorFactory for type[%s]", type);
throw new UnsupportedSQLQueryException("EARLIEST aggregator is not supported for '%s' type", type);
}
}
},
@ -110,7 +112,7 @@ public class EarliestLatestAnySqlAggregator implements SqlAggregator
case COMPLEX:
return new StringLastAggregatorFactory(name, fieldName, timeColumn, maxStringBytes);
default:
throw new ISE("Cannot build LATEST aggregatorFactory for type[%s]", type);
throw new UnsupportedSQLQueryException("LATEST aggregator is not supported for '%s' type", type);
}
}
},
@ -129,7 +131,7 @@ public class EarliestLatestAnySqlAggregator implements SqlAggregator
case STRING:
return new StringAnyAggregatorFactory(name, fieldName, maxStringBytes);
default:
throw new ISE("Cannot build ANY aggregatorFactory for type[%s]", type);
throw new UnsupportedSQLQueryException("ANY aggregation is not supported for '%s' type", type);
}
}
};

View File

@ -22,7 +22,6 @@ package org.apache.druid.sql.calcite.aggregation.builtin;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.DoubleMaxAggregatorFactory;
@ -32,6 +31,7 @@ import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.ValueType;
import org.apache.druid.sql.calcite.aggregation.Aggregation;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
public class MaxSqlAggregator extends SimpleSqlAggregator
{
@ -73,7 +73,7 @@ public class MaxSqlAggregator extends SimpleSqlAggregator
case DOUBLE:
return new DoubleMaxAggregatorFactory(name, fieldName, expression, macroTable);
default:
throw new ISE("Cannot create aggregator factory for type[%s]", aggregationType);
throw new UnsupportedSQLQueryException("Max aggregation is not supported for '%s' type", aggregationType);
}
}
}

View File

@ -22,7 +22,6 @@ package org.apache.druid.sql.calcite.aggregation.builtin;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.DoubleMinAggregatorFactory;
@ -31,6 +30,7 @@ import org.apache.druid.query.aggregation.LongMinAggregatorFactory;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.sql.calcite.aggregation.Aggregation;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
public class MinSqlAggregator extends SimpleSqlAggregator
{
@ -69,7 +69,7 @@ public class MinSqlAggregator extends SimpleSqlAggregator
case DOUBLE:
return new DoubleMinAggregatorFactory(name, fieldName, expression, macroTable);
default:
throw new ISE("Cannot create aggregator factory for type[%s]", aggregationType);
throw new UnsupportedSQLQueryException("MIN aggregator is not supported for '%s' type", aggregationType);
}
}
}

View File

@ -22,7 +22,6 @@ package org.apache.druid.sql.calcite.aggregation.builtin;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.DoubleSumAggregatorFactory;
@ -32,6 +31,7 @@ import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.ValueType;
import org.apache.druid.sql.calcite.aggregation.Aggregation;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
public class SumSqlAggregator extends SimpleSqlAggregator
{
@ -73,7 +73,7 @@ public class SumSqlAggregator extends SimpleSqlAggregator
case DOUBLE:
return new DoubleSumAggregatorFactory(name, fieldName, expression, macroTable);
default:
throw new ISE("Cannot create aggregator factory for type[%s]", aggregationType);
throw new UnsupportedSQLQueryException("Sum aggregation is not supported for '%s' type", aggregationType);
}
}
}

View File

@ -70,8 +70,10 @@ import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.guava.BaseSequence;
import org.apache.druid.java.util.common.guava.Sequence;
import org.apache.druid.java.util.common.guava.Sequences;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.query.Query;
import org.apache.druid.query.QueryContexts;
import org.apache.druid.segment.DimensionHandlerUtils;
import org.apache.druid.server.security.Action;
import org.apache.druid.server.security.Resource;
@ -83,6 +85,7 @@ import org.apache.druid.sql.calcite.rel.DruidRel;
import org.apache.druid.sql.calcite.rel.DruidUnionRel;
import org.apache.druid.sql.calcite.run.QueryMaker;
import org.apache.druid.sql.calcite.run.QueryMakerFactory;
import org.apache.druid.utils.Throwables;
import javax.annotation.Nullable;
@ -204,20 +207,38 @@ public class DruidPlanner implements Closeable
try {
return planWithDruidConvention(rootQueryRel, parsed.getExplainNode(), parsed.getInsertNode());
}
catch (RelOptPlanner.CannotPlanException e) {
if (parsed.getInsertNode() == null) {
// Try again with BINDABLE convention. Used for querying Values and metadata tables.
try {
return planWithBindableConvention(rootQueryRel, parsed.getExplainNode());
}
catch (Exception e2) {
e.addSuppressed(e2);
throw e;
}
} else {
catch (Exception e) {
Throwable cannotPlanException = Throwables.getCauseOfType(e, RelOptPlanner.CannotPlanException.class);
if (null == cannotPlanException) {
// Not a CannotPlanningException, rethrow without trying with bindable
throw e;
}
if (parsed.getInsertNode() != null) {
// Cannot INSERT with BINDABLE.
throw e;
}
// Try again with BINDABLE convention. Used for querying Values and metadata tables.
try {
return planWithBindableConvention(rootQueryRel, parsed.getExplainNode());
}
catch (Exception e2) {
e.addSuppressed(e2);
Logger logger = log;
if (!QueryContexts.isDebug(plannerContext.getQueryContext())) {
logger = log.noStackTrace();
}
logger.warn(e, "Failed to plan the query '%s'", plannerContext.getSql());
String errorMessage = plannerContext.getPlanningError();
if (null == errorMessage && cannotPlanException instanceof UnsupportedSQLQueryException) {
errorMessage = cannotPlanException.getMessage();
}
if (null == errorMessage) {
errorMessage = "Please check broker logs for more details";
} else {
errorMessage = "Possible error: " + errorMessage;
}
throw new UnsupportedSQLQueryException(errorMessage);
}
}
}

View File

@ -25,7 +25,6 @@ import org.apache.calcite.rex.RexExecutor;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.math.expr.ExprEval;
import org.apache.druid.math.expr.ExprType;
@ -94,7 +93,7 @@ public class DruidRexExecutor implements RexExecutor
// as a primitive long/float/double.
// ExprEval.isNumericNull checks whether the parsed primitive value is null or not.
if (!constExp.getType().isNullable() && exprResult.isNumericNull()) {
throw new IAE("Illegal DATE constant: %s", constExp);
throw new UnsupportedSQLQueryException("Illegal DATE constant: %s", constExp);
}
literal = rexBuilder.makeDateLiteral(
@ -108,7 +107,7 @@ public class DruidRexExecutor implements RexExecutor
// as a primitive long/float/double.
// ExprEval.isNumericNull checks whether the parsed primitive value is null or not.
if (!constExp.getType().isNullable() && exprResult.isNumericNull()) {
throw new IAE("Illegal TIMESTAMP constant: %s", constExp);
throw new UnsupportedSQLQueryException("Illegal TIMESTAMP constant: %s", constExp);
}
literal = rexBuilder.makeTimestampLiteral(
@ -134,7 +133,7 @@ public class DruidRexExecutor implements RexExecutor
double exprResultDouble = exprResult.asDouble();
if (Double.isNaN(exprResultDouble) || Double.isInfinite(exprResultDouble)) {
String expression = druidExpression.getExpression();
throw new IAE("'%s' evaluates to '%s' that is not supported in SQL. You can either cast the expression as bigint ('cast(%s as bigint)') or char ('cast(%s as char)') or change the expression itself",
throw new UnsupportedSQLQueryException("'%s' evaluates to '%s' that is not supported in SQL. You can either cast the expression as bigint ('cast(%s as bigint)') or char ('cast(%s as char)') or change the expression itself",
expression,
Double.toString(exprResultDouble),
expression,
@ -157,7 +156,7 @@ public class DruidRexExecutor implements RexExecutor
doubleVal -> {
if (Double.isNaN(doubleVal) || Double.isInfinite(doubleVal)) {
String expression = druidExpression.getExpression();
throw new IAE(
throw new UnsupportedSQLQueryException(
"'%s' contains an element that evaluates to '%s' which is not supported in SQL. You can either cast the element in the array to bigint or char or change the expression itself",
expression,
Double.toString(doubleVal)

View File

@ -31,6 +31,7 @@ import org.apache.calcite.schema.SchemaPlus;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.Numbers;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.BaseQuery;
import org.apache.druid.server.security.Access;
@ -43,6 +44,7 @@ import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -89,6 +91,9 @@ public class PlannerContext
private Set<ResourceAction> resourceActions = null;
// result of authorizing set of resources against authentication identity
private Access authorizationResult;
// error messages encountered while planning the query
@Nullable
private String planningError;
private QueryMaker queryMaker;
private PlannerContext(
@ -252,6 +257,23 @@ public class PlannerContext
this.nativeQueryIds.add(queryId);
}
@Nullable
public String getPlanningError()
{
return planningError;
}
/**
* Sets the planning error in the context that will be shown to the user if the SQL query cannot be translated
* to a native query. This error is often a hint and thus should be phrased as such. Also, the final plan can
* be very different from SQL that user has written. So again, the error should be phrased to indicate this gap
* clearly.
*/
public void setPlanningError(String formatText, Object... arguments)
{
planningError = StringUtils.nonStrictFormat(formatText, arguments);
}
public DataContext createDataContext(final JavaTypeFactory typeFactory, List<TypedValue> parameters)
{
class DruidDataContext implements DataContext

View File

@ -0,0 +1,38 @@
/*
* 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.druid.sql.calcite.planner;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.druid.java.util.common.StringUtils;
/**
* This class is different from {@link RelOptPlanner.CannotPlanException} in that the error
* messages are user-friendly unlike its parent class. This exception class should be used instead of
* {@link org.apache.druid.java.util.common.ISE} or {@link org.apache.druid.java.util.common.IAE} when processing is
* to be halted during planning. Similarly, Druid planner can catch this exception and know that the error
* can be directly exposed to end-users.
*/
public class UnsupportedSQLQueryException extends RelOptPlanner.CannotPlanException
{
public UnsupportedSQLQueryException(String formatText, Object... arguments)
{
super(StringUtils.nonStrictFormat(formatText, arguments));
}
}

View File

@ -38,7 +38,6 @@ import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlKind;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.Pair;
import org.apache.druid.java.util.common.StringUtils;
@ -54,9 +53,11 @@ import org.apache.druid.sql.calcite.expression.Expressions;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.PlannerConfig;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.apache.druid.sql.calcite.table.RowSignatures;
import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -335,7 +336,7 @@ public class DruidJoinQueryRel extends DruidRel<DruidJoinQueryRel>
case INNER:
return JoinType.INNER;
default:
throw new IAE("Cannot handle joinType[%s]", calciteJoinType);
throw new UnsupportedSQLQueryException("Cannot handle joinType '%s'", calciteJoinType);
}
}

View File

@ -87,6 +87,7 @@ import org.apache.druid.sql.calcite.table.RowSignatures;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -405,6 +406,7 @@ public class DruidQuery
final ColumnType outputType = Calcites.getColumnTypeForRelDataType(dataType);
if (Types.isNullOr(outputType, ValueType.COMPLEX)) {
// Can't group on unknown or COMPLEX types.
plannerContext.setPlanningError("SQL requires a group-by on a column of type %s that is unsupported.", outputType);
throw new CannotBuildQueryException(aggregate, rexNode);
}
@ -515,6 +517,9 @@ public class DruidQuery
);
if (aggregation == null) {
if (null == plannerContext.getPlanningError()) {
plannerContext.setPlanningError("Aggregation [%s] is not supported", aggCall);
}
throw new CannotBuildQueryException(aggregate, aggCall);
}
@ -1149,6 +1154,8 @@ public class DruidQuery
|| orderByColumns.stream()
.anyMatch(orderBy -> !orderBy.getColumnName().equals(ColumnHolder.TIME_COLUMN_NAME)))) {
// Cannot handle this ordering.
// Scan cannot ORDER BY non-time columns.
plannerContext.setPlanningError("SQL query requires order by non-time column %s that is not supported.", orderByColumns);
return null;
}

View File

@ -124,12 +124,17 @@ public class DruidUnionDataSourceRel extends DruidRel<DruidUnionDataSourceRel>
for (final RelNode relNode : unionRel.getInputs()) {
final DruidRel<?> druidRel = (DruidRel<?>) relNode;
if (!DruidRels.isScanOrMapping(druidRel, false)) {
getPlannerContext().setPlanningError("SQL requires union between inputs that are not simple table scans " +
"and involve a filter or aliasing");
throw new CannotBuildQueryException(druidRel);
}
final DruidQuery query = druidRel.toDruidQuery(false);
final DataSource dataSource = query.getDataSource();
if (!(dataSource instanceof TableDataSource)) {
getPlannerContext().setPlanningError("SQL requires union with input of '%s' type that is not supported."
+ " Union operation is only supported between regular tables. ",
dataSource.getClass().getSimpleName());
throw new CannotBuildQueryException(druidRel);
}
@ -140,6 +145,7 @@ public class DruidUnionDataSourceRel extends DruidRel<DruidUnionDataSourceRel>
if (signature.getColumnNames().equals(query.getOutputRowSignature().getColumnNames())) {
dataSources.add((TableDataSource) dataSource);
} else {
getPlannerContext().setPlanningError("There is a mismatch between the output row signature of input tables and the row signature of union output.");
throw new CannotBuildQueryException(druidRel);
}
}

View File

@ -43,6 +43,8 @@ import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.druid.java.util.common.Pair;
import org.apache.druid.query.LookupDataSource;
import org.apache.druid.query.QueryContexts;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.rel.DruidJoinQueryRel;
import org.apache.druid.sql.calcite.rel.DruidQueryRel;
import org.apache.druid.sql.calcite.rel.DruidRel;
@ -59,12 +61,11 @@ import java.util.stream.Collectors;
public class DruidJoinRule extends RelOptRule
{
private static final DruidJoinRule INSTANCE = new DruidJoinRule(true);
private static final DruidJoinRule LEFT_SCAN_AS_QUERY = new DruidJoinRule(false);
private final boolean enableLeftScanDirect;
private final PlannerContext plannerContext;
private DruidJoinRule(final boolean enableLeftScanDirect)
private DruidJoinRule(final PlannerContext plannerContext)
{
super(
operand(
@ -73,12 +74,13 @@ public class DruidJoinRule extends RelOptRule
operand(DruidRel.class, any())
)
);
this.enableLeftScanDirect = enableLeftScanDirect;
this.enableLeftScanDirect = QueryContexts.getEnableJoinLeftScanDirect(plannerContext.getQueryContext());
this.plannerContext = plannerContext;
}
public static DruidJoinRule instance(boolean enableLeftScanDirect)
public static DruidJoinRule instance(PlannerContext plannerContext)
{
return enableLeftScanDirect ? INSTANCE : LEFT_SCAN_AS_QUERY;
return new DruidJoinRule(plannerContext);
}
@Override
@ -220,7 +222,7 @@ public class DruidJoinRule extends RelOptRule
* Returns whether {@link #analyzeCondition} would return something.
*/
@VisibleForTesting
static boolean canHandleCondition(final RexNode condition, final RelDataType leftRowType, DruidRel<?> right)
boolean canHandleCondition(final RexNode condition, final RelDataType leftRowType, DruidRel<?> right)
{
return analyzeCondition(condition, leftRowType, right).isPresent();
}
@ -229,7 +231,7 @@ public class DruidJoinRule extends RelOptRule
* If this condition is an AND of some combination of (1) literals; (2) equality conditions of the form
* {@code f(LeftRel) = RightColumn}, then return a {@link ConditionAnalysis}.
*/
private static Optional<ConditionAnalysis> analyzeCondition(
private Optional<ConditionAnalysis> analyzeCondition(
final RexNode condition,
final RelDataType leftRowType,
DruidRel<?> right
@ -265,6 +267,8 @@ public class DruidJoinRule extends RelOptRule
if (!subCondition.isA(SqlKind.EQUALS)) {
// If it's not EQUALS, it's not supported.
plannerContext.setPlanningError("SQL requires a join with '%s' condition that is not supported.",
subCondition.getKind());
return Optional.empty();
}
@ -280,6 +284,7 @@ public class DruidJoinRule extends RelOptRule
rightColumns.add((RexInputRef) operands.get(0));
} else {
// Cannot handle this condition.
plannerContext.setPlanningError("SQL is resulting in a join that have unsupported operand types.");
return Optional.empty();
}
}
@ -294,6 +299,7 @@ public class DruidJoinRule extends RelOptRule
if (distinctRightColumns > 1) {
// it means that the join's right side is lookup and the join condition contains both key and value columns of lookup.
// currently, the lookup datasource in the native engine doesn't support using value column in the join condition.
plannerContext.setPlanningError("SQL is resulting in a join involving lookup where value column is used in the condition.");
return Optional.empty();
}
}

View File

@ -25,11 +25,11 @@ import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.rel.logical.LogicalValues;
import org.apache.calcite.rex.RexLiteral;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.query.InlineDataSource;
import org.apache.druid.segment.column.RowSignature;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.apache.druid.sql.calcite.rel.DruidQueryRel;
import org.apache.druid.sql.calcite.table.DruidTable;
import org.apache.druid.sql.calcite.table.RowSignatures;
@ -120,7 +120,7 @@ public class DruidLogicalValuesRule extends RelOptRule
case TIME:
case TIME_WITH_LOCAL_TIME_ZONE:
default:
throw new IAE("Unsupported type[%s]", literal.getTypeName());
throw new UnsupportedSQLQueryException("%s type is not supported", literal.getType().getSqlTypeName());
}
}
}

View File

@ -29,7 +29,6 @@ import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.core.Sort;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.query.QueryContexts;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.rel.DruidOuterQueryRel;
import org.apache.druid.sql.calcite.rel.DruidRel;
@ -51,7 +50,6 @@ public class DruidRules
public static List<RelOptRule> rules(PlannerContext plannerContext)
{
boolean enableLeftScanDirect = QueryContexts.getEnableJoinLeftScanDirect(plannerContext.getQueryContext());
return ImmutableList.of(
new DruidQueryRule<>(
Filter.class,
@ -92,10 +90,10 @@ public class DruidRules
DruidOuterQueryRule.WHERE_FILTER,
DruidOuterQueryRule.SELECT_PROJECT,
DruidOuterQueryRule.SORT,
DruidUnionRule.instance(),
DruidUnionDataSourceRule.instance(),
new DruidUnionRule(plannerContext),
new DruidUnionDataSourceRule(plannerContext),
DruidSortUnionRule.instance(),
DruidJoinRule.instance(enableLeftScanDirect)
DruidJoinRule.instance(plannerContext)
);
}

View File

@ -27,6 +27,7 @@ import org.apache.calcite.util.mapping.Mappings;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.query.TableDataSource;
import org.apache.druid.segment.column.RowSignature;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.rel.DruidQueryRel;
import org.apache.druid.sql.calcite.rel.DruidRel;
import org.apache.druid.sql.calcite.rel.DruidRels;
@ -34,7 +35,10 @@ import org.apache.druid.sql.calcite.rel.DruidUnionDataSourceRel;
import org.apache.druid.sql.calcite.rel.PartialDruidQuery;
import org.apache.druid.sql.calcite.table.DruidTable;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -44,9 +48,9 @@ import java.util.Optional;
*/
public class DruidUnionDataSourceRule extends RelOptRule
{
private static final DruidUnionDataSourceRule INSTANCE = new DruidUnionDataSourceRule();
private final PlannerContext plannerContext;
private DruidUnionDataSourceRule()
public DruidUnionDataSourceRule(final PlannerContext plannerContext)
{
super(
operand(
@ -55,11 +59,7 @@ public class DruidUnionDataSourceRule extends RelOptRule
operand(DruidQueryRel.class, none())
)
);
}
public static DruidUnionDataSourceRule instance()
{
return INSTANCE;
this.plannerContext = plannerContext;
}
@Override
@ -69,7 +69,7 @@ public class DruidUnionDataSourceRule extends RelOptRule
final DruidRel<?> firstDruidRel = call.rel(1);
final DruidQueryRel secondDruidRel = call.rel(2);
return isCompatible(unionRel, firstDruidRel, secondDruidRel);
return isCompatible(unionRel, firstDruidRel, secondDruidRel, plannerContext);
}
@Override
@ -90,7 +90,7 @@ public class DruidUnionDataSourceRule extends RelOptRule
call.transformTo(
DruidUnionDataSourceRel.create(
(Union) newUnionRel,
getColumnNamesIfTableOrUnion(firstDruidRel).get(),
getColumnNamesIfTableOrUnion(firstDruidRel, plannerContext).get(),
firstDruidRel.getPlannerContext()
)
);
@ -103,7 +103,7 @@ public class DruidUnionDataSourceRule extends RelOptRule
call.transformTo(
DruidUnionDataSourceRel.create(
unionRel,
getColumnNamesIfTableOrUnion(firstDruidRel).get(),
getColumnNamesIfTableOrUnion(firstDruidRel, plannerContext).get(),
firstDruidRel.getPlannerContext()
)
);
@ -112,22 +112,39 @@ public class DruidUnionDataSourceRule extends RelOptRule
// Can only do UNION ALL of inputs that have compatible schemas (or schema mappings) and right side
// is a simple table scan
public static boolean isCompatible(final Union unionRel, final DruidRel<?> first, final DruidRel<?> second)
public static boolean isCompatible(final Union unionRel, final DruidRel<?> first, final DruidRel<?> second, @Nullable PlannerContext plannerContext)
{
if (!(second instanceof DruidQueryRel)) {
return false;
}
return unionRel.all && isUnionCompatible(first, second);
if (!unionRel.all && null != plannerContext) {
plannerContext.setPlanningError("SQL requires 'UNION' but only 'UNION ALL' is supported.");
}
return unionRel.all && isUnionCompatible(first, second, plannerContext);
}
private static boolean isUnionCompatible(final DruidRel<?> first, final DruidRel<?> second)
private static boolean isUnionCompatible(final DruidRel<?> first, final DruidRel<?> second, @Nullable PlannerContext plannerContext)
{
final Optional<List<String>> columnNames = getColumnNamesIfTableOrUnion(first);
return columnNames.isPresent() && columnNames.equals(getColumnNamesIfTableOrUnion(second));
final Optional<List<String>> firstColumnNames = getColumnNamesIfTableOrUnion(first, plannerContext);
final Optional<List<String>> secondColumnNames = getColumnNamesIfTableOrUnion(second, plannerContext);
if (!firstColumnNames.isPresent() || !secondColumnNames.isPresent()) {
// No need to set the planning error here
return false;
}
if (!firstColumnNames.equals(secondColumnNames)) {
if (null != plannerContext) {
plannerContext.setPlanningError("SQL requires union between two tables and column names queried for " +
"each table are different Left: %s, Right: %s.",
firstColumnNames.orElse(Collections.emptyList()),
secondColumnNames.orElse(Collections.emptyList()));
}
return false;
}
return true;
}
static Optional<List<String>> getColumnNamesIfTableOrUnion(final DruidRel<?> druidRel)
static Optional<List<String>> getColumnNamesIfTableOrUnion(final DruidRel<?> druidRel, @Nullable PlannerContext plannerContext)
{
final PartialDruidQuery partialQuery = druidRel.getPartialDruidQuery();
@ -171,7 +188,17 @@ public class DruidUnionDataSourceRule extends RelOptRule
// This rel is a union itself.
return Optional.of(((DruidUnionDataSourceRel) druidRel).getUnionColumnNames());
} else if (druidTable.isPresent()) {
if (null != plannerContext) {
plannerContext.setPlanningError("SQL requires union between inputs that are not simple table scans " +
"and involve a filter or aliasing. Or column types of tables being unioned are not of same type.");
}
return Optional.empty();
} else {
if (null != plannerContext) {
plannerContext.setPlanningError("SQL requires union with input of a datasource type that is not supported."
+ " Union operation is only supported between regular tables. ");
}
return Optional.empty();
}
}

View File

@ -23,6 +23,7 @@ import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Union;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.rel.DruidRel;
import org.apache.druid.sql.calcite.rel.DruidUnionRel;
@ -33,9 +34,9 @@ import java.util.List;
*/
public class DruidUnionRule extends RelOptRule
{
private static final DruidUnionRule INSTANCE = new DruidUnionRule();
private final PlannerContext plannerContext;
private DruidUnionRule()
public DruidUnionRule(PlannerContext plannerContext)
{
super(
operand(
@ -44,11 +45,7 @@ public class DruidUnionRule extends RelOptRule
operand(DruidRel.class, none())
)
);
}
public static DruidUnionRule instance()
{
return INSTANCE;
this.plannerContext = plannerContext;
}
@Override
@ -58,7 +55,7 @@ public class DruidUnionRule extends RelOptRule
final Union unionRel = call.rel(0);
final DruidRel<?> firstDruidRel = call.rel(1);
final DruidRel<?> secondDruidRel = call.rel(2);
return !DruidUnionDataSourceRule.isCompatible(unionRel, firstDruidRel, secondDruidRel);
return !DruidUnionDataSourceRule.isCompatible(unionRel, firstDruidRel, secondDruidRel, null);
}
@Override
@ -78,6 +75,8 @@ public class DruidUnionRule extends RelOptRule
-1
)
);
} else {
plannerContext.setPlanningError("SQL requires 'UNION' but only 'UNION ALL' is supported.");
}
}
}

View File

@ -213,7 +213,7 @@ public class SqlResource
final Throwable exceptionToReport;
if (e instanceof RelOptPlanner.CannotPlanException) {
exceptionToReport = new ISE("Cannot build plan for query: %s", sqlQuery.getQuery());
exceptionToReport = new ISE("Cannot build plan for query: %s. %s", sqlQuery.getQuery(), e.getMessage());
} else {
exceptionToReport = e;
}

View File

@ -489,13 +489,13 @@ public class BaseCalciteQueryTest extends CalciteTestBase
@Rule
public QueryLogHook getQueryLogHook()
{
return queryLogHook = QueryLogHook.create(createQueryJsonMapper());
queryJsonMapper = createQueryJsonMapper();
return queryLogHook = QueryLogHook.create(queryJsonMapper);
}
@Before
public void setUp() throws Exception
{
queryJsonMapper = createQueryJsonMapper();
walker = createQuerySegmentWalker();
// also register the static injected mapper, though across multiple test runs
@ -571,12 +571,12 @@ public class BaseCalciteQueryTest extends CalciteTestBase
return modules;
}
public void assertQueryIsUnplannable(final String sql)
public void assertQueryIsUnplannable(final String sql, String expectedError)
{
assertQueryIsUnplannable(PLANNER_CONFIG_DEFAULT, sql);
assertQueryIsUnplannable(PLANNER_CONFIG_DEFAULT, sql, expectedError);
}
public void assertQueryIsUnplannable(final PlannerConfig plannerConfig, final String sql)
public void assertQueryIsUnplannable(final PlannerConfig plannerConfig, final String sql, String expectedError)
{
Exception e = null;
try {
@ -590,6 +590,7 @@ public class BaseCalciteQueryTest extends CalciteTestBase
log.error(e, "Expected CannotPlanException for query: %s", sql);
Assert.fail(sql);
}
Assert.assertEquals(sql, expectedError, e.getMessage());
}
/**

View File

@ -3118,7 +3118,7 @@ public class CalciteJoinQueryTest extends BaseCalciteQueryTest
// This query is expected to fail as we do not support join with constant in the on condition
// (see issue https://github.com/apache/druid/issues/9942 for more information)
// TODO: Remove expected Exception when https://github.com/apache/druid/issues/9942 is fixed
@Test(expected = RelOptPlanner.CannotPlanException.class)
@Test
@Parameters(source = QueryContextForJoinProvider.class)
public void testJoinOnConstantShouldFail(Map<String, Object> queryContext) throws Exception
{
@ -3126,12 +3126,19 @@ public class CalciteJoinQueryTest extends BaseCalciteQueryTest
final String query = "SELECT t1.dim1 from foo as t1 LEFT JOIN foo as t2 on t1.dim1 = '10.1'";
testQuery(
query,
queryContext,
ImmutableList.of(),
ImmutableList.of()
);
try {
testQuery(
query,
queryContext,
ImmutableList.of(),
ImmutableList.of()
);
}
catch (RelOptPlanner.CannotPlanException cpe) {
Assert.assertEquals(cpe.getMessage(), "Possible error: SQL is resulting in a join that have unsupported operand types.");
return;
}
Assert.fail("Expected an exception of type: " + RelOptPlanner.CannotPlanException.class);
}
@Test

View File

@ -27,7 +27,6 @@ import org.apache.calcite.plan.RelOptPlanner;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.HumanReadableBytes;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.JodaUtils;
import org.apache.druid.java.util.common.StringUtils;
@ -112,6 +111,7 @@ import org.apache.druid.sql.calcite.filtration.Filtration;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.PlannerConfig;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.apache.druid.sql.calcite.rel.CannotBuildQueryException;
import org.apache.druid.sql.calcite.util.CalciteTests;
import org.joda.time.DateTime;
@ -2606,8 +2606,9 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
+ "dim3, dim2, SUM(m1), COUNT(*)\n"
+ "FROM (SELECT dim3, dim2, m1 FROM foo2 UNION ALL SELECT dim3, dim2, m1 FROM foo)\n"
+ "WHERE dim2 = 'a' OR dim2 = 'en'\n"
+ "GROUP BY 1, 2"
);
+ "GROUP BY 1, 2",
"Possible error: SQL requires union between inputs that are not simple table scans and involve a " +
"filter or aliasing. Or column types of tables being unioned are not of same type.");
}
@Test
@ -2620,7 +2621,20 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
+ "c, COUNT(*)\n"
+ "FROM (SELECT dim1 AS c, m1 FROM foo UNION ALL SELECT dim2 AS c, m1 FROM numfoo)\n"
+ "WHERE c = 'a' OR c = 'def'\n"
+ "GROUP BY 1"
+ "GROUP BY 1",
"Possible error: SQL requires union between two tables " +
"and column names queried for each table are different Left: [dim1], Right: [dim2]."
);
}
@Test
public void testUnionIsUnplannable()
{
// Cannot plan this UNION operation
assertQueryIsUnplannable(
"SELECT dim2, dim1, m1 FROM foo2 UNION SELECT dim1, dim2, m1 FROM foo",
"Possible error: SQL requires 'UNION' but only 'UNION ALL' is supported."
);
}
@ -2634,7 +2648,9 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
+ "c, COUNT(*)\n"
+ "FROM (SELECT dim1 AS c, m1 FROM foo UNION ALL SELECT cnt AS c, m1 FROM numfoo)\n"
+ "WHERE c = 'a' OR c = 'def'\n"
+ "GROUP BY 1"
+ "GROUP BY 1",
"Possible error: SQL requires union between inputs that are not simple table scans and involve " +
"a filter or aliasing. Or column types of tables being unioned are not of same type."
);
}
@ -2732,7 +2748,8 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
+ "dim1, dim2, SUM(m1), COUNT(*)\n"
+ "FROM (SELECT dim1, dim2, m1 FROM foo UNION ALL SELECT dim2, dim1, m1 FROM foo)\n"
+ "WHERE dim2 = 'a' OR dim2 = 'def'\n"
+ "GROUP BY 1, 2"
+ "GROUP BY 1, 2",
"Possible error: SQL requires union between two tables and column names queried for each table are different Left: [dim1, dim2, m1], Right: [dim2, dim1, m1]."
);
}
@ -3629,31 +3646,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
);
}
@Test
public void testUnplannableQueries()
{
// All of these queries are unplannable because they rely on features Druid doesn't support.
// This test is here to confirm that we don't fall back to Calcite's interpreter or enumerable implementation.
// It's also here so when we do support these features, we can have "real" tests for these queries.
final List<String> queries = ImmutableList.of(
// SELECT query with order by non-__time.
"SELECT dim1 FROM druid.foo ORDER BY dim1",
// JOIN condition with not-equals (<>).
"SELECT foo.dim1, foo.dim2, l.k, l.v\n"
+ "FROM foo INNER JOIN lookup.lookyloo l ON foo.dim2 <> l.k",
// JOIN condition with a function of both sides.
"SELECT foo.dim1, foo.dim2, l.k, l.v\n"
+ "FROM foo INNER JOIN lookup.lookyloo l ON CHARACTER_LENGTH(foo.dim2 || l.k) > 3\n"
);
for (final String query : queries) {
assertQueryIsUnplannable(query);
}
}
@Test
public void testGroupingWithNullInFilter() throws Exception
{
@ -3764,30 +3756,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
);
}
@Test
public void testUnplannableTwoExactCountDistincts()
{
// Requires GROUPING SETS + GROUPING to be translated by AggregateExpandDistinctAggregatesRule.
assertQueryIsUnplannable(
PLANNER_CONFIG_NO_HLL,
"SELECT dim2, COUNT(distinct dim1), COUNT(distinct dim2) FROM druid.foo GROUP BY dim2"
);
}
@Test
public void testUnplannableExactCountDistinctOnSketch()
{
// COUNT DISTINCT on a sketch cannot be exact.
assertQueryIsUnplannable(
PLANNER_CONFIG_NO_HLL,
"SELECT COUNT(distinct unique_dim1) FROM druid.foo"
);
}
@Test
public void testGroupByNothingWithLiterallyFalseFilter() throws Exception
{
@ -5186,6 +5154,35 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
);
}
@Test
public void testUnplannableQueries()
{
// All of these queries are unplannable because they rely on features Druid doesn't support.
// This test is here to confirm that we don't fall back to Calcite's interpreter or enumerable implementation.
// It's also here so when we do support these features, we can have "real" tests for these queries.
final Map<String, String> queries = ImmutableMap.of(
// SELECT query with order by non-__time.
"SELECT dim1 FROM druid.foo ORDER BY dim1",
"Possible error: SQL query requires order by non-time column [dim1 ASC] that is not supported.",
// JOIN condition with not-equals (<>).
"SELECT foo.dim1, foo.dim2, l.k, l.v\n"
+ "FROM foo INNER JOIN lookup.lookyloo l ON foo.dim2 <> l.k",
"Possible error: SQL requires a join with 'NOT_EQUALS' condition that is not supported.",
// JOIN condition with a function of both sides.
"SELECT foo.dim1, foo.dim2, l.k, l.v\n"
+ "FROM foo INNER JOIN lookup.lookyloo l ON CHARACTER_LENGTH(foo.dim2 || l.k) > 3\n",
"Possible error: SQL requires a join with 'GREATER_THAN' condition that is not supported."
);
for (final Map.Entry<String, String> queryErrorPair : queries.entrySet()) {
assertQueryIsUnplannable(queryErrorPair.getKey(), queryErrorPair.getValue());
}
}
@Test
public void testCountStarWithBoundFilterSimplifyOnMetric() throws Exception
{
@ -5228,6 +5225,30 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
);
}
@Test
public void testUnplannableTwoExactCountDistincts()
{
// Requires GROUPING SETS + GROUPING to be translated by AggregateExpandDistinctAggregatesRule.
assertQueryIsUnplannable(
PLANNER_CONFIG_NO_HLL,
"SELECT dim2, COUNT(distinct dim1), COUNT(distinct dim2) FROM druid.foo GROUP BY dim2",
"Possible error: SQL requires a join with 'IS_NOT_DISTINCT_FROM' condition that is not supported."
);
}
@Test
public void testUnplannableExactCountDistinctOnSketch()
{
// COUNT DISTINCT on a sketch cannot be exact.
assertQueryIsUnplannable(
PLANNER_CONFIG_NO_HLL,
"SELECT COUNT(distinct unique_dim1) FROM druid.foo",
"Possible error: SQL requires a group-by on a column of type COMPLEX<hyperUnique> that is unsupported."
);
}
@Test
public void testCountStarWithBoundFilterSimplifyAnd() throws Exception
{
@ -5392,9 +5413,9 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
}
catch (Throwable t) {
Throwable rootException = CalciteTests.getRootCauseFromInvocationTargetExceptionChain(t);
Assert.assertEquals(IAE.class, rootException.getClass());
Assert.assertEquals(UnsupportedSQLQueryException.class, rootException.getClass());
Assert.assertEquals(
"Illegal TIMESTAMP constant: CAST('z2000-01-01 00:00:00'):TIMESTAMP(3) NOT NULL",
"Possible error: Illegal TIMESTAMP constant: CAST('z2000-01-01 00:00:00'):TIMESTAMP(3) NOT NULL",
rootException.getMessage()
);
}
@ -11920,8 +11941,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
);
}
@Test
public void testRepeatedIdenticalVirtualExpressionGrouping() throws Exception
{

View File

@ -30,8 +30,11 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.sql.calcite.planner.DruidTypeSystem;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import java.math.BigDecimal;
import java.util.List;
@ -56,12 +59,21 @@ public class DruidJoinRuleTest
),
ImmutableList.of("left", "right")
);
private DruidJoinRule druidJoinRule;
@Before
public void setup()
{
PlannerContext plannerContext = Mockito.mock(PlannerContext.class);
druidJoinRule = DruidJoinRule.instance(plannerContext);
}
@Test
public void test_canHandleCondition_leftEqRight()
{
Assert.assertTrue(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeCall(
SqlStdOperatorTable.EQUALS,
rexBuilder.makeInputRef(joinType, 0),
@ -77,7 +89,7 @@ public class DruidJoinRuleTest
public void test_canHandleCondition_leftFnEqRight()
{
Assert.assertTrue(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeCall(
SqlStdOperatorTable.EQUALS,
rexBuilder.makeCall(
@ -97,7 +109,7 @@ public class DruidJoinRuleTest
public void test_canHandleCondition_leftEqRightFn()
{
Assert.assertFalse(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeCall(
SqlStdOperatorTable.EQUALS,
rexBuilder.makeInputRef(typeFactory.createSqlType(SqlTypeName.VARCHAR), 0),
@ -117,7 +129,7 @@ public class DruidJoinRuleTest
public void test_canHandleCondition_leftEqLeft()
{
Assert.assertFalse(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeCall(
SqlStdOperatorTable.EQUALS,
rexBuilder.makeInputRef(typeFactory.createSqlType(SqlTypeName.VARCHAR), 0),
@ -133,7 +145,7 @@ public class DruidJoinRuleTest
public void test_canHandleCondition_rightEqRight()
{
Assert.assertFalse(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeCall(
SqlStdOperatorTable.EQUALS,
rexBuilder.makeInputRef(typeFactory.createSqlType(SqlTypeName.VARCHAR), 1),
@ -149,7 +161,7 @@ public class DruidJoinRuleTest
public void test_canHandleCondition_true()
{
Assert.assertTrue(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeLiteral(true),
leftType,
null
@ -161,7 +173,7 @@ public class DruidJoinRuleTest
public void test_canHandleCondition_false()
{
Assert.assertTrue(
DruidJoinRule.canHandleCondition(
druidJoinRule.canHandleCondition(
rexBuilder.makeLiteral(false),
leftType,
null

View File

@ -31,6 +31,7 @@ import org.apache.calcite.util.TimestampString;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.sql.calcite.planner.DruidTypeSystem;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.Assert;
@ -164,8 +165,8 @@ public class DruidLogicalValuesRuleTest
new TimestampString("2021-04-01 16:54:31"),
0
);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Unsupported type[TIMESTAMP_WITH_LOCAL_TIME_ZONE]");
expectedException.expect(UnsupportedSQLQueryException.class);
expectedException.expectMessage("TIMESTAMP_WITH_LOCAL_TIME_ZONE type is not supported");
DruidLogicalValuesRule.getValueFromLiteral(literal, DEFAULT_CONTEXT);
}
@ -173,8 +174,8 @@ public class DruidLogicalValuesRuleTest
public void testGetValueFromTimeLiteral()
{
RexLiteral literal = REX_BUILDER.makeTimeLiteral(new TimeString("16:54:31"), 0);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Unsupported type[TIME]");
expectedException.expect(UnsupportedSQLQueryException.class);
expectedException.expectMessage("TIME type is not supported");
DruidLogicalValuesRule.getValueFromLiteral(literal, DEFAULT_CONTEXT);
}
@ -182,8 +183,8 @@ public class DruidLogicalValuesRuleTest
public void testGetValueFromTimeWithLocalTimeZoneLiteral()
{
RexLiteral literal = REX_BUILDER.makeTimeWithLocalTimeZoneLiteral(new TimeString("16:54:31"), 0);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Unsupported type[TIME_WITH_LOCAL_TIME_ZONE]");
expectedException.expect(UnsupportedSQLQueryException.class);
expectedException.expectMessage("TIME_WITH_LOCAL_TIME_ZONE type is not supported");
DruidLogicalValuesRule.getValueFromLiteral(literal, DEFAULT_CONTEXT);
}
}

View File

@ -68,7 +68,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.of(ImmutableList.of("__time", "col1", "col2")),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -85,7 +85,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.of(ImmutableList.of("col1")),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -102,7 +102,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.empty(),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -119,7 +119,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.empty(),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -137,7 +137,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.of(ImmutableList.of("__time", "col1", "col2")),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -164,7 +164,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.of(ImmutableList.of("col2", "col1")),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -182,7 +182,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.of(ImmutableList.of("__time", "col1", "col2")),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -199,7 +199,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.empty(),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
@ -216,7 +216,7 @@ public class DruidUnionDataSourceRuleTest
Assert.assertEquals(
Optional.empty(),
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel)
DruidUnionDataSourceRule.getColumnNamesIfTableOrUnion(druidRel, null)
);
}
}