mirror of https://github.com/apache/druid.git
Fix NPE for SQL queries when a query parameter is missing in the mid (#11900)
* Fix NPE for SQL queries when a query parameter is missing in the mid * checkstyle * Throw SqlPlanningException instead of IAE
This commit is contained in:
parent
14b0b4aee2
commit
13bec7468a
|
@ -21,6 +21,7 @@ package org.apache.druid.tests.query;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import org.apache.calcite.avatica.AvaticaSqlException;
|
||||||
import org.apache.druid.https.SSLClientConfig;
|
import org.apache.druid.https.SSLClientConfig;
|
||||||
import org.apache.druid.java.util.common.StringUtils;
|
import org.apache.druid.java.util.common.StringUtils;
|
||||||
import org.apache.druid.java.util.common.logger.Logger;
|
import org.apache.druid.java.util.common.logger.Logger;
|
||||||
|
@ -163,7 +164,7 @@ public class ITJdbcQueryTest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch (SQLException throwables) {
|
catch (SQLException throwables) {
|
||||||
Assert.assertFalse(true, throwables.getMessage());
|
Assert.fail(throwables.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +186,7 @@ public class ITJdbcQueryTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (SQLException throwables) {
|
catch (SQLException throwables) {
|
||||||
Assert.assertFalse(true, throwables.getMessage());
|
Assert.fail(throwables.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,7 +209,18 @@ public class ITJdbcQueryTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (SQLException throwables) {
|
catch (SQLException throwables) {
|
||||||
Assert.assertFalse(true, throwables.getMessage());
|
Assert.fail(throwables.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expectedExceptions = AvaticaSqlException.class, expectedExceptionsMessageRegExp = ".* Parameter at position\\[0] is not bound")
|
||||||
|
public void testJdbcPrepareStatementQueryMissingParameters() throws SQLException
|
||||||
|
{
|
||||||
|
for (String url : connections) {
|
||||||
|
try (Connection connection = DriverManager.getConnection(url, connectionProperties);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(QUERY_PARAMETERIZED);
|
||||||
|
ResultSet resultSet = statement.executeQuery()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class SqlPlanningException extends BadQueryException
|
||||||
this(PlanningError.VALIDATION_ERROR, e.getMessage());
|
this(PlanningError.VALIDATION_ERROR, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqlPlanningException(PlanningError planningError, String errorMessage)
|
public SqlPlanningException(PlanningError planningError, String errorMessage)
|
||||||
{
|
{
|
||||||
this(planningError.errorCode, errorMessage, planningError.errorClass);
|
this(planningError.errorCode, errorMessage, planningError.errorClass);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,9 @@ import org.apache.calcite.rex.RexDynamicParam;
|
||||||
import org.apache.calcite.rex.RexNode;
|
import org.apache.calcite.rex.RexNode;
|
||||||
import org.apache.calcite.rex.RexShuttle;
|
import org.apache.calcite.rex.RexShuttle;
|
||||||
import org.apache.calcite.sql.type.SqlTypeName;
|
import org.apache.calcite.sql.type.SqlTypeName;
|
||||||
import org.apache.druid.java.util.common.ISE;
|
import org.apache.druid.java.util.common.StringUtils;
|
||||||
|
import org.apache.druid.sql.SqlPlanningException;
|
||||||
|
import org.apache.druid.sql.SqlPlanningException.PlanningError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverse {@link RelNode} tree and replaces all {@link RexDynamicParam} with {@link org.apache.calcite.rex.RexLiteral}
|
* Traverse {@link RelNode} tree and replaces all {@link RexDynamicParam} with {@link org.apache.calcite.rex.RexLiteral}
|
||||||
|
@ -198,6 +200,12 @@ public class RelParameterizerShuttle implements RelShuttle
|
||||||
// if we have a value for dynamic parameter, replace with a literal, else add to list of unbound parameters
|
// if we have a value for dynamic parameter, replace with a literal, else add to list of unbound parameters
|
||||||
if (plannerContext.getParameters().size() > dynamicParam.getIndex()) {
|
if (plannerContext.getParameters().size() > dynamicParam.getIndex()) {
|
||||||
TypedValue param = plannerContext.getParameters().get(dynamicParam.getIndex());
|
TypedValue param = plannerContext.getParameters().get(dynamicParam.getIndex());
|
||||||
|
if (param == null) {
|
||||||
|
throw new SqlPlanningException(
|
||||||
|
PlanningError.VALIDATION_ERROR,
|
||||||
|
StringUtils.format("Parameter at position[%s] is not bound", dynamicParam.getIndex())
|
||||||
|
);
|
||||||
|
}
|
||||||
if (param.value == null) {
|
if (param.value == null) {
|
||||||
return builder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.NULL));
|
return builder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.NULL));
|
||||||
}
|
}
|
||||||
|
@ -208,7 +216,10 @@ public class RelParameterizerShuttle implements RelShuttle
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new ISE("Parameter: [%s] is not bound", dynamicParam.getName());
|
throw new SqlPlanningException(
|
||||||
|
PlanningError.VALIDATION_ERROR,
|
||||||
|
StringUtils.format("Parameter at position[%s] is not bound", dynamicParam.getIndex())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.apache.calcite.sql.SqlNode;
|
||||||
import org.apache.calcite.sql.type.SqlTypeName;
|
import org.apache.calcite.sql.type.SqlTypeName;
|
||||||
import org.apache.calcite.sql.util.SqlShuttle;
|
import org.apache.calcite.sql.util.SqlShuttle;
|
||||||
import org.apache.calcite.util.TimestampString;
|
import org.apache.calcite.util.TimestampString;
|
||||||
|
import org.apache.druid.java.util.common.IAE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces all {@link SqlDynamicParam} encountered in an {@link SqlNode} tree with a {@link SqlLiteral} if a value
|
* Replaces all {@link SqlDynamicParam} encountered in an {@link SqlNode} tree with a {@link SqlLiteral} if a value
|
||||||
|
@ -51,6 +52,9 @@ public class SqlParameterizerShuttle extends SqlShuttle
|
||||||
try {
|
try {
|
||||||
if (plannerContext.getParameters().size() > param.getIndex()) {
|
if (plannerContext.getParameters().size() > param.getIndex()) {
|
||||||
TypedValue paramBinding = plannerContext.getParameters().get(param.getIndex());
|
TypedValue paramBinding = plannerContext.getParameters().get(param.getIndex());
|
||||||
|
if (paramBinding == null) {
|
||||||
|
throw new IAE("Parameter at position[%s] is not bound", param.getIndex());
|
||||||
|
}
|
||||||
if (paramBinding.value == null) {
|
if (paramBinding.value == null) {
|
||||||
return SqlLiteral.createNull(param.getParserPosition());
|
return SqlLiteral.createNull(param.getParserPosition());
|
||||||
}
|
}
|
||||||
|
@ -67,6 +71,8 @@ public class SqlParameterizerShuttle extends SqlShuttle
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeName.createLiteral(paramBinding.value, param.getParserPosition());
|
return typeName.createLiteral(paramBinding.value, param.getParserPosition());
|
||||||
|
} else {
|
||||||
|
throw new IAE("Parameter at position[%s] is not bound", param.getIndex());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ClassCastException ignored) {
|
catch (ClassCastException ignored) {
|
||||||
|
|
|
@ -36,7 +36,10 @@ public class SqlQuery
|
||||||
public static List<TypedValue> getParameterList(List<SqlParameter> parameters)
|
public static List<TypedValue> getParameterList(List<SqlParameter> parameters)
|
||||||
{
|
{
|
||||||
return parameters.stream()
|
return parameters.stream()
|
||||||
.map(SqlParameter::getTypedValue)
|
// null params are not good!
|
||||||
|
// we pass them to the planner, so that it can generate a proper error message.
|
||||||
|
// see SqlParameterizerShuttle and RelParameterizerShuttle.
|
||||||
|
.map(p -> p == null ? null : p.getTypedValue())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,12 +38,16 @@ import org.apache.druid.query.scan.ScanQuery;
|
||||||
import org.apache.druid.query.scan.ScanQuery.ResultFormat;
|
import org.apache.druid.query.scan.ScanQuery.ResultFormat;
|
||||||
import org.apache.druid.segment.column.ColumnType;
|
import org.apache.druid.segment.column.ColumnType;
|
||||||
import org.apache.druid.segment.column.RowSignature;
|
import org.apache.druid.segment.column.RowSignature;
|
||||||
|
import org.apache.druid.sql.SqlPlanningException;
|
||||||
import org.apache.druid.sql.calcite.filtration.Filtration;
|
import org.apache.druid.sql.calcite.filtration.Filtration;
|
||||||
import org.apache.druid.sql.calcite.util.CalciteTests;
|
import org.apache.druid.sql.calcite.util.CalciteTests;
|
||||||
import org.apache.druid.sql.http.SqlParameter;
|
import org.apache.druid.sql.http.SqlParameter;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class has copied a subset of the tests in {@link CalciteQueryTest} and replaced various parts of queries with
|
* This class has copied a subset of the tests in {@link CalciteQueryTest} and replaced various parts of queries with
|
||||||
* dynamic parameters. It is NOT important that this file remains in sync with {@link CalciteQueryTest}, the tests
|
* dynamic parameters. It is NOT important that this file remains in sync with {@link CalciteQueryTest}, the tests
|
||||||
|
@ -575,8 +579,8 @@ public class CalciteParameterQueryTest extends BaseCalciteQueryTest
|
||||||
@Test
|
@Test
|
||||||
public void testMissingParameter() throws Exception
|
public void testMissingParameter() throws Exception
|
||||||
{
|
{
|
||||||
expectedException.expect(IllegalStateException.class);
|
expectedException.expect(SqlPlanningException.class);
|
||||||
expectedException.expectMessage("Parameter: [?0] is not bound");
|
expectedException.expectMessage("Parameter at position[0] is not bound");
|
||||||
testQuery(
|
testQuery(
|
||||||
"SELECT COUNT(*)\n"
|
"SELECT COUNT(*)\n"
|
||||||
+ "FROM druid.numfoo\n"
|
+ "FROM druid.numfoo\n"
|
||||||
|
@ -590,8 +594,8 @@ public class CalciteParameterQueryTest extends BaseCalciteQueryTest
|
||||||
@Test
|
@Test
|
||||||
public void testPartiallyMissingParameter() throws Exception
|
public void testPartiallyMissingParameter() throws Exception
|
||||||
{
|
{
|
||||||
expectedException.expect(IllegalStateException.class);
|
expectedException.expect(SqlPlanningException.class);
|
||||||
expectedException.expectMessage("Parameter: [?1] is not bound");
|
expectedException.expectMessage("Parameter at position[1] is not bound");
|
||||||
testQuery(
|
testQuery(
|
||||||
"SELECT COUNT(*)\n"
|
"SELECT COUNT(*)\n"
|
||||||
+ "FROM druid.numfoo\n"
|
+ "FROM druid.numfoo\n"
|
||||||
|
@ -602,6 +606,22 @@ public class CalciteParameterQueryTest extends BaseCalciteQueryTest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPartiallyMissingParameterInTheMiddle() throws Exception
|
||||||
|
{
|
||||||
|
List<SqlParameter> params = new ArrayList<>();
|
||||||
|
params.add(null);
|
||||||
|
params.add(new SqlParameter(SqlType.INTEGER, 1));
|
||||||
|
expectedException.expect(SqlPlanningException.class);
|
||||||
|
expectedException.expectMessage("Parameter at position[0] is not bound");
|
||||||
|
testQuery(
|
||||||
|
"SELECT 1 + ?, dim1 FROM foo LIMIT ?",
|
||||||
|
ImmutableList.of(),
|
||||||
|
ImmutableList.of(),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testWrongTypeParameter() throws Exception
|
public void testWrongTypeParameter() throws Exception
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue