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]

Reply via email to