Better error message for date_trunc operators (#15759)

IAEs are not bubbled up and show up as a runtime failure to the user which are not helpful. See https://apachedruidworkspace.slack.com/archives/C0303FDCZEZ/p1706185796975109 for one such example. This change will fix that.
This commit is contained in:
Abhishek Agarwal 2024-01-27 11:22:39 +05:30 committed by GitHub
parent f58fd5b75f
commit 989a8f7874
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 50 additions and 50 deletions

View File

@ -19,6 +19,12 @@
package org.apache.druid.error;
/**
* 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. There is wiring in place to bubble up the error message to the user when wrapped
* in this exception class.
*/
public class InvalidSqlInput extends InvalidInput
{
public static DruidException exception(String msg, Object... args)

View File

@ -27,7 +27,7 @@ import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.error.InvalidSqlInput;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.segment.column.RowSignature;
@ -67,6 +67,7 @@ public class DateTruncOperatorConversion implements SqlOperatorConversion
.operatorBuilder("DATE_TRUNC")
.operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.TIMESTAMP)
.requiredOperandCount(2)
.literalOperands(0)
.returnTypeCascadeNullable(SqlTypeName.TIMESTAMP)
.functionCategory(SqlFunctionCategory.TIMEDATE)
.build();
@ -92,15 +93,15 @@ public class DateTruncOperatorConversion implements SqlOperatorConversion
final DruidExpression arg = inputExpressions.get(1);
final Expr truncTypeExpr = plannerContext.parseExpression(inputExpressions.get(0).getExpression());
if (!truncTypeExpr.isLiteral()) {
throw new IAE("Operator[%s] truncType must be a literal", calciteOperator().getName());
}
final String truncType = (String) truncTypeExpr.getLiteralValue();
final Period truncPeriod = TRUNC_PERIOD_MAP.get(StringUtils.toLowerCase(truncType));
if (truncPeriod == null) {
throw new IAE("Operator[%s] cannot truncate to[%s]", calciteOperator().getName(), truncType);
throw InvalidSqlInput.exception(
"Operator[%s] cannot truncate to[%s]",
calciteOperator().getName(),
truncType
);
}
return DruidExpression.ofFunctionCall(

View File

@ -239,6 +239,17 @@ public abstract class QueryHandler extends SqlStatementHandler.BaseStatementHand
if (de != null) {
throw de;
}
// Exceptions during rule evaluations could be wrapped inside a RuntimeException by VolcanoRuleCall class.
// This block will extract a user-friendly message from the exception chain.
if (e.getMessage() != null
&& e.getCause() != null
&& e.getCause().getMessage() != null
&& e.getMessage().startsWith("Error while applying rule")) {
throw DruidException.forPersona(DruidException.Persona.ADMIN)
.ofCategory(DruidException.Category.UNCATEGORIZED)
.build(e, "%s", e.getCause().getMessage());
}
throw DruidPlanner.translateException(e);
}
catch (Exception e) {
@ -681,9 +692,6 @@ public abstract class QueryHandler extends SqlStatementHandler.BaseStatementHand
private DruidException buildSQLPlanningError(RelOptPlanner.CannotPlanException exception)
{
String errorMessage = handlerContext.plannerContext().getPlanningError();
if (null == errorMessage && exception instanceof UnsupportedSQLQueryException) {
errorMessage = exception.getMessage();
}
if (errorMessage == null) {
throw DruidException.forPersona(DruidException.Persona.OPERATOR)
.ofCategory(DruidException.Category.UNSUPPORTED)

View File

@ -1,38 +0,0 @@
/*
* 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

@ -14759,6 +14759,30 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
)
);
}
@Test
public void testGroupByDateTrunc()
{
testQuery(
"select DATE_TRUNC('HOUR', __time), COUNT(*) from druid.foo group by DATE_TRUNC('HOUR', __time)",
ImmutableList.of(
Druids.newTimeseriesQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(querySegmentSpec(Filtration.eternity()))
.granularity(Granularities.HOUR)
.aggregators(aggregators(new CountAggregatorFactory("a0")))
.context(QUERY_CONTEXT_DEFAULT)
.build()
),
ImmutableList.of(
new Object[]{946684800000L, 1L},
new Object[]{946771200000L, 1L},
new Object[]{946857600000L, 1L},
new Object[]{978307200000L, 1L},
new Object[]{978393600000L, 1L},
new Object[]{978480000000L, 1L}
)
);
}
@Test
public void testLatestByOnStringColumnWithoutMaxBytesSpecified()

View File

@ -98,7 +98,6 @@ import org.apache.druid.sql.calcite.planner.PlannerConfig;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.PlannerFactory;
import org.apache.druid.sql.calcite.planner.PlannerResult;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
import org.apache.druid.sql.calcite.run.NativeSqlEngine;
import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog;
import org.apache.druid.sql.calcite.util.CalciteTestBase;
@ -1399,12 +1398,12 @@ public class SqlResourceTest extends CalciteTestBase
}
/**
* This test is for {@link UnsupportedSQLQueryException} exceptions that are thrown by druid rules during query
* This test is for {@link org.apache.druid.error.InvalidSqlInput} exceptions that are thrown by druid rules during query
* planning. e.g. doing max aggregation on string type. The test checks that the API returns correct error messages
* for such planning errors.
*/
@Test
public void testCannotConvert_UnsupportedSQLQueryException() throws Exception
public void testCannotConvert_InvalidSQL() throws Exception
{
// max(string) unsupported
ErrorResponse errorResponse = postSyncForException(