This is an automated email from the ASF dual-hosted git repository.
zachjsh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new d02bb8bb6e Set explain attributes after the query is prepared (#14490)
d02bb8bb6e is described below
commit d02bb8bb6e1e6d0aae3ebdb06f2f1ea335b8ad6d
Author: Abhishek Radhakrishnan <[email protected]>
AuthorDate: Thu Jul 6 11:13:32 2023 -0700
Set explain attributes after the query is prepared (#14490)
* Add support for DML WITH AS.
* One more UT for with as subquery.
* Add a test with join query
* Use root query prepared node instead of individual SqlNode types.
- Set the explain plan attributes after the query is prepared when
the query is planned and we've the finalized output names in the root
source rel node.
- Adjust tests; add unit test for negative ordinal case.
- Remove the exception / error handling logic from resolveClusteredBy
function since the validations now happen before it comes to the function
* Update comment.
---
.../sql/calcite/parser/DruidSqlParserUtils.java | 86 ++++-----
.../druid/sql/calcite/planner/DruidPlanner.java | 3 -
.../druid/sql/calcite/planner/IngestHandler.java | 4 +-
.../druid/sql/calcite/planner/QueryHandler.java | 1 +
.../druid/sql/calcite/CalciteInsertDmlTest.java | 203 +++++++++++++++++++++
.../druid/sql/calcite/CalciteReplaceDmlTest.java | 38 ++++
.../calcite/parser/DruidSqlParserUtilsTest.java | 176 ++++++------------
7 files changed, 334 insertions(+), 177 deletions(-)
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtils.java
b/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtils.java
index 36fe62e2db..0cd11b5e02 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtils.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtils.java
@@ -32,13 +32,11 @@ import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlNumericLiteral;
-import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlOrderBy;
-import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlTimestampLiteral;
-import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
import org.apache.calcite.tools.ValidationException;
+import org.apache.calcite.util.Pair;
import org.apache.druid.error.DruidException;
import org.apache.druid.error.InvalidSqlInput;
import org.apache.druid.java.util.common.StringUtils;
@@ -327,67 +325,41 @@ public class DruidSqlParserUtils
* </pre>
*
* <p>
- * The function will return the following clusteredBy columns for the above
SQL: ["__time", "page_alias", "FLOOR(\"cost\")", cityName"]
+ * The function will return the following clusteredBy columns for the above
SQL: ["__time", "page_alias", "FLOOR(\"cost\")", cityName"].
* Any ordinal and expression specified in the CLUSTERED BY clause will
resolve to the final output column name.
* </p>
+ * <p>
+ * This function must be called after the query is prepared when all the
validations are complete, including {@link #validateClusteredByColumns},
+ * so we can safely access the arguments.
+ * </p>
* @param clusteredByNodes List of {@link SqlNode}s representing columns to
be clustered by.
- * @param sourceNode The select or order by source node.
+ * @param sourceFieldMappings The source field output mappings extracted
from the validated root query rel node post prepare phase.
+ *
*/
@Nullable
- public static List<String>
resolveClusteredByColumnsToOutputColumns(SqlNodeList clusteredByNodes, SqlNode
sourceNode)
+ public static List<String> resolveClusteredByColumnsToOutputColumns(
+ final SqlNodeList clusteredByNodes,
+ final ImmutableList<Pair<Integer, String>> sourceFieldMappings
+ )
{
// CLUSTERED BY is an optional clause
if (clusteredByNodes == null) {
return null;
}
- Preconditions.checkArgument(
- sourceNode instanceof SqlSelect || sourceNode instanceof SqlOrderBy,
- "Source node must be either SqlSelect or SqlOrderBy, but found [%s]",
- sourceNode == null ? null : sourceNode.getKind()
- );
-
- final SqlSelect selectNode = (sourceNode instanceof SqlSelect) ?
(SqlSelect) sourceNode
- :
(SqlSelect) ((SqlOrderBy) sourceNode).query;
- final List<SqlNode> selectList = selectNode.getSelectList().getList();
final List<String> retClusteredByNames = new ArrayList<>();
for (SqlNode clusteredByNode : clusteredByNodes) {
- if (SqlUtil.isLiteral(clusteredByNode)) {
- // The node is a literal number -- an ordinal is specified in the
CLUSTERED BY clause. Validate and lookup the
- // ordinal in the select list.
- int ordinal = ((SqlNumericLiteral)
clusteredByNode).getValueAs(Integer.class);
- if (ordinal < 1 || ordinal > selectList.size()) {
- throw InvalidSqlInput.exception(
- "Ordinal[%d] specified in the CLUSTERED BY clause is invalid. It
must be between 1 and %d.",
- ordinal,
- selectList.size()
- );
- }
- SqlNode node = selectList.get(ordinal - 1);
-
- if (node instanceof SqlBasicCall) {
- retClusteredByNames.add(getColumnNameFromSqlCall(node));
- } else {
- Preconditions.checkArgument(
- node instanceof SqlIdentifier,
- "Node must be a SqlIdentifier, but found [%s]",
- node.getKind()
- );
- SqlIdentifier n = ((SqlIdentifier) node);
- retClusteredByNames.add(n.isSimple() ? n.getSimple() :
n.names.get(1));
- }
+ if (clusteredByNode instanceof SqlNumericLiteral) {
+ // The node is a literal number -- an ordinal in the CLUSTERED BY
clause. Lookup the ordinal in field mappings.
+ final int ordinal = ((SqlNumericLiteral)
clusteredByNode).getValueAs(Integer.class);
+ retClusteredByNames.add(sourceFieldMappings.get(ordinal - 1).right);
} else if (clusteredByNode instanceof SqlBasicCall) {
// The node is an expression/operator.
- retClusteredByNames.add(getColumnNameFromSqlCall(clusteredByNode));
+ retClusteredByNames.add(getColumnNameFromSqlCall((SqlBasicCall)
clusteredByNode));
} else {
- // The node is a simple SqlIdentifier, add the name.
- Preconditions.checkArgument(
- clusteredByNode instanceof SqlIdentifier,
- "ClusteredBy node must be a SqlIdentifier, but found [%s]",
- clusteredByNode.getKind()
- );
+ // For everything else, just return the simple string representation
of the node.
retClusteredByNames.add(clusteredByNode.toString());
}
}
@@ -395,16 +367,12 @@ public class DruidSqlParserUtils
return retClusteredByNames;
}
- private static String getColumnNameFromSqlCall(final SqlNode sqlCallNode)
+ private static String getColumnNameFromSqlCall(final SqlBasicCall
sqlCallNode)
{
- Preconditions.checkArgument(sqlCallNode instanceof SqlBasicCall, "Node
must be a SqlBasicCall type");
-
// The node may be an alias or expression, in which case we'll get the
output name
- SqlBasicCall sqlBasicCall = (SqlBasicCall) sqlCallNode;
- SqlOperator operator = (sqlBasicCall).getOperator();
- if (operator instanceof SqlAsOperator) {
+ if (sqlCallNode.getOperator() instanceof SqlAsOperator) {
// Get the output name for the alias operator.
- SqlNode sqlNode = (sqlBasicCall).getOperandList().get(1);
+ SqlNode sqlNode = sqlCallNode.getOperandList().get(1);
return sqlNode.toString();
} else {
// Return the expression as-is.
@@ -430,6 +398,18 @@ public class DruidSqlParserUtils
clusteredByNode
);
}
+
+ // Calcite already throws Ordinal out of range exception for positive
non-existent ordinals. This negative ordinal check
+ // is for completeness and is fixed in later Calcite versions.
+ if (clusteredByNode instanceof SqlNumericLiteral) {
+ final int ordinal = ((SqlNumericLiteral)
clusteredByNode).getValueAs(Integer.class);
+ if (ordinal < 1) {
+ throw InvalidSqlInput.exception(
+ "Ordinal [%d] specified in the CLUSTERED BY clause is invalid.
It must be a positive integer.",
+ ordinal
+ );
+ }
+ }
}
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java
index 7d7bdc8d54..490f220ea9 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java
@@ -151,9 +151,6 @@ public class DruidPlanner implements Closeable
handler = createHandler(root);
handler.validate();
plannerContext.setResourceActions(handler.resourceActions());
- if (root.getKind() == SqlKind.EXPLAIN) {
- plannerContext.setExplainAttributes(handler.explainAttributes());
- }
state = State.VALIDATED;
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java
index 2ce19acbba..3d38c6b3f2 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java
@@ -277,7 +277,7 @@ public abstract class IngestHandler extends QueryHandler
DruidSqlInsert.OPERATOR.getName(),
targetDatasource,
ingestionGranularity,
-
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(sqlNode.getClusteredBy(),
sqlNode.getSource()),
+
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(sqlNode.getClusteredBy(),
rootQueryRel.fields),
null
);
}
@@ -351,7 +351,7 @@ public abstract class IngestHandler extends QueryHandler
DruidSqlReplace.OPERATOR.getName(),
targetDatasource,
ingestionGranularity,
-
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(sqlNode.getClusteredBy(),
sqlNode.getSource()),
+
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(sqlNode.getClusteredBy(),
rootQueryRel.fields),
replaceIntervals
);
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
index ac3c6faff1..bb313222dc 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
@@ -162,6 +162,7 @@ public abstract class QueryHandler extends
SqlStatementHandler.BaseStatementHand
final RelDataType returnedRowType;
if (explain != null) {
+
handlerContext.plannerContext().setExplainAttributes(explainAttributes());
returnedRowType = getExplainStructType(typeFactory);
} else {
returnedRowType = returnedRowType();
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
index 71672ae8d5..395543f082 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
@@ -755,6 +755,209 @@ public class CalciteInsertDmlTest extends
CalciteIngestionDmlTest
didTest = true;
}
+ @Test
+ public void testExplainPlanInsertWithAsSubQueryClusteredBy()
+ {
+ skipVectorize();
+
+ final String resources =
"[{\"name\":\"EXTERNAL\",\"type\":\"EXTERNAL\"},{\"name\":\"foo\",\"type\":\"DATASOURCE\"}]";
+ final String attributes =
"{\"statementType\":\"INSERT\",\"targetDataSource\":\"foo\",\"partitionedBy\":{\"type\":\"all\"},\"clusteredBy\":[\"namespace\",\"country\"]}";
+
+ final String sql = "EXPLAIN PLAN FOR\n"
+ + "INSERT INTO \"foo\"\n"
+ + "WITH dd AS (\n"
+ + "SELECT * FROM TABLE(\n"
+ + " EXTERN(\n"
+ + " '{\"type\":\"inline\",\"data\":\"{\\\" \\\":
1681794225551, \\\"namespace\\\": \\\"day1\\\", \\\"country\\\":
\\\"one\\\"}\\n{\\\"__time\\\": 1681794225558, \\\"namespace\\\": \\\"day2\\\",
\\\"country\\\": \\\"two\\\"}\"}',\n"
+ + " '{\"type\":\"json\"}',\n"
+ + "
'[{\"name\":\"__time\",\"type\":\"long\"},{\"name\":\"namespace\",\"type\":\"string\"},{\"name\":\"country\",\"type\":\"string\"}]'\n"
+ + " )\n"
+ + "))\n"
+ + "\n"
+ + "SELECT\n"
+ + " __time,\n"
+ + " namespace,\n"
+ + " country\n"
+ + "FROM dd\n"
+ + "PARTITIONED BY ALL\n"
+ + "CLUSTERED BY 2, 3";
+
+ final String legacyExplanation = "DruidQueryRel("
+ +
"query=[{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"external\","
+ +
"\"inputSource\":{\"type\":\"inline\",\"data\":\"{\\\" \\\": 1681794225551,
\\\"namespace\\\": \\\"day1\\\", \\\"country\\\": \\\"one\\\"}\\n"
+ + "{\\\"__time\\\": 1681794225558,
\\\"namespace\\\": \\\"day2\\\", \\\"country\\\": \\\"two\\\"}\"},"
+ +
"\"inputFormat\":{\"type\":\"json\",\"keepNullColumns\":false,\"assumeNewlineDelimited\":false,\"useJsonNodeReader\":false},"
+ +
"\"signature\":[{\"name\":\"__time\",\"type\":\"LONG\"},{\"name\":\"namespace\",\"type\":\"STRING\"},{\"name\":\"country\",\"type\":\"STRING\"}]},"
+ +
"\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},"
+ +
"\"resultFormat\":\"compactedList\",\"orderBy\":[{\"columnName\":\"namespace\",\"order\":\"ascending\"},{\"columnName\":\"country\",\"order\":\"ascending\"}],"
+ +
"\"columns\":[\"__time\",\"country\",\"namespace\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"{\\\"type\\\":\\\"all\\\"}\","
+ +
"\"sqlQueryId\":\"dummy\",\"vectorize\":\"false\",\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}}],"
+ + " signature=[{__time:LONG,
namespace:STRING, country:STRING}])\n";
+
+ // Use testQuery for EXPLAIN (not testIngestionQuery).
+ testQuery(
+ PLANNER_CONFIG_LEGACY_QUERY_EXPLAIN,
+ ImmutableMap.of("sqlQueryId", "dummy"),
+ Collections.emptyList(),
+ sql,
+ CalciteTests.SUPER_USER_AUTH_RESULT,
+ ImmutableList.of(),
+ new DefaultResultsVerifier(
+ ImmutableList.of(
+ new Object[]{
+ legacyExplanation,
+ resources,
+ attributes
+ }
+ ),
+ null
+ ),
+ null
+ );
+
+ // Test correctness of the query when only the CLUSTERED BY clause is
present
+ final String explanation = "[{\"query\":{\"queryType\":\"scan\"," +
"\"dataSource\":{\"type\":\"external\",\"inputSource\":{\"type\":\"inline\","
+ + "\"data\":\"{\\\" \\\": 1681794225551,
\\\"namespace\\\": \\\"day1\\\", \\\"country\\\": \\\"one\\\"}\\n"
+ + "{\\\"__time\\\": 1681794225558,
\\\"namespace\\\": \\\"day2\\\", \\\"country\\\": \\\"two\\\"}\"},"
+ +
"\"inputFormat\":{\"type\":\"json\",\"keepNullColumns\":false,\"assumeNewlineDelimited\":false,\"useJsonNodeReader\":false},"
+ +
"\"signature\":[{\"name\":\"__time\",\"type\":\"LONG\"},{\"name\":\"namespace\",\"type\":\"STRING\"},{\"name\":\"country\",\"type\":\"STRING\"}]},"
+ +
"\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},"
+ +
"\"resultFormat\":\"compactedList\",\"orderBy\":[{\"columnName\":\"namespace\",\"order\":\"ascending\"},{\"columnName\":\"country\",\"order\":\"ascending\"}],"
+ +
"\"columns\":[\"__time\",\"country\",\"namespace\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"{\\\"type\\\":\\\"all\\\"}\","
+ +
"\"sqlQueryId\":\"dummy\",\"vectorize\":\"false\",\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}},"
+ +
"\"signature\":[{\"name\":\"__time\",\"type\":\"LONG\"},{\"name\":\"namespace\",\"type\":\"STRING\"},{\"name\":\"country\",\"type\":\"STRING\"}],"
+ +
"\"columnMappings\":[{\"queryColumn\":\"__time\",\"outputColumn\":\"__time\"},{\"queryColumn\":\"namespace\",\"outputColumn\":\"namespace\"},"
+ +
"{\"queryColumn\":\"country\",\"outputColumn\":\"country\"}]}]";
+
+ testQuery(
+ PLANNER_CONFIG_NATIVE_QUERY_EXPLAIN,
+ ImmutableMap.of("sqlQueryId", "dummy"),
+ Collections.emptyList(),
+ sql,
+ CalciteTests.SUPER_USER_AUTH_RESULT,
+ ImmutableList.of(),
+ new DefaultResultsVerifier(
+ ImmutableList.of(
+ new Object[]{
+ explanation,
+ resources,
+ attributes
+ }
+ ),
+ null
+ ),
+ null
+ );
+
+ // Not using testIngestionQuery, so must set didTest manually to satisfy
the check in tearDown.
+ didTest = true;
+ }
+
+ @Test
+ public void testExplainPlanInsertJoinQuery()
+ {
+ skipVectorize();
+
+ final String resources =
"[{\"name\":\"EXTERNAL\",\"type\":\"EXTERNAL\"},{\"name\":\"my_table\",\"type\":\"DATASOURCE\"}]";
+ final String attributes =
"{\"statementType\":\"INSERT\",\"targetDataSource\":\"my_table\",\"partitionedBy\":\"HOUR\",\"clusteredBy\":[\"__time\",\"isRobotAlias\",\"countryCapital\",\"regionName\"]}";
+
+ final String sql = "EXPLAIN PLAN FOR\n"
+ + "INSERT INTO my_table\n"
+ + "WITH\n"
+ + "wikidata AS (SELECT * FROM TABLE(\n"
+ + " EXTERN(\n"
+ + "
'{\"type\":\"http\",\"uris\":[\"https://boo.gz\"]}',\n"
+ + " '{\"type\":\"json\"}',\n"
+ + "
'[{\"name\":\"isRobot\",\"type\":\"string\"},{\"name\":\"timestamp\",\"type\":\"string\"},{\"name\":\"cityName\",\"type\":\"string\"},{\"name\":\"countryIsoCode\",\"type\":\"string\"},{\"name\":\"regionName\",\"type\":\"string\"}]'\n"
+ + " )\n"
+ + ")),\n"
+ + "countries AS (SELECT * FROM TABLE(\n"
+ + " EXTERN(\n"
+ + "
'{\"type\":\"http\",\"uris\":[\"https://foo.tsv\"]}',\n"
+ + "
'{\"type\":\"tsv\",\"findColumnsFromHeader\":true}',\n"
+ + "
'[{\"name\":\"Country\",\"type\":\"string\"},{\"name\":\"Capital\",\"type\":\"string\"},"
+ +
"{\"name\":\"ISO3\",\"type\":\"string\"},{\"name\":\"ISO2\",\"type\":\"string\"}]'\n"
+ + " )\n"
+ + "))\n"
+ + "SELECT\n"
+ + " TIME_PARSE(\"timestamp\") AS __time,\n"
+ + " isRobot AS isRobotAlias,\n"
+ + " countries.Capital AS countryCapital,\n"
+ + " regionName\n"
+ + "FROM wikidata\n"
+ + "LEFT JOIN countries ON wikidata.countryIsoCode =
countries.ISO2\n"
+ + "PARTITIONED BY HOUR\n"
+ + "CLUSTERED BY 1, 2, 3, regionName";
+
+ final String legacyExplanation = "DruidJoinQueryRel(condition=[=($3, $6)],
joinType=[left],
query=[{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"table\",\"name\":\"__join__\"},"
+ +
"\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},\"virtualColumns\":[{\"type\":\"expression\",\"name\":\"v0\","
+ +
"\"expression\":\"timestamp_parse(\\\"timestamp\\\",null,'UTC')\",\"outputType\":\"LONG\"}],\"resultFormat\":\"compactedList\",\"orderBy\":[{\"columnName\":\"v0\",\"order\":\"ascending\"},{\"columnName\":\"isRobot\",\"order\":\"ascending\"},"
+ +
"{\"columnName\":\"Capital\",\"order\":\"ascending\"},{\"columnName\":\"regionName\",\"order\":\"ascending\"}],\"columns\":[\"Capital\",\"isRobot\",\"regionName\",\"v0\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\","
+ +
"\"vectorize\":\"false\",\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}}],
signature=[{v0:LONG, isRobot:STRING, Capital:STRING, regionName:STRING}])\n"
+ + "
DruidQueryRel(query=[{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"external\",\"inputSource\":{\"type\":\"http\",\"uris\":[\"https://boo.gz\"]},\"inputFormat\":{\"type\":\"json\",\"keepNullColumns\":false,\"assumeNewlineDelimited\":false,"
+ +
"\"useJsonNodeReader\":false},\"signature\":[{\"name\":\"isRobot\",\"type\":\"STRING\"},{\"name\":\"timestamp\",\"type\":\"STRING\"},{\"name\":\"cityName\",\"type\":\"STRING\"},{\"name\":\"countryIsoCode\",\"type\":\"STRING\"},{\"name\":\"regionName\",\"type\":\"STRING\"}]},"
+ +
"\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},\"resultFormat\":\"compactedList\",\"columns\":[\"cityName\",\"countryIsoCode\",\"isRobot\",\"regionName\",\"timestamp\"],\"legacy\":false,"
+ +
"\"context\":{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\",\"vectorize\":\"false\",\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}}],
signature=[{isRobot:STRING, timestamp:STRING, cityName:STRING,
countryIsoCode:STRING, regionName:STRING}])\n"
+ + "
DruidQueryRel(query=[{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"external\",\"inputSource\":{\"type\":\"http\",\"uris\":[\"https://foo.tsv\"]},\"inputFormat\":{\"type\":\"tsv\",\"delimiter\":\"\\t\",\"findColumnsFromHeader\":true},"
+ +
"\"signature\":[{\"name\":\"Country\",\"type\":\"STRING\"},{\"name\":\"Capital\",\"type\":\"STRING\"},{\"name\":\"ISO3\",\"type\":\"STRING\"},{\"name\":\"ISO2\",\"type\":\"STRING\"}]},\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},"
+ +
"\"resultFormat\":\"compactedList\",\"columns\":[\"Capital\",\"ISO2\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\",\"vectorize\":\"false\",\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}}],
signature=[{Capital:STRING, ISO2:STRING}])\n";
+ // Use testQuery for EXPLAIN (not testIngestionQuery).
+ testQuery(
+ PLANNER_CONFIG_LEGACY_QUERY_EXPLAIN,
+ ImmutableMap.of("sqlQueryId", "dummy"),
+ Collections.emptyList(),
+ sql,
+ CalciteTests.SUPER_USER_AUTH_RESULT,
+ ImmutableList.of(),
+ new DefaultResultsVerifier(
+ ImmutableList.of(
+ new Object[]{
+ legacyExplanation,
+ resources,
+ attributes
+ }
+ ),
+ null
+ ),
+ null
+ );
+
+ // Test correctness of the query when only the CLUSTERED BY clause is
present
+ final String explanation =
"[{\"query\":{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"join\",\"left\":{\"type\":\"external\",\"inputSource\":{\"type\":\"http\",\"uris\":[\"https://boo.gz\"]},\"inputFormat\":{\"type\":\"json\",\"keepNullColumns\":false,"
+ +
"\"assumeNewlineDelimited\":false,\"useJsonNodeReader\":false},\"signature\":[{\"name\":\"isRobot\",\"type\":\"STRING\"},{\"name\":\"timestamp\",\"type\":\"STRING\"},{\"name\":\"cityName\",\"type\":\"STRING\"},{\"name\":\"countryIsoCode\",\"type\":\"STRING\"},"
+ +
"{\"name\":\"regionName\",\"type\":\"STRING\"}]},\"right\":{\"type\":\"query\",\"query\":{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"external\",\"inputSource\":{\"type\":\"http\",\"uris\":[\"https://foo.tsv\"]},\"inputFormat\":{\"type\":\"tsv\",\"delimiter\":\"\\t\",\"findColumnsFromHeader\":true},"
+ +
"\"signature\":[{\"name\":\"Country\",\"type\":\"STRING\"},{\"name\":\"Capital\",\"type\":\"STRING\"},{\"name\":\"ISO3\",\"type\":\"STRING\"},{\"name\":\"ISO2\",\"type\":\"STRING\"}]},\"intervals\":{\"type\":\"intervals\","
+ +
"\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},\"resultFormat\":\"compactedList\",\"columns\":[\"Capital\",\"ISO2\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\",\"vectorize\":\"false\","
+ +
"\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}}},\"rightPrefix\":\"j0.\",\"condition\":\"(\\\"countryIsoCode\\\"
==
\\\"j0.ISO2\\\")\",\"joinType\":\"LEFT\"},\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},"
+ +
"\"virtualColumns\":[{\"type\":\"expression\",\"name\":\"v0\",\"expression\":\"timestamp_parse(\\\"timestamp\\\",null,'UTC')\",\"outputType\":\"LONG\"}],\"resultFormat\":\"compactedList\",\"orderBy\":[{\"columnName\":\"v0\",\"order\":\"ascending\"},{\"columnName\":\"isRobot\",\"order\":\"ascending\"},"
+ +
"{\"columnName\":\"j0.Capital\",\"order\":\"ascending\"},{\"columnName\":\"regionName\",\"order\":\"ascending\"}],\"columns\":[\"isRobot\",\"j0.Capital\",\"regionName\",\"v0\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\",\"vectorize\":\"false\","
+ +
"\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}},\"signature\":[{\"name\":\"v0\",\"type\":\"LONG\"},{\"name\":\"isRobot\",\"type\":\"STRING\"},{\"name\":\"j0.Capital\",\"type\":\"STRING\"},{\"name\":\"regionName\",\"type\":\"STRING\"}],\"columnMappings\":[{\"queryColumn\":\"v0\",\"outputColumn\":\"__time\"},"
+ +
"{\"queryColumn\":\"isRobot\",\"outputColumn\":\"isRobotAlias\"},{\"queryColumn\":\"j0.Capital\",\"outputColumn\":\"countryCapital\"},{\"queryColumn\":\"regionName\",\"outputColumn\":\"regionName\"}]}]";
+
+ testQuery(
+ PLANNER_CONFIG_NATIVE_QUERY_EXPLAIN,
+ ImmutableMap.of("sqlQueryId", "dummy"),
+ Collections.emptyList(),
+ sql,
+ CalciteTests.SUPER_USER_AUTH_RESULT,
+ ImmutableList.of(),
+ new DefaultResultsVerifier(
+ ImmutableList.of(
+ new Object[]{
+ explanation,
+ resources,
+ attributes
+ }
+ ),
+ null
+ ),
+ null
+ );
+
+ // Not using testIngestionQuery, so must set didTest manually to satisfy
the check in tearDown.
+ didTest = true;
+ }
+
@Test
public void testExplainPlanInsertWithClusteredByDescThrowsException()
{
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java
index 282ea7d825..c4f9503d4c 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java
@@ -936,6 +936,44 @@ public class CalciteReplaceDmlTest extends
CalciteIngestionDmlTest
.verify();
}
+ @Test
+ public void testReplaceWithNonExistentOrdinalInClusteredBy()
+ {
+ skipVectorize();
+
+ final String sql = "REPLACE INTO dst"
+ + " OVERWRITE WHERE __time >= TIMESTAMP '2000-01-01
00:00:00' AND __time < TIMESTAMP '2000-01-02 00:00:00' "
+ + " SELECT * FROM foo"
+ + " PARTITIONED BY DAY"
+ + " CLUSTERED BY 1, 2, 100";
+
+ testIngestionQuery()
+ .sql(sql)
+ .expectValidationError(
+ invalidSqlContains("Ordinal out of range")
+ )
+ .verify();
+ }
+
+ @Test
+ public void testReplaceWithNegativeOrdinalInClusteredBy()
+ {
+ skipVectorize();
+
+ final String sql = "REPLACE INTO dst"
+ + " OVERWRITE WHERE __time >= TIMESTAMP '2000-01-01
00:00:00' AND __time < TIMESTAMP '2000-01-02 00:00:00' "
+ + " SELECT * FROM foo"
+ + " PARTITIONED BY DAY"
+ + " CLUSTERED BY 1, -2, 3 DESC";
+
+ testIngestionQuery()
+ .sql(sql)
+ .expectValidationError(
+ invalidSqlIs("Ordinal [-2] specified in the CLUSTERED BY clause is
invalid. It must be a positive integer.")
+ )
+ .verify();
+ }
+
@Test
public void testReplaceFromExternalProjectSort()
{
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtilsTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtilsTest.java
index b47e5bfeaa..4abc210b83 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtilsTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlParserUtilsTest.java
@@ -29,11 +29,10 @@ import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
-import org.apache.calcite.sql.SqlOrderBy;
import org.apache.calcite.sql.SqlPostfixOperator;
-import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.util.Pair;
import org.apache.druid.error.DruidException;
import org.apache.druid.error.DruidExceptionMatcher;
import org.apache.druid.java.util.common.granularity.Granularities;
@@ -144,49 +143,47 @@ public class DruidSqlParserUtilsTest
@Test
public void testNullClusteredBy()
{
- final SqlNodeList selectArgs = new SqlNodeList(SqlParserPos.ZERO);
- selectArgs.add(new SqlIdentifier("__time", new SqlParserPos(0, 1)));
-
Assert.assertNull(DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(
- null,
- new SqlSelect(SqlParserPos.ZERO, null, selectArgs, null, null, null,
null, null, null, null, null)
- )
+ final ImmutableList<Pair<Integer, String>> fields = ImmutableList.of(
+ Pair.of(1, "__time"),
+ Pair.of(2, "foo"),
+ Pair.of(3, "bar")
+ );
+ Assert.assertNull(
+ DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(
+ null,
+ fields
+ )
);
}
@Test
- public void testNullSource()
+ public void testSimpledClusteredByWithNullSource()
{
final SqlNodeList args = new SqlNodeList(SqlParserPos.ZERO);
args.add(new SqlIdentifier("__time", SqlParserPos.ZERO));
- args.add(new SqlIntervalQualifier(TimeUnit.DAY, null,
SqlParserPos.ZERO));
-
- IllegalArgumentException iae = Assert.assertThrows(
- IllegalArgumentException.class,
- () ->
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(args, null)
+ args.add(new SqlIdentifier("FOO", new SqlParserPos(0, 2)));
+ SqlBasicCall sqlBasicCall1 = new SqlBasicCall(
+ new SqlAsOperator(),
+ new SqlNode[]{
+ new SqlIdentifier("DIM3", SqlParserPos.ZERO),
+ new SqlIdentifier("DIM3_ALIAS", SqlParserPos.ZERO)
+ },
+ new SqlParserPos(0, 3)
+ );
+ args.add(sqlBasicCall1);
+ Assert.assertEquals(
+ Arrays.asList("__time", "FOO", "DIM3_ALIAS"),
+ DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(args,
null)
);
- Assert.assertEquals("Source node must be either SqlSelect or SqlOrderBy,
but found [null]", iae.getMessage());
}
@Test
public void testSimpleClusteredBy()
{
- final SqlNodeList selectArgs = new SqlNodeList(SqlParserPos.ZERO);
- selectArgs.add(new SqlIdentifier("__time", new SqlParserPos(0, 1)));
- selectArgs.add(new SqlIdentifier("FOO", new SqlParserPos(0, 2)));
- selectArgs.add(new SqlIdentifier("BOO", new SqlParserPos(0, 3)));
-
- final SqlSelect sqlSelect = new SqlSelect(
- SqlParserPos.ZERO,
- null,
- selectArgs,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null
+ final ImmutableList<Pair<Integer, String>> sourceFieldMappings =
ImmutableList.of(
+ Pair.of(1, "__time"),
+ Pair.of(2, "FOO"),
+ Pair.of(3, "BOO")
);
final SqlNodeList clusteredByArgs = new SqlNodeList(SqlParserPos.ZERO);
@@ -196,42 +193,7 @@ public class DruidSqlParserUtilsTest
Assert.assertEquals(
Arrays.asList("__time", "FOO", "BOO"),
-
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(clusteredByArgs,
sqlSelect)
- );
- }
-
- @Test
- public void testClusteredByOrdinalInvalidThrowsException()
- {
- final SqlNodeList selectArgs = new SqlNodeList(SqlParserPos.ZERO);
- selectArgs.add(new SqlIdentifier("__time", new SqlParserPos(0, 1)));
- selectArgs.add(new SqlIdentifier("FOO", new SqlParserPos(0, 2)));
- selectArgs.add(new SqlIdentifier("BOO", new SqlParserPos(0, 3)));
-
- final SqlSelect sqlSelect = new SqlSelect(
- SqlParserPos.ZERO,
- null,
- selectArgs,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null
- );
-
- final SqlNodeList clusteredByArgs = new SqlNodeList(SqlParserPos.ZERO);
- clusteredByArgs.add(new SqlIdentifier("__time", SqlParserPos.ZERO));
- clusteredByArgs.add(new SqlIdentifier("FOO", SqlParserPos.ZERO));
- clusteredByArgs.add(SqlLiteral.createExactNumeric("4",
SqlParserPos.ZERO));
-
- MatcherAssert.assertThat(
- Assert.assertThrows(DruidException.class, () ->
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(clusteredByArgs,
sqlSelect)),
- DruidExceptionMatcher.invalidSqlInput().expectMessageIs(
- "Ordinal[4] specified in the CLUSTERED BY clause is invalid. It
must be between 1 and 3."
- )
+
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(clusteredByArgs,
sourceFieldMappings)
);
}
@@ -272,18 +234,14 @@ public class DruidSqlParserUtilsTest
args3.add(SqlLiteral.createCharString("PT1H", SqlParserPos.ZERO));
selectArgs.add(TimeFloorOperatorConversion.SQL_FUNCTION.createCall(args3));
- final SqlSelect sqlSelect = new SqlSelect(
- SqlParserPos.ZERO,
- null,
- selectArgs,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null
+ final ImmutableList<Pair<Integer, String>> sourceFieldMappings =
ImmutableList.of(
+ Pair.of(1, "__time"),
+ Pair.of(2, "DIM3"),
+ Pair.of(3, "DIM3_ALIAS"),
+ Pair.of(4, "floor_dim4_time"),
+ Pair.of(5, "DIM5"),
+ Pair.of(5, "DIM6"),
+ Pair.of(7, "TIME_FLOOR(\"timestamps\", 'PT1H')")
);
// Construct the clustered by args
@@ -295,45 +253,7 @@ public class DruidSqlParserUtilsTest
Assert.assertEquals(
Arrays.asList("DIM3_ALIAS", "floor_dim4_time", "DIM5",
"TIME_FLOOR(\"timestamps\", 'PT1H')"),
-
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(clusteredByArgs,
sqlSelect)
- );
- }
-
- @Test
- public void testSimpleClusteredByWithOrderBy()
- {
- final SqlNodeList selectArgs = new SqlNodeList(SqlParserPos.ZERO);
- selectArgs.add(new SqlIdentifier("__time", new SqlParserPos(0, 1)));
- selectArgs.add(new SqlIdentifier("FOO", new SqlParserPos(0, 2)));
- selectArgs.add(new SqlIdentifier("BOO", new SqlParserPos(0, 3)));
-
- final SqlSelect sqlSelect = new SqlSelect(
- SqlParserPos.ZERO,
- null,
- selectArgs,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null
- );
-
- SqlNodeList orderList = new SqlNodeList(SqlParserPos.ZERO);
- orderList.add(sqlSelect);
-
- SqlNode orderByNode = new SqlOrderBy(SqlParserPos.ZERO, sqlSelect,
orderList, null, null);
-
- final SqlNodeList clusteredByArgs = new SqlNodeList(SqlParserPos.ZERO);
- clusteredByArgs.add(new SqlIdentifier("__time", SqlParserPos.ZERO));
- clusteredByArgs.add(new SqlIdentifier("FOO", SqlParserPos.ZERO));
- clusteredByArgs.add(SqlLiteral.createExactNumeric("3",
SqlParserPos.ZERO));
-
- Assert.assertEquals(
- Arrays.asList("__time", "FOO", "BOO"),
-
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(clusteredByArgs,
orderByNode)
+
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(clusteredByArgs,
sourceFieldMappings)
);
}
}
@@ -390,6 +310,24 @@ public class DruidSqlParserUtilsTest
.expectMessageIs("Invalid CLUSTERED BY clause [`DIM4` DESC]: cannot
sort in descending order.")
.assertThrowsAndMatches(() ->
DruidSqlParserUtils.validateClusteredByColumns(clusteredByArgs));
}
+
+ /**
+ * Tests clause "CLUSTERED BY DIM1, DIM2, 3, -10"
+ */
+ @Test
+ public void testClusteredByColumnsWithNegativeOrdinalThrowsException()
+ {
+ final SqlNodeList clusteredByArgs = new SqlNodeList(SqlParserPos.ZERO);
+ clusteredByArgs.add(new SqlIdentifier("DIM1", SqlParserPos.ZERO));
+ clusteredByArgs.add(new SqlIdentifier("DIM2", SqlParserPos.ZERO));
+ clusteredByArgs.add(new SqlIdentifier("3", SqlParserPos.ZERO));
+ clusteredByArgs.add(SqlLiteral.createExactNumeric("-10",
SqlParserPos.ZERO));
+
+ DruidExceptionMatcher
+ .invalidSqlInput()
+ .expectMessageIs("Ordinal [-10] specified in the CLUSTERED BY clause
is invalid. It must be a positive integer.")
+ .assertThrowsAndMatches(() ->
DruidSqlParserUtils.validateClusteredByColumns(clusteredByArgs));
+ }
}
public static class FloorToGranularityConversionErrorsTest
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]