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 79bff4bbf7 Improvements to `EXPLAIN PLAN` attributes (#14441)
79bff4bbf7 is described below
commit 79bff4bbf7b317ec0d552d595e07b6da29b810b4
Author: Abhishek Radhakrishnan <[email protected]>
AuthorDate: Mon Jun 26 20:01:11 2023 -0700
Improvements to `EXPLAIN PLAN` attributes (#14441)
* Updates: use the target table directly, sanitized replace time chunks and
clustered by cols.
* Add DruidSqlParserUtil and tests.
* minor refactor
* Use SqlUtil.isLiteral
* Throw ValidationException if CLUSTERED BY column descending order is
specified.
- Fails query planning
* Some more tests.
* fixup existing comment
* Update comment
* checkstyle fix: remove unused imports
* Remove InsertCannotOrderByDescendingFault and deprecate the fault in
readme.
* minor naming
* move deprecated field to the bottom
* update docs.
* add one more example.
* Collapsible query and result
* checkstyle fixes
* Code cleanup
* order by changes
* conditionally set attributes only for explain queries.
* Cleaner ordinal check.
* Add limit test and update javadoc.
* Commentary and minor adjustments.
* Checkstyle fixes.
* One more checkArg.
* add unexpected kind to exception.
---
docs/querying/sql-translation.md | 260 +++++++++++++++++++--
.../druid/sql/calcite/parser/DruidSqlIngest.java | 3 +-
.../sql/calcite/parser/DruidSqlParserUtils.java | 112 ++++++++-
.../druid/sql/calcite/planner/DruidPlanner.java | 4 +-
.../sql/calcite/planner/ExplainAttributes.java | 25 +-
.../druid/sql/calcite/planner/IngestHandler.java | 23 +-
.../druid/sql/calcite/CalciteInsertDmlTest.java | 4 +-
.../druid/sql/calcite/CalciteReplaceDmlTest.java | 110 ++++++++-
.../calcite/parser/DruidSqlParserUtilsTest.java | 213 +++++++++++++++++
.../sql/calcite/parser/DruidSqlUnparseTest.java | 3 +-
.../sql/calcite/planner/ExplainAttributesTest.java | 87 +++++--
11 files changed, 771 insertions(+), 73 deletions(-)
diff --git a/docs/querying/sql-translation.md b/docs/querying/sql-translation.md
index 5126b9fc35..5528db9532 100644
--- a/docs/querying/sql-translation.md
+++ b/docs/querying/sql-translation.md
@@ -76,6 +76,8 @@ EXPLAIN PLAN statements return:
Example 1: EXPLAIN PLAN for a `SELECT` query on the `wikipedia` datasource:
+<details><summary>Show the query</summary>
+
```sql
EXPLAIN PLAN FOR
SELECT
@@ -85,9 +87,12 @@ FROM wikipedia
WHERE channel IN (SELECT page FROM wikipedia GROUP BY page ORDER BY COUNT(*)
DESC LIMIT 10)
GROUP BY channel
```
+</details>
The above EXPLAIN PLAN query returns the following result:
+<details><summary>Show the result</summary>
+
```json
[
[
@@ -224,13 +229,15 @@ The above EXPLAIN PLAN query returns the following result:
}
]
```
+</details>
-Example 2: EXPLAIN PLAN for a `REPLACE` query that replaces all the data in
the `wikipedia` datasource:
+Example 2: EXPLAIN PLAN for an `INSERT` query that inserts data into the
`wikipedia` datasource:
+
+<details><summary>Show the query</summary>
```sql
EXPLAIN PLAN FOR
-REPLACE INTO wikipedia
-OVERWRITE ALL
+INSERT INTO wikipedia2
SELECT
TIME_PARSE("timestamp") AS __time,
namespace,
@@ -247,11 +254,14 @@ FROM TABLE(
'[{"name":"timestamp","type":"string"},{"name":"namespace","type":"string"},{"name":"cityName","type":"string"},{"name":"countryName","type":"string"},{"name":"regionIsoCode","type":"string"},{"name":"metroCode","type":"long"},{"name":"countryIsoCode","type":"string"},{"name":"regionName","type":"string"}]'
)
)
-PARTITIONED BY HOUR
-CLUSTERED BY cityName
+PARTITIONED BY ALL
```
+</details>
-The above EXPLAIN PLAN query returns the following result:
+
+The above EXPLAIN PLAN returns the following result:
+
+<details><summary>Show the result</summary>
```json
[
@@ -323,12 +333,229 @@ The above EXPLAIN PLAN query returns the following
result:
}
],
"resultFormat": "compactedList",
- "orderBy": [
+ "columns": [
+ "cityName",
+ "countryIsoCode",
+ "countryName",
+ "metroCode",
+ "namespace",
+ "regionIsoCode",
+ "regionName",
+ "v0"
+ ],
+ "legacy": false,
+ "context": {
+ "finalizeAggregations": false,
+ "forceExpressionVirtualColumns": true,
+ "groupByEnableMultiValueUnnesting": false,
+ "maxNumTasks": 5,
+ "multiStageQuery": true,
+ "queryId": "42e3de2b-daaf-40f9-a0e7-2c6184529ea3",
+ "scanSignature":
"[{\"name\":\"cityName\",\"type\":\"STRING\"},{\"name\":\"countryIsoCode\",\"type\":\"STRING\"},{\"name\":\"countryName\",\"type\":\"STRING\"},{\"name\":\"metroCode\",\"type\":\"LONG\"},{\"name\":\"namespace\",\"type\":\"STRING\"},{\"name\":\"regionIsoCode\",\"type\":\"STRING\"},{\"name\":\"regionName\",\"type\":\"STRING\"},{\"name\":\"v0\",\"type\":\"LONG\"}]",
+ "sqlInsertSegmentGranularity": "{\"type\":\"all\"}",
+ "sqlQueryId": "42e3de2b-daaf-40f9-a0e7-2c6184529ea3",
+ "useNativeQueryExplain": true
+ },
+ "granularity": {
+ "type": "all"
+ }
+ },
+ "signature": [
+ {
+ "name": "v0",
+ "type": "LONG"
+ },
+ {
+ "name": "namespace",
+ "type": "STRING"
+ },
+ {
+ "name": "cityName",
+ "type": "STRING"
+ },
+ {
+ "name": "countryName",
+ "type": "STRING"
+ },
+ {
+ "name": "regionIsoCode",
+ "type": "STRING"
+ },
+ {
+ "name": "metroCode",
+ "type": "LONG"
+ },
+ {
+ "name": "countryIsoCode",
+ "type": "STRING"
+ },
+ {
+ "name": "regionName",
+ "type": "STRING"
+ }
+ ],
+ "columnMappings": [
+ {
+ "queryColumn": "v0",
+ "outputColumn": "__time"
+ },
+ {
+ "queryColumn": "namespace",
+ "outputColumn": "namespace"
+ },
+ {
+ "queryColumn": "cityName",
+ "outputColumn": "cityName"
+ },
+ {
+ "queryColumn": "countryName",
+ "outputColumn": "countryName"
+ },
+ {
+ "queryColumn": "regionIsoCode",
+ "outputColumn": "regionIsoCode"
+ },
+ {
+ "queryColumn": "metroCode",
+ "outputColumn": "metroCode"
+ },
+ {
+ "queryColumn": "countryIsoCode",
+ "outputColumn": "countryIsoCode"
+ },
+ {
+ "queryColumn": "regionName",
+ "outputColumn": "regionName"
+ }
+ ]
+ }
+ ],
+ [
+ {
+ "name": "EXTERNAL",
+ "type": "EXTERNAL"
+ },
+ {
+ "name": "wikipedia",
+ "type": "DATASOURCE"
+ }
+ ],
+ {
+ "statementType": "INSERT",
+ "targetDataSource": "wikipedia",
+ "partitionedBy": {
+ "type": "all"
+ }
+ }
+]
+```
+</details>
+
+Example 3: EXPLAIN PLAN for a `REPLACE` query that replaces all the data in
the `wikipedia` datasource with a `DAY`
+time partitioning, and `cityName` and `countryName` as the clustering columns:
+
+<details><summary>Show the query</summary>
+
+```sql
+EXPLAIN PLAN FOR
+REPLACE INTO wikipedia
+OVERWRITE ALL
+SELECT
+ TIME_PARSE("timestamp") AS __time,
+ namespace,
+ cityName,
+ countryName,
+ regionIsoCode,
+ metroCode,
+ countryIsoCode,
+ regionName
+FROM TABLE(
+ EXTERN(
+
'{"type":"http","uris":["https://druid.apache.org/data/wikipedia.json.gz"]}',
+ '{"type":"json"}',
+
'[{"name":"timestamp","type":"string"},{"name":"namespace","type":"string"},{"name":"cityName","type":"string"},{"name":"countryName","type":"string"},{"name":"regionIsoCode","type":"string"},{"name":"metroCode","type":"long"},{"name":"countryIsoCode","type":"string"},{"name":"regionName","type":"string"}]'
+ )
+ )
+PARTITIONED BY DAY
+CLUSTERED BY cityName, countryName
+```
+</details>
+
+
+The above EXPLAIN PLAN query returns the following result:
+
+<details><summary>Show the result</summary>
+
+```json
+[
+ [
+ {
+ "query": {
+ "queryType": "scan",
+ "dataSource": {
+ "type": "external",
+ "inputSource": {
+ "type": "http",
+ "uris": [
+ "https://druid.apache.org/data/wikipedia.json.gz"
+ ]
+ },
+ "inputFormat": {
+ "type": "json",
+ "keepNullColumns": false,
+ "assumeNewlineDelimited": false,
+ "useJsonNodeReader": false
+ },
+ "signature": [
+ {
+ "name": "timestamp",
+ "type": "STRING"
+ },
+ {
+ "name": "namespace",
+ "type": "STRING"
+ },
+ {
+ "name": "cityName",
+ "type": "STRING"
+ },
+ {
+ "name": "countryName",
+ "type": "STRING"
+ },
+ {
+ "name": "regionIsoCode",
+ "type": "STRING"
+ },
+ {
+ "name": "metroCode",
+ "type": "LONG"
+ },
+ {
+ "name": "countryIsoCode",
+ "type": "STRING"
+ },
+ {
+ "name": "regionName",
+ "type": "STRING"
+ }
+ ]
+ },
+ "intervals": {
+ "type": "intervals",
+ "intervals": [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z"
+ ]
+ },
+ "virtualColumns": [
{
- "columnName": "cityName",
- "order": "ascending"
+ "type": "expression",
+ "name": "v0",
+ "expression": "timestamp_parse(\"timestamp\",null,'UTC')",
+ "outputType": "LONG"
}
],
+ "resultFormat": "compactedList",
"columns": [
"cityName",
"countryIsoCode",
@@ -344,10 +571,10 @@ The above EXPLAIN PLAN query returns the following result:
"finalizeAggregations": false,
"groupByEnableMultiValueUnnesting": false,
"maxNumTasks": 5,
- "queryId": "b474c0d5-a5ce-432d-be94-535ccdb7addc",
+ "queryId": "d88e0823-76d4-40d9-a1a7-695c8577b79f",
"scanSignature":
"[{\"name\":\"cityName\",\"type\":\"STRING\"},{\"name\":\"countryIsoCode\",\"type\":\"STRING\"},{\"name\":\"countryName\",\"type\":\"STRING\"},{\"name\":\"metroCode\",\"type\":\"LONG\"},{\"name\":\"namespace\",\"type\":\"STRING\"},{\"name\":\"regionIsoCode\",\"type\":\"STRING\"},{\"name\":\"regionName\",\"type\":\"STRING\"},{\"name\":\"v0\",\"type\":\"LONG\"}]",
- "sqlInsertSegmentGranularity": "\"HOUR\"",
- "sqlQueryId": "b474c0d5-a5ce-432d-be94-535ccdb7addc",
+ "sqlInsertSegmentGranularity": "\"DAY\"",
+ "sqlQueryId": "d88e0823-76d4-40d9-a1a7-695c8577b79f",
"sqlReplaceTimeChunks": "all"
},
"granularity": {
@@ -437,13 +664,16 @@ The above EXPLAIN PLAN query returns the following result:
{
"statementType": "REPLACE",
"targetDataSource": "wikipedia",
- "partitionedBy": "HOUR",
- "clusteredBy": "`cityName`",
- "replaceTimeChunks": "'ALL'"
+ "partitionedBy": "DAY",
+ "clusteredBy": ["cityName","countryName"],
+ "replaceTimeChunks": "all"
}
]
```
+</details>
+
+
In this case the JOIN operator gets translated to a `join` datasource. See the
[Join translation](#joins) section
for more details about how this works.
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlIngest.java
b/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlIngest.java
index 26f019e0a1..146d13673b 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlIngest.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/parser/DruidSqlIngest.java
@@ -42,7 +42,8 @@ public abstract class DruidSqlIngest extends SqlInsert
@Nullable
protected final SqlNodeList clusteredBy;
- public DruidSqlIngest(SqlParserPos pos,
+ public DruidSqlIngest(
+ SqlParserPos pos,
SqlNodeList keywords,
SqlNode targetTable,
SqlNode source,
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 5f11c6f836..36fe62e2db 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
@@ -22,6 +22,7 @@ package org.apache.druid.sql.calcite.parser;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import org.apache.calcite.sql.SqlAsOperator;
import org.apache.calcite.sql.SqlBasicCall;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlIdentifier;
@@ -30,8 +31,13 @@ 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.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.druid.error.DruidException;
import org.apache.druid.error.InvalidSqlInput;
@@ -57,6 +63,7 @@ import org.joda.time.Interval;
import org.joda.time.Period;
import org.joda.time.base.AbstractInterval;
+import javax.annotation.Nullable;
import java.sql.Timestamp;
import java.time.ZonedDateTime;
import java.util.ArrayList;
@@ -200,7 +207,7 @@ public class DruidSqlParserUtils
}
/**
- * This method validates and converts a {@link SqlNode} representing a query
into an optmizied list of intervals to
+ * This method validates and converts a {@link SqlNode} representing a query
into an optimized list of intervals to
* be used in creating an ingestion spec. If the sqlNode is an SqlLiteral of
{@link #ALL}, returns a singleton list of
* "ALL". Otherwise, it converts and optimizes the query using {@link
MoveTimeFiltersToIntervals} into a list of
* intervals which contain all valid values of time as per the query.
@@ -302,6 +309,109 @@ public class DruidSqlParserUtils
);
}
+ /**
+ * Return resolved clustered by column output names.
+ * For example, consider the following SQL:
+ * <pre>
+ * EXPLAIN PLAN FOR
+ * INSERT INTO w000
+ * SELECT
+ * TIME_PARSE("timestamp") AS __time,
+ * page AS page_alias,
+ * FLOOR("cost"),
+ * country,
+ * citName
+ * FROM ...
+ * PARTITIONED BY DAY
+ * CLUSTERED BY 1, 2, 3, cityName
+ * </pre>
+ *
+ * <p>
+ * 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>
+ * @param clusteredByNodes List of {@link SqlNode}s representing columns to
be clustered by.
+ * @param sourceNode The select or order by source node.
+ */
+ @Nullable
+ public static List<String>
resolveClusteredByColumnsToOutputColumns(SqlNodeList clusteredByNodes, SqlNode
sourceNode)
+ {
+ // 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));
+ }
+ } else if (clusteredByNode instanceof SqlBasicCall) {
+ // The node is an expression/operator.
+ retClusteredByNames.add(getColumnNameFromSqlCall(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()
+ );
+ retClusteredByNames.add(clusteredByNode.toString());
+ }
+ }
+
+ return retClusteredByNames;
+ }
+
+ private static String getColumnNameFromSqlCall(final SqlNode 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) {
+ // Get the output name for the alias operator.
+ SqlNode sqlNode = (sqlBasicCall).getOperandList().get(1);
+ return sqlNode.toString();
+ } else {
+ // Return the expression as-is.
+ return sqlCallNode.toSqlString(CalciteSqlDialect.DEFAULT).toString();
+ }
+ }
+
/**
* Validates the clustered by columns to ensure that it does not contain
DESCENDING order columns.
*
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 fc6c08a040..7d7bdc8d54 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,7 +151,9 @@ public class DruidPlanner implements Closeable
handler = createHandler(root);
handler.validate();
plannerContext.setResourceActions(handler.resourceActions());
- plannerContext.setExplainAttributes(handler.explainAttributes());
+ 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/ExplainAttributes.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/ExplainAttributes.java
index 8d040f23fa..e2ae4fa7a1 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/ExplainAttributes.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/ExplainAttributes.java
@@ -21,11 +21,10 @@ package org.apache.druid.sql.calcite.planner;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
-import org.apache.calcite.sql.SqlNode;
-import org.apache.calcite.sql.SqlNodeList;
import org.apache.druid.java.util.common.granularity.Granularity;
import javax.annotation.Nullable;
+import java.util.List;
/**
* ExplainAttributes holds the attributes of a SQL statement that is used in
the EXPLAIN PLAN result.
@@ -35,23 +34,23 @@ public final class ExplainAttributes
private final String statementType;
@Nullable
- private final SqlNode targetDataSource;
+ private final String targetDataSource;
@Nullable
private final Granularity partitionedBy;
@Nullable
- private final SqlNodeList clusteredBy;
+ private final List<String> clusteredBy;
@Nullable
- private final SqlNode replaceTimeChunks;
+ private final String replaceTimeChunks;
public ExplainAttributes(
@JsonProperty("statementType") final String statementType,
- @JsonProperty("targetDataSource") @Nullable final SqlNode
targetDataSource,
+ @JsonProperty("targetDataSource") @Nullable final String
targetDataSource,
@JsonProperty("partitionedBy") @Nullable final Granularity partitionedBy,
- @JsonProperty("clusteredBy") @Nullable final SqlNodeList clusteredBy,
- @JsonProperty("replaceTimeChunks") @Nullable final SqlNode
replaceTimeChunks
+ @JsonProperty("clusteredBy") @Nullable final List<String> clusteredBy,
+ @JsonProperty("replaceTimeChunks") @Nullable final String
replaceTimeChunks
)
{
this.statementType = statementType;
@@ -62,7 +61,7 @@ public final class ExplainAttributes
}
/**
- * @return the statement kind of a SQL statement. For example, SELECT,
INSERT, or REPLACE.
+ * @return the SQL statement type. For example, SELECT, INSERT, or REPLACE.
*/
@JsonProperty
public String getStatementType()
@@ -79,7 +78,7 @@ public final class ExplainAttributes
@JsonInclude(JsonInclude.Include.NON_NULL)
public String getTargetDataSource()
{
- return targetDataSource == null ? null : targetDataSource.toString();
+ return targetDataSource;
}
/**
@@ -101,9 +100,9 @@ public final class ExplainAttributes
@Nullable
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
- public String getClusteredBy()
+ public List<String> getClusteredBy()
{
- return clusteredBy == null ? null : clusteredBy.toString();
+ return clusteredBy;
}
/**
@@ -115,7 +114,7 @@ public final class ExplainAttributes
@JsonInclude(JsonInclude.Include.NON_NULL)
public String getReplaceTimeChunks()
{
- return replaceTimeChunks == null ? null : replaceTimeChunks.toString();
+ return replaceTimeChunks;
}
@Override
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 52b4efcfeb..2ce19acbba 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
@@ -275,9 +275,9 @@ public abstract class IngestHandler extends QueryHandler
{
return new ExplainAttributes(
DruidSqlInsert.OPERATOR.getName(),
- sqlNode.getTargetTable(),
- sqlNode.getPartitionedBy(),
- sqlNode.getClusteredBy(),
+ targetDatasource,
+ ingestionGranularity,
+
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(sqlNode.getClusteredBy(),
sqlNode.getSource()),
null
);
}
@@ -289,7 +289,7 @@ public abstract class IngestHandler extends QueryHandler
protected static class ReplaceHandler extends IngestHandler
{
private final DruidSqlReplace sqlNode;
- private List<String> replaceIntervals;
+ private String replaceIntervals;
public ReplaceHandler(
SqlStatementHandler.HandlerContext handlerContext,
@@ -329,16 +329,17 @@ public abstract class IngestHandler extends QueryHandler
);
}
- replaceIntervals =
DruidSqlParserUtils.validateQueryAndConvertToIntervals(
+ List<String> replaceIntervalsList =
DruidSqlParserUtils.validateQueryAndConvertToIntervals(
replaceTimeQuery,
ingestionGranularity,
handlerContext.timeZone()
);
super.validate();
- if (replaceIntervals != null) {
+ if (replaceIntervalsList != null) {
+ replaceIntervals = String.join(",", replaceIntervalsList);
handlerContext.queryContextMap().put(
DruidSqlReplace.SQL_REPLACE_TIME_CHUNKS,
- String.join(",", replaceIntervals)
+ replaceIntervals
);
}
}
@@ -348,10 +349,10 @@ public abstract class IngestHandler extends QueryHandler
{
return new ExplainAttributes(
DruidSqlReplace.OPERATOR.getName(),
- sqlNode.getTargetTable(),
- sqlNode.getPartitionedBy(),
- sqlNode.getClusteredBy(),
- sqlNode.getReplaceTimeQuery()
+ targetDatasource,
+ ingestionGranularity,
+
DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(sqlNode.getClusteredBy(),
sqlNode.getSource()),
+ replaceIntervals
);
}
}
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 7fb52843b4..1679b86fb0 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
@@ -657,7 +657,7 @@ public class CalciteInsertDmlTest extends
CalciteIngestionDmlTest
skipVectorize();
final String resources =
"[{\"name\":\"dst\",\"type\":\"DATASOURCE\"},{\"name\":\"foo\",\"type\":\"DATASOURCE\"}]";
- final String attributes =
"{\"statementType\":\"INSERT\",\"targetDataSource\":\"druid.dst\",\"partitionedBy\":\"DAY\",\"clusteredBy\":\"2,
`dim1`, CEIL(`m2`)\"}";
+ final String attributes =
"{\"statementType\":\"INSERT\",\"targetDataSource\":\"dst\",\"partitionedBy\":\"DAY\",\"clusteredBy\":[\"floor_m1\",\"dim1\",\"CEIL(\\\"m2\\\")\"]}";
final String sql = "EXPLAIN PLAN FOR INSERT INTO druid.dst "
+ "SELECT __time, FLOOR(m1) as floor_m1, dim1, CEIL(m2)
as ceil_m2 FROM foo "
@@ -1126,7 +1126,7 @@ public class CalciteInsertDmlTest extends
CalciteIngestionDmlTest
+ "}]";
final String resources =
"[{\"name\":\"dst\",\"type\":\"DATASOURCE\"},{\"name\":\"foo\",\"type\":\"DATASOURCE\"}]";
- final String attributes =
"{\"statementType\":\"INSERT\",\"targetDataSource\":\"druid.dst\",\"partitionedBy\":\"DAY\",\"clusteredBy\":\"2,
`dim1`, CEIL(`m2`)\"}";
+ final String attributes =
"{\"statementType\":\"INSERT\",\"targetDataSource\":\"dst\",\"partitionedBy\":\"DAY\",\"clusteredBy\":[\"floor_m1\",\"dim1\",\"CEIL(\\\"m2\\\")\"]}";
// Use testQuery for EXPLAIN (not testIngestionQuery).
testQuery(
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 d7ba655c1e..282ea7d825 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
@@ -654,7 +654,7 @@ public class CalciteReplaceDmlTest extends
CalciteIngestionDmlTest
+
"\"columnMappings\":[{\"queryColumn\":\"x\",\"outputColumn\":\"x\"},{\"queryColumn\":\"y\",\"outputColumn\":\"y\"},{\"queryColumn\":\"z\",\"outputColumn\":\"z\"}]}]";
final String resources =
"[{\"name\":\"EXTERNAL\",\"type\":\"EXTERNAL\"},{\"name\":\"dst\",\"type\":\"DATASOURCE\"}]";
- final String attributes =
"{\"statementType\":\"REPLACE\",\"targetDataSource\":\"dst\",\"partitionedBy\":{\"type\":\"all\"},\"replaceTimeChunks\":\"'ALL'\"}";
+ final String attributes =
"{\"statementType\":\"REPLACE\",\"targetDataSource\":\"dst\",\"partitionedBy\":{\"type\":\"all\"},\"replaceTimeChunks\":\"all\"}";
// Use testQuery for EXPLAIN (not testIngestionQuery).
testQuery(
@@ -732,12 +732,116 @@ public class CalciteReplaceDmlTest extends
CalciteIngestionDmlTest
final String explanation =
"[{\"query\":{\"queryType\":\"scan\",\"dataSource\":{\"type\":\"table\",\"name\":\"foo\"},\"intervals\":{\"type\":\"intervals\",\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},\"resultFormat\":\"compactedList\",\"orderBy\":[{\"columnName\":\"dim1\",\"order\":\"ascending\"}],\"columns\":[\"__time\",\"cnt\",\"dim1\",\"dim2\",\"dim3\",\"m1\",\"m2\",\"unique_dim1\"],\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":
[...]
final String resources =
"[{\"name\":\"dst\",\"type\":\"DATASOURCE\"},{\"name\":\"foo\",\"type\":\"DATASOURCE\"}]";
- final String attributes =
"{\"statementType\":\"REPLACE\",\"targetDataSource\":\"dst\",\"partitionedBy\":\"DAY\",\"clusteredBy\":\"`dim1`\",\"replaceTimeChunks\":\"`__time`
>= TIMESTAMP '2000-01-01 00:00:00' AND `__time` < TIMESTAMP '2000-01-02
00:00:00'\"}";
+ final String attributes =
"{\"statementType\":\"REPLACE\",\"targetDataSource\":\"dst\",\"partitionedBy\":\"DAY\",\"clusteredBy\":[\"dim1\"],\"replaceTimeChunks\":\"2000-01-01T00:00:00.000Z/2000-01-02T00:00:00.000Z\"}";
final String sql = "EXPLAIN PLAN FOR"
+ " 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
dim1";
+ + "SELECT * FROM foo PARTITIONED BY DAY CLUSTERED BY
dim1 ASC";
+ // 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
+ );
+
+ 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 testExplainReplaceWithLimitAndClusteredByOrdinals() throws
IOException
+ {
+ // Skip vectorization since otherwise the "context" will change for each
subtest.
+ skipVectorize();
+
+ ObjectMapper queryJsonMapper = queryFramework().queryJsonMapper();
+ final ScanQuery expectedQuery = newScanQueryBuilder()
+ .dataSource("foo")
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .columns("__time", "cnt", "dim1", "dim2", "dim3", "m1", "m2",
"unique_dim1")
+ .limit(10)
+ .orderBy(
+ ImmutableList.of(
+ new ScanQuery.OrderBy("__time", ScanQuery.Order.ASCENDING),
+ new ScanQuery.OrderBy("dim1", ScanQuery.Order.ASCENDING),
+ new ScanQuery.OrderBy("dim3", ScanQuery.Order.ASCENDING),
+ new ScanQuery.OrderBy("dim2", ScanQuery.Order.ASCENDING)
+ )
+ )
+ .context(
+ queryJsonMapper.readValue(
+
"{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\",\"sqlReplaceTimeChunks\":\"2000-01-01T00:00:00.000Z/2000-01-02T00:00:00.000Z\",\"vectorize\":\"false\",\"vectorizeVirtualColumns\":\"false\"}",
+ JacksonUtils.TYPE_REFERENCE_MAP_STRING_OBJECT
+ )
+ )
+ .build();
+
+ final String legacyExplanation =
+ "DruidQueryRel(query=["
+ + queryJsonMapper.writeValueAsString(expectedQuery)
+ + "], signature=[{__time:LONG, dim1:STRING, dim2:STRING, dim3:STRING,
cnt:LONG, m1:FLOAT, m2:DOUBLE, unique_dim1:COMPLEX<hyperUnique>}])\n";
+
+ final String explanation = "["
+ +
"{\"query\":{\"queryType\":\"scan\",\"dataSource\":"
+ +
"{\"type\":\"table\",\"name\":\"foo\"},\"intervals\":{\"type\":\"intervals\","
+ +
"\"intervals\":[\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\"]},"
+ +
"\"resultFormat\":\"compactedList\",\"limit\":10,"
+ +
"\"orderBy\":[{\"columnName\":\"__time\",\"order\":\"ascending\"},{\"columnName\":\"dim1\",\"order\":\"ascending\"},"
+ +
"{\"columnName\":\"dim3\",\"order\":\"ascending\"},{\"columnName\":\"dim2\",\"order\":\"ascending\"}],"
+ +
"\"columns\":[\"__time\",\"cnt\",\"dim1\",\"dim2\",\"dim3\",\"m1\",\"m2\",\"unique_dim1\"],"
+ +
"\"legacy\":false,\"context\":{\"sqlInsertSegmentGranularity\":\"\\\"HOUR\\\"\",\"sqlQueryId\":\"dummy\","
+ +
"\"sqlReplaceTimeChunks\":\"2000-01-01T00:00:00.000Z/2000-01-02T00:00:00.000Z\",\"vectorize\":\"false\","
+ +
"\"vectorizeVirtualColumns\":\"false\"},\"granularity\":{\"type\":\"all\"}},"
+ +
"\"signature\":[{\"name\":\"__time\",\"type\":\"LONG\"},{\"name\":\"dim1\",\"type\":\"STRING\"},{\"name\":\"dim2\",\"type\":\"STRING\"},{\"name\":\"dim3\",\"type\":\"STRING\"},"
+ +
"{\"name\":\"cnt\",\"type\":\"LONG\"},{\"name\":\"m1\",\"type\":\"FLOAT\"},{\"name\":\"m2\",\"type\":\"DOUBLE\"},{\"name\":\"unique_dim1\",\"type\":\"COMPLEX<hyperUnique>\"}],"
+ +
"\"columnMappings\":[{\"queryColumn\":\"__time\",\"outputColumn\":\"__time\"},{\"queryColumn\":\"dim1\",\"outputColumn\":\"dim1\"},"
+ +
"{\"queryColumn\":\"dim2\",\"outputColumn\":\"dim2\"},{\"queryColumn\":\"dim3\",\"outputColumn\":\"dim3\"},{\"queryColumn\":\"cnt\",\"outputColumn\":\"cnt\"},"
+ +
"{\"queryColumn\":\"m1\",\"outputColumn\":\"m1\"},{\"queryColumn\":\"m2\",\"outputColumn\":\"m2\"},{\"queryColumn\":\"unique_dim1\",\"outputColumn\":\"unique_dim1\"}]}]";
+ final String resources =
"[{\"name\":\"dst\",\"type\":\"DATASOURCE\"},{\"name\":\"foo\",\"type\":\"DATASOURCE\"}]";
+ final String attributes =
"{\"statementType\":\"REPLACE\",\"targetDataSource\":\"dst\",\"partitionedBy\":\"HOUR\","
+ +
"\"clusteredBy\":[\"__time\",\"dim1\",\"dim3\",\"dim2\"],\"replaceTimeChunks\":\"2000-01-01T00:00:00.000Z/2000-01-02T00:00:00.000Z\"}";
+
+ final String sql = "EXPLAIN PLAN FOR"
+ + " 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 LIMIT 10"
+ + " PARTITIONED BY HOUR CLUSTERED BY __time, dim1, 4,
dim2";
+
// Use testQuery for EXPLAIN (not testIngestionQuery).
testQuery(
PLANNER_CONFIG_LEGACY_QUERY_EXPLAIN,
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 01f0544e15..b47e5bfeaa 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
@@ -21,6 +21,7 @@ package org.apache.druid.sql.calcite.parser;
import com.google.common.collect.ImmutableList;
import org.apache.calcite.avatica.util.TimeUnit;
+import org.apache.calcite.sql.SqlAsOperator;
import org.apache.calcite.sql.SqlBasicCall;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlIntervalQualifier;
@@ -28,7 +29,9 @@ 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.druid.error.DruidException;
@@ -43,6 +46,8 @@ import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import java.util.Arrays;
+
@RunWith(Enclosed.class)
public class DruidSqlParserUtilsTest
{
@@ -125,6 +130,214 @@ public class DruidSqlParserUtilsTest
}
}
+ /**
+ * Test class that validates the resolution of "CLUSTERED BY" columns to
output columns.
+ */
+ public static class ResolveClusteredByColumnsTest
+ {
+ @Test
+ public void testNullClusteredByAndSource()
+ {
+
Assert.assertNull(DruidSqlParserUtils.resolveClusteredByColumnsToOutputColumns(null,
null));
+ }
+
+ @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)
+ )
+ );
+ }
+
+ @Test
+ public void testNullSource()
+ {
+ 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)
+ );
+ 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 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,
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."
+ )
+ );
+ }
+
+
+ @Test
+ public void testClusteredByOrdinalsAndAliases()
+ {
+ // Construct the select source args
+ final SqlNodeList selectArgs = new SqlNodeList(SqlParserPos.ZERO);
+ selectArgs.add(new SqlIdentifier("__time", new SqlParserPos(0, 1)));
+ selectArgs.add(new SqlIdentifier("DIM3", 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)
+ );
+ selectArgs.add(sqlBasicCall1);
+
+ SqlBasicCall sqlBasicCall2 = new SqlBasicCall(
+ new SqlAsOperator(),
+ new SqlNode[]{
+ new SqlIdentifier("FLOOR(__time)", SqlParserPos.ZERO),
+ new SqlIdentifier("floor_dim4_time", SqlParserPos.ZERO)
+ },
+ new SqlParserPos(0, 4)
+ );
+ selectArgs.add(sqlBasicCall2);
+
+ selectArgs.add(new SqlIdentifier("DIM5", new SqlParserPos(0, 5)));
+ selectArgs.add(new SqlIdentifier("DIM6", new SqlParserPos(0, 6)));
+
+ final SqlNodeList args3 = new SqlNodeList(SqlParserPos.ZERO);
+ args3.add(new SqlIdentifier("timestamps", SqlParserPos.ZERO));
+ 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
+ );
+
+ // Construct the clustered by args
+ final SqlNodeList clusteredByArgs = new SqlNodeList(SqlParserPos.ZERO);
+ clusteredByArgs.add(SqlLiteral.createExactNumeric("3",
SqlParserPos.ZERO));
+ clusteredByArgs.add(SqlLiteral.createExactNumeric("4",
SqlParserPos.ZERO));
+ clusteredByArgs.add(SqlLiteral.createExactNumeric("5",
SqlParserPos.ZERO));
+ clusteredByArgs.add(SqlLiteral.createExactNumeric("7",
SqlParserPos.ZERO));
+
+ 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)
+ );
+ }
+ }
+
public static class ClusteredByColumnsValidationTest
{
/**
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlUnparseTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlUnparseTest.java
index 20ea22c67a..3f41bf7df3 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlUnparseTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/parser/DruidSqlUnparseTest.java
@@ -30,7 +30,7 @@ import java.io.StringReader;
import static org.junit.Assert.assertEquals;
/**
- * A class containing unit tests for testing implmentations of {@link
org.apache.calcite.sql.SqlNode#unparse(SqlWriter, int, int)}
+ * A class containing unit tests for testing implementations of {@link
org.apache.calcite.sql.SqlNode#unparse(SqlWriter, int, int)}
* in custom Druid SqlNode classes, like {@link DruidSqlInsert} and {@link
DruidSqlReplace}.
*/
public class DruidSqlUnparseTest
@@ -80,7 +80,6 @@ public class DruidSqlUnparseTest
+ "CLUSTERED BY \"dim1\"";
DruidSqlParserImpl druidSqlParser = createTestParser(sqlQuery);
DruidSqlReplace druidSqlReplace = (DruidSqlReplace)
druidSqlParser.DruidSqlReplaceEof();
-
druidSqlReplace.unparse(sqlWriter, 0, 0);
assertEquals(prettySqlQuery, sqlWriter.toSqlString().getSql());
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/ExplainAttributesTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/ExplainAttributesTest.java
index 0b3466634c..67f97d6421 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/ExplainAttributesTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/ExplainAttributesTest.java
@@ -21,29 +21,16 @@ package org.apache.druid.sql.calcite.planner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.calcite.sql.SqlNode;
-import org.apache.calcite.sql.SqlNodeList;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Test;
-import org.mockito.Mockito;
+
+import java.util.Arrays;
public class ExplainAttributesTest
{
private static final ObjectMapper DEFAULT_OBJECT_MAPPER = new
DefaultObjectMapper();
- private static final SqlNode DATA_SOURCE = Mockito.mock(SqlNode.class);
- private static final SqlNodeList CLUSTERED_BY =
Mockito.mock(SqlNodeList.class);
- private static final SqlNode TIME_CHUNKS = Mockito.mock(SqlNode.class);
-
- @Before
- public void setup()
- {
- Mockito.when(DATA_SOURCE.toString()).thenReturn("foo");
- Mockito.when(CLUSTERED_BY.toString()).thenReturn("`bar`, `jazz`");
- Mockito.when(TIME_CHUNKS.toString()).thenReturn("ALL");
- }
@Test
public void testSimpleGetters()
@@ -77,7 +64,7 @@ public class ExplainAttributesTest
{
ExplainAttributes insertAttributes = new ExplainAttributes(
"INSERT",
- DATA_SOURCE,
+ "foo",
Granularities.DAY,
null,
null
@@ -95,7 +82,7 @@ public class ExplainAttributesTest
{
ExplainAttributes insertAttributes = new ExplainAttributes(
"INSERT",
- DATA_SOURCE,
+ "foo",
Granularities.ALL,
null,
null
@@ -111,20 +98,72 @@ public class ExplainAttributesTest
@Test
public void testSerializeReplaceAttributes() throws JsonProcessingException
{
- ExplainAttributes replaceAttributes = new ExplainAttributes(
+ ExplainAttributes replaceAttributes1 = new ExplainAttributes(
"REPLACE",
- DATA_SOURCE,
+ "foo",
Granularities.HOUR,
- CLUSTERED_BY,
- TIME_CHUNKS
+ null,
+ "ALL"
);
- final String expectedAttributes = "{"
+ final String expectedAttributes1 = "{"
+ "\"statementType\":\"REPLACE\","
+ "\"targetDataSource\":\"foo\","
+ "\"partitionedBy\":\"HOUR\","
- + "\"clusteredBy\":\"`bar`, `jazz`\","
+ "\"replaceTimeChunks\":\"ALL\""
+ "}";
- Assert.assertEquals(expectedAttributes,
DEFAULT_OBJECT_MAPPER.writeValueAsString(replaceAttributes));
+ Assert.assertEquals(expectedAttributes1,
DEFAULT_OBJECT_MAPPER.writeValueAsString(replaceAttributes1));
+
+
+ ExplainAttributes replaceAttributes2 = new ExplainAttributes(
+ "REPLACE",
+ "foo",
+ Granularities.HOUR,
+ null,
+ "2019-08-25T02:00:00.000Z/2019-08-25T03:00:00.000Z"
+ );
+ final String expectedAttributes2 = "{"
+ + "\"statementType\":\"REPLACE\","
+ + "\"targetDataSource\":\"foo\","
+ + "\"partitionedBy\":\"HOUR\","
+ +
"\"replaceTimeChunks\":\"2019-08-25T02:00:00.000Z/2019-08-25T03:00:00.000Z\""
+ + "}";
+ Assert.assertEquals(expectedAttributes2,
DEFAULT_OBJECT_MAPPER.writeValueAsString(replaceAttributes2));
+ }
+
+ @Test
+ public void testSerializeReplaceWithClusteredByAttributes() throws
JsonProcessingException
+ {
+ ExplainAttributes replaceAttributes1 = new ExplainAttributes(
+ "REPLACE",
+ "foo",
+ Granularities.HOUR,
+ Arrays.asList("foo", "CEIL(`f2`)"),
+ "ALL"
+ );
+ final String expectedAttributes1 = "{"
+ + "\"statementType\":\"REPLACE\","
+ + "\"targetDataSource\":\"foo\","
+ + "\"partitionedBy\":\"HOUR\","
+ +
"\"clusteredBy\":[\"foo\",\"CEIL(`f2`)\"],"
+ + "\"replaceTimeChunks\":\"ALL\""
+ + "}";
+ Assert.assertEquals(expectedAttributes1,
DEFAULT_OBJECT_MAPPER.writeValueAsString(replaceAttributes1));
+
+
+ ExplainAttributes replaceAttributes2 = new ExplainAttributes(
+ "REPLACE",
+ "foo",
+ Granularities.HOUR,
+ Arrays.asList("foo", "boo"),
+ "2019-08-25T02:00:00.000Z/2019-08-25T03:00:00.000Z"
+ );
+ final String expectedAttributes2 = "{"
+ + "\"statementType\":\"REPLACE\","
+ + "\"targetDataSource\":\"foo\","
+ + "\"partitionedBy\":\"HOUR\","
+ + "\"clusteredBy\":[\"foo\",\"boo\"],"
+ +
"\"replaceTimeChunks\":\"2019-08-25T02:00:00.000Z/2019-08-25T03:00:00.000Z\""
+ + "}";
+ Assert.assertEquals(expectedAttributes2,
DEFAULT_OBJECT_MAPPER.writeValueAsString(replaceAttributes2));
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]