This is an automated email from the ASF dual-hosted git repository. jackietien pushed a commit to branch ty/explain_format in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 196df02de9d6eea8a923d3ed67a3f15872defdb3 Author: JackieTien97 <[email protected]> AuthorDate: Thu Apr 2 12:14:20 2026 +0800 Address review feedback for EXPLAIN FORMAT JSON feature - Fix ExplainAnalyzeOperator to only instantiate the needed drawer (TEXT or JSON), avoiding wasted work - Replace hand-concatenated JSON in mergeExplainResultsJson with Gson to prevent injection from unescaped CTE names - Add proper imports in PlanGraphJsonPrinter, replace FQN with simple class names - Use JsonArray for list-type properties instead of String.valueOf() - Fix ExplainAnalyze.equals()/hashCode() to include outputFormat and verbose - Add Javadoc to ExplainOutputFormat documenting valid formats per statement - Default MPPQueryContext.explainOutputFormat to TEXT instead of null - Mark old 5-arg ExplainAnalyzeOperator constructor @Deprecated - Improve testExplainInvalidFormat to assert on error message content - Add common pitfalls section to CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- CLAUDE.md | 137 +++++++++++++++++++++ .../it/query/recent/IoTExplainJsonFormatIT.java | 10 +- .../db/queryengine/common/MPPQueryContext.java | 2 +- .../execution/operator/ExplainAnalyzeOperator.java | 26 ++-- .../TableModelStatementMemorySourceVisitor.java | 43 +++---- .../planner/plan/node/PlanGraphJsonPrinter.java | 116 ++++++++--------- .../plan/relational/sql/ast/ExplainAnalyze.java | 6 +- .../relational/sql/ast/ExplainOutputFormat.java | 9 ++ .../FragmentInstanceStatisticsJsonDrawer.java | 1 + 9 files changed, 254 insertions(+), 96 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..421c68ad8ba --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,137 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Apache IoTDB is a time series database for IoT data. It uses a distributed architecture with ConfigNodes (metadata/coordination) and DataNodes (storage/query). Data is stored in TsFile columnar format (separate repo: https://github.com/apache/tsfile). Current version is 2.0.7-SNAPSHOT. + +## Build Commands + +```bash +# Full build (skip tests) +mvn clean package -pl distribution -am -DskipTests + +# Build a specific module (e.g., datanode) +mvn clean package -pl iotdb-core/datanode -am -DskipTests + +# Run unit tests for a specific module +mvn clean test -pl iotdb-core/datanode + +# Run a single test class +mvn clean test -pl iotdb-core/datanode -Dtest=ClassName + +# Run a single test method +mvn clean test -pl iotdb-core/datanode -Dtest=ClassName#methodName + +# Format code (requires JDK 17+; auto-skipped on JDK <17) +mvn spotless:apply + +# Format code in integration-test module +mvn spotless:apply -P with-integration-tests + +# Check formatting without applying +mvn spotless:check +``` + +## Integration Tests + +Integration tests live in `integration-test/` (not included in default build). They require the `with-integration-tests` profile: + +```bash +# Build template-node first (needed once, or after code changes) +mvn clean package -DskipTests -pl integration-test -am -P with-integration-tests + +# Run tree-model ITs (simple: 1 ConfigNode + 1 DataNode) +mvn clean verify -DskipUTs -pl integration-test -am -P with-integration-tests + +# Run tree-model ITs (cluster: 1 ConfigNode + 3 DataNodes) +mvn clean verify -DskipUTs -pl integration-test -am -PClusterIT -P with-integration-tests + +# Run table-model ITs (simple) +mvn clean verify -DskipUTs -pl integration-test -am -PTableSimpleIT -P with-integration-tests + +# Run table-model ITs (cluster) +mvn clean verify -DskipUTs -pl integration-test -am -PTableClusterIT -P with-integration-tests +``` + +To run integration tests from IntelliJ: enable the `with-integration-tests` profile in Maven sidebar, then run test cases directly. + +## Code Style + +- **Spotless** with Google Java Format (GOOGLE style). Import order: `org.apache.iotdb`, blank, `javax`, `java`, static. +- **Checkstyle** is also configured (see `checkstyle.xml` at project root). +- Java source/target level is 1.8 (compiled with `maven.compiler.release=8` on JDK 9+). + +## Architecture + +### Node Types + +- **ConfigNode** (`iotdb-core/confignode`): Manages cluster metadata, schema regions, data regions, partition tables. Coordinates via Ratis consensus. +- **DataNode** (`iotdb-core/datanode`): Handles data storage, query execution, and client connections. The main server component. +- **AINode** (`iotdb-core/ainode`): Python-based node for AI/ML inference tasks. + +### Dual Data Model + +IoTDB supports two data models operating on the same storage: +- **Tree model**: Traditional IoT hierarchy (e.g., `root.ln.wf01.wt01.temperature`). SQL uses path-based addressing. +- **Table model** (relational): SQL table semantics. Grammar lives in `iotdb-core/relational-grammar/`. Query plan code under `queryengine/plan/relational/`. + +### Key DataNode Subsystems (`iotdb-core/datanode`) + +- **queryengine**: SQL parsing, planning, optimization, and execution. + - `plan/parser/` - ANTLR-based SQL parser + - `plan/statement/` - AST statement nodes + - `plan/planner/` - Logical and physical planning (tree model: `TreeModelPlanner`, table model: under `plan/relational/`) + - `plan/optimization/` - Query optimization rules + - `execution/operator/` - Physical operators (volcano-style iterator model) + - `execution/exchange/` - Inter-node data exchange + - `execution/fragment/` - Distributed query fragment management +- **storageengine**: Write path, memtable, flush, WAL, compaction, TsFile management. + - `dataregion/` - DataRegion lifecycle, memtable, flush, compaction + - `dataregion/wal/` - Write-ahead log + - `buffer/` - Memory buffer management +- **schemaengine**: Schema (timeseries metadata) management. +- **pipe**: Data sync/replication framework (source -> processor -> sink pipeline). +- **consensus**: DataNode-side consensus integration. +- **subscription**: Client subscription service for streaming data changes. + +### Consensus (`iotdb-core/consensus`) + +Pluggable consensus protocols: Simple (single-node), Ratis (Raft-based), IoT Consensus (optimized for IoT writes). Factory pattern via `ConsensusFactory`. + +### Protocol Layer (`iotdb-protocol/`) + +Thrift IDL definitions for RPC between nodes. Generated sources are produced automatically during build. Sub-modules: `thrift-commons`, `thrift-confignode`, `thrift-datanode`, `thrift-consensus`, `thrift-ainode`. + +### Client Libraries (`iotdb-client/`) + +- `session/` - Java Session API (primary client interface) +- `jdbc/` - JDBC driver +- `cli/` - Command-line client +- `client-cpp/`, `client-go/`, `client-py/` - Multi-language clients +- `service-rpc/` - Shared Thrift service definitions + +### API Layer (`iotdb-api/`) + +Extension point interfaces: `udf-api` (user-defined functions), `trigger-api` (event triggers), `pipe-api` (data sync plugins), `external-api`, `external-service-api`. + +## IDE Setup + +After `mvn package`, right-click the root project in IntelliJ and choose "Maven -> Reload Project" to add generated source roots (Thrift and ANTLR). + +Generated source directories that need to be on the source path: +- `**/target/generated-sources/thrift` +- `**/target/generated-sources/antlr4` + +## Common Pitfalls + +### Build + +- **Missing Thrift compiler**: The local machine may not have the `thrift` binary installed. Running `mvn clean package -pl <module> -am -DskipTests` will fail at the `iotdb-thrift` module. **Workaround**: To verify your changes compile, use `mvn compile -pl <module>` (without `-am` or `clean`) to leverage existing target caches. +- **Pre-existing compilation errors in unrelated modules**: The datanode module may have pre-existing compile errors in other subsystems (e.g., pipe, copyto) that cause `mvn clean test -pl iotdb-core/datanode -Dtest=XxxTest` to fail during compilation. **Workaround**: First run `mvn compile -pl iotdb-core/datanode` to confirm your changed files compile successfully. If the errors are in files you did not modify, they are pre-existing and do not affect your changes. + +### Code Style + +- **Always run `mvn spotless:apply` after editing Java files**: Spotless runs `spotless:check` automatically during the `compile` phase. Format violations cause an immediate BUILD FAILURE. Make it a habit to run `mvn spotless:apply -pl <module>` right after editing, not at the end. For files under `integration-test/`, add `-P with-integration-tests`. +- **Gson version compatibility**: `JsonObject.isEmpty()` / `JsonArray.isEmpty()` may not be available in the Gson version used by this project. Use `size() > 0` instead and add a comment explaining why. diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTExplainJsonFormatIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTExplainJsonFormatIT.java index 94ed692ab64..e023d6ce386 100644 --- a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTExplainJsonFormatIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTExplainJsonFormatIT.java @@ -239,13 +239,19 @@ public class IoTExplainJsonFormatIT { } } - @Test(expected = SQLException.class) - public void testExplainInvalidFormat() throws SQLException { + @Test + public void testExplainInvalidFormat() { String sql = "EXPLAIN (FORMAT XML) SELECT * FROM testtb"; try (Connection conn = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); Statement statement = conn.createStatement()) { statement.execute("USE " + DATABASE_NAME); statement.executeQuery(sql); + fail("Expected SQLException for invalid format"); + } catch (SQLException e) { + Assert.assertTrue( + "Error message should mention the invalid format", + e.getMessage().toUpperCase().contains("FORMAT") + || e.getMessage().toUpperCase().contains("XML")); } } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java index 43b214f226c..8f248c29363 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java @@ -109,7 +109,7 @@ public class MPPQueryContext implements IAuditEntity { // - EXPLAIN: Show the logical and physical query plan without execution // - EXPLAIN_ANALYZE: Execute the query and collect detailed execution statistics private ExplainType explainType = ExplainType.NONE; - private ExplainOutputFormat explainOutputFormat = null; + private ExplainOutputFormat explainOutputFormat = ExplainOutputFormat.TEXT; private boolean verbose = false; private QueryPlanStatistics queryPlanStatistics = null; diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/ExplainAnalyzeOperator.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/ExplainAnalyzeOperator.java index 52af0bb7455..9676c94844a 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/ExplainAnalyzeOperator.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/ExplainAnalyzeOperator.java @@ -82,15 +82,14 @@ public class ExplainAnalyzeOperator implements ProcessOperator { private final List<FragmentInstance> instances; private final ExplainOutputFormat outputFormat; - private final FragmentInstanceStatisticsDrawer fragmentInstanceStatisticsDrawer = - new FragmentInstanceStatisticsDrawer(); - private final FragmentInstanceStatisticsJsonDrawer fragmentInstanceStatisticsJsonDrawer = - new FragmentInstanceStatisticsJsonDrawer(); + private final FragmentInstanceStatisticsDrawer fragmentInstanceStatisticsDrawer; + private final FragmentInstanceStatisticsJsonDrawer fragmentInstanceStatisticsJsonDrawer; private final ScheduledFuture<?> logRecordTask; private final IClientManager<TEndPoint, SyncDataNodeInternalServiceClient> clientManager; private final MPPQueryContext mppQueryContext; + @Deprecated public ExplainAnalyzeOperator( OperatorContext operatorContext, Operator child, @@ -118,8 +117,16 @@ public class ExplainAnalyzeOperator implements ProcessOperator { QueryExecution queryExecution = (QueryExecution) coordinator.getQueryExecution(queryId); this.instances = queryExecution.getDistributedPlan().getInstances(); mppQueryContext = queryExecution.getContext(); - fragmentInstanceStatisticsDrawer.renderPlanStatistics(mppQueryContext); - fragmentInstanceStatisticsJsonDrawer.renderPlanStatistics(mppQueryContext); + + if (outputFormat == ExplainOutputFormat.JSON) { + this.fragmentInstanceStatisticsDrawer = null; + this.fragmentInstanceStatisticsJsonDrawer = new FragmentInstanceStatisticsJsonDrawer(); + fragmentInstanceStatisticsJsonDrawer.renderPlanStatistics(mppQueryContext); + } else { + this.fragmentInstanceStatisticsDrawer = new FragmentInstanceStatisticsDrawer(); + this.fragmentInstanceStatisticsJsonDrawer = null; + fragmentInstanceStatisticsDrawer.renderPlanStatistics(mppQueryContext); + } // The time interval guarantees the result of EXPLAIN ANALYZE will be printed at least three // times. @@ -146,8 +153,11 @@ public class ExplainAnalyzeOperator implements ProcessOperator { return null; } - fragmentInstanceStatisticsDrawer.renderDispatchCost(mppQueryContext); - fragmentInstanceStatisticsJsonDrawer.renderDispatchCost(mppQueryContext); + if (outputFormat == ExplainOutputFormat.JSON) { + fragmentInstanceStatisticsJsonDrawer.renderDispatchCost(mppQueryContext); + } else { + fragmentInstanceStatisticsDrawer.renderDispatchCost(mppQueryContext); + } // fetch statics from all fragment instances TsBlock result = buildResult(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/memory/TableModelStatementMemorySourceVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/memory/TableModelStatementMemorySourceVisitor.java index c6a7692395a..2633805074d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/memory/TableModelStatementMemorySourceVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/memory/TableModelStatementMemorySourceVisitor.java @@ -41,6 +41,11 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Node; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ShowDevice; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Table; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.read.common.block.TsBlock; import org.apache.tsfile.utils.Pair; @@ -60,6 +65,8 @@ import static org.apache.iotdb.db.queryengine.plan.execution.memory.StatementMem public class TableModelStatementMemorySourceVisitor extends AstVisitor<StatementMemorySource, TableModelStatementMemorySourceContext> { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + @Override public StatementMemorySource visitNode( final Node node, final TableModelStatementMemorySourceContext context) { @@ -170,36 +177,18 @@ public class TableModelStatementMemorySourceVisitor return mainExplainResult; } - // For JSON format with CTEs, wrap everything in a combined JSON object - StringBuilder sb = new StringBuilder(); - sb.append("{\n"); - sb.append(" \"cteQueries\": [\n"); - int cteIndex = 0; - int cteSize = cteExplainResults.size(); + JsonObject wrapper = new JsonObject(); + JsonArray cteArray = new JsonArray(); for (Map.Entry<NodeRef<Table>, Pair<Integer, List<String>>> entry : cteExplainResults.entrySet()) { - sb.append(" {\n"); - sb.append(" \"name\": \"").append(entry.getKey().getNode().getName()).append("\",\n"); - sb.append(" \"plan\": "); - // Each CTE's plan is already a JSON string - for (String line : entry.getValue().getRight()) { - sb.append(line); - } - sb.append("\n }"); - if (++cteIndex < cteSize) { - sb.append(","); - } - sb.append("\n"); - } - sb.append(" ],\n"); - sb.append(" \"mainQuery\": "); - for (String line : mainExplainResult) { - sb.append(line); + JsonObject cte = new JsonObject(); + cte.addProperty("name", entry.getKey().getNode().getName()); + cte.add("plan", JsonParser.parseString(entry.getValue().getRight().get(0))); + cteArray.add(cte); } - sb.append("\n}"); + wrapper.add("cteQueries", cteArray); + wrapper.add("mainQuery", JsonParser.parseString(mainExplainResult.get(0))); - List<String> result = new ArrayList<>(); - result.add(sb.toString()); - return result; + return Collections.singletonList(GSON.toJson(wrapper)); } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java index e35c23d4660..97e7d88328f 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java @@ -19,12 +19,21 @@ package org.apache.iotdb.db.queryengine.plan.planner.plan.node; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExplainAnalyzeNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.FilterNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.JoinNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.LimitNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.MergeSortNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.OffsetNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.OutputNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode; +import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -59,6 +68,7 @@ public class PlanGraphJsonPrinter { jsonNode.addProperty("id", nodeId); JsonObject properties = buildProperties(node); + // JsonObject.isEmpty() is not available in all Gson versions if (properties.size() > 0) { jsonNode.add("properties", properties); } @@ -80,71 +90,68 @@ public class PlanGraphJsonPrinter { if (node instanceof OutputNode) { OutputNode n = (OutputNode) node; - properties.addProperty("OutputColumns", String.valueOf(n.getOutputColumnNames())); - properties.addProperty("OutputSymbols", String.valueOf(n.getOutputSymbols())); + properties.add("OutputColumns", toJsonArray(n.getOutputColumnNames())); + properties.add("OutputSymbols", toJsonArray(n.getOutputSymbols())); } else if (node instanceof ExplainAnalyzeNode) { ExplainAnalyzeNode n = (ExplainAnalyzeNode) node; - properties.addProperty("ChildPermittedOutputs", String.valueOf(n.getChildPermittedOutputs())); + properties.add("ChildPermittedOutputs", toJsonArray(n.getChildPermittedOutputs())); } else if (node instanceof TableScanNode) { buildTableScanProperties(properties, (TableScanNode) node); } else if (node instanceof ExchangeNode) { // No extra properties needed - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode) { - buildAggregationProperties( - properties, - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode) node); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.FilterNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.FilterNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.FilterNode) node; + } else if (node instanceof AggregationNode) { + buildAggregationProperties(properties, (AggregationNode) node); + } else if (node instanceof FilterNode) { + FilterNode n = (FilterNode) node; properties.addProperty("Predicate", String.valueOf(n.getPredicate())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode) node; - properties.addProperty("OutputSymbols", String.valueOf(n.getOutputSymbols())); - properties.addProperty("Expressions", String.valueOf(n.getAssignments().getMap().values())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.LimitNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.LimitNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.LimitNode) node; - properties.addProperty("Count", String.valueOf(n.getCount())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.OffsetNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.OffsetNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.OffsetNode) node; - properties.addProperty("Count", String.valueOf(n.getCount())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode) node; + } else if (node instanceof ProjectNode) { + ProjectNode n = (ProjectNode) node; + properties.add("OutputSymbols", toJsonArray(n.getOutputSymbols())); + properties.add("Expressions", toJsonArray(n.getAssignments().getMap().values())); + } else if (node instanceof LimitNode) { + LimitNode n = (LimitNode) node; + properties.addProperty("Count", n.getCount()); + } else if (node instanceof OffsetNode) { + OffsetNode n = (OffsetNode) node; + properties.addProperty("Count", n.getCount()); + } else if (node instanceof SortNode) { + SortNode n = (SortNode) node; properties.addProperty("OrderBy", String.valueOf(n.getOrderingScheme())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.MergeSortNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.MergeSortNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.MergeSortNode) node; + } else if (node instanceof MergeSortNode) { + MergeSortNode n = (MergeSortNode) node; properties.addProperty("OrderBy", String.valueOf(n.getOrderingScheme())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.JoinNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.JoinNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.JoinNode) node; + } else if (node instanceof JoinNode) { + JoinNode n = (JoinNode) node; properties.addProperty("JoinType", String.valueOf(n.getJoinType())); - properties.addProperty("Criteria", String.valueOf(n.getCriteria())); - properties.addProperty("OutputSymbols", String.valueOf(n.getOutputSymbols())); - } else if (node - instanceof org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode) { - org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode n = - (org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode) node; - properties.addProperty("OutputSymbols", String.valueOf(n.getOutputSymbols())); + properties.add("Criteria", toJsonArray(n.getCriteria())); + properties.add("OutputSymbols", toJsonArray(n.getOutputSymbols())); + } else if (node instanceof UnionNode) { + UnionNode n = (UnionNode) node; + properties.add("OutputSymbols", toJsonArray(n.getOutputSymbols())); } return properties; } + private static <T> JsonArray toJsonArray(java.util.Collection<T> items) { + JsonArray array = new JsonArray(); + for (T item : items) { + array.add(String.valueOf(item)); + } + return array; + } + + private static <T> JsonArray toJsonArray(List<T> items) { + JsonArray array = new JsonArray(); + for (T item : items) { + array.add(String.valueOf(item)); + } + return array; + } + private static void buildTableScanProperties(JsonObject properties, TableScanNode node) { properties.addProperty("QualifiedTableName", node.getQualifiedObjectName().toString()); - properties.addProperty("OutputSymbols", String.valueOf(node.getOutputSymbols())); + properties.add("OutputSymbols", toJsonArray(node.getOutputSymbols())); if (node instanceof DeviceTableScanNode) { DeviceTableScanNode deviceNode = (DeviceTableScanNode) node; @@ -182,15 +189,12 @@ public class PlanGraphJsonPrinter { } } - private static void buildAggregationProperties( - JsonObject properties, - org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode node) { - properties.addProperty("OutputSymbols", String.valueOf(node.getOutputSymbols())); + private static void buildAggregationProperties(JsonObject properties, AggregationNode node) { + properties.add("OutputSymbols", toJsonArray(node.getOutputSymbols())); JsonArray aggregators = new JsonArray(); int i = 0; - for (org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode.Aggregation - aggregation : node.getAggregations().values()) { + for (AggregationNode.Aggregation aggregation : node.getAggregations().values()) { JsonObject agg = new JsonObject(); agg.addProperty("index", i++); agg.addProperty("function", aggregation.getResolvedFunction().toString()); @@ -204,10 +208,10 @@ public class PlanGraphJsonPrinter { } properties.add("Aggregators", aggregators); - properties.addProperty("GroupingKeys", String.valueOf(node.getGroupingKeys())); + properties.add("GroupingKeys", toJsonArray(node.getGroupingKeys())); if (node.isStreamable()) { properties.addProperty("Streamable", true); - properties.addProperty("PreGroupedSymbols", String.valueOf(node.getPreGroupedSymbols())); + properties.add("PreGroupedSymbols", toJsonArray(node.getPreGroupedSymbols())); } properties.addProperty("Step", String.valueOf(node.getStep())); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainAnalyze.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainAnalyze.java index dc7798967e4..b824705df18 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainAnalyze.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainAnalyze.java @@ -86,7 +86,7 @@ public class ExplainAnalyze extends Statement { @Override public int hashCode() { - return Objects.hash(statement, verbose); + return Objects.hash(statement, verbose, outputFormat); } @Override @@ -98,7 +98,9 @@ public class ExplainAnalyze extends Statement { return false; } ExplainAnalyze o = (ExplainAnalyze) obj; - return Objects.equals(statement, o.statement); + return Objects.equals(statement, o.statement) + && verbose == o.verbose + && outputFormat == o.outputFormat; } @Override diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainOutputFormat.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainOutputFormat.java index 9dba3649a7f..d7d1fc08bb3 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainOutputFormat.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/ExplainOutputFormat.java @@ -19,6 +19,15 @@ package org.apache.iotdb.db.queryengine.plan.relational.sql.ast; +/** + * Output format for EXPLAIN and EXPLAIN ANALYZE statements. + * + * <ul> + * <li>{@link #GRAPHVIZ} - Box-drawing plan visualization. Valid for EXPLAIN only (default). + * <li>{@link #TEXT} - Text-based output. Valid for EXPLAIN ANALYZE only (default). + * <li>{@link #JSON} - Structured JSON output. Valid for both EXPLAIN and EXPLAIN ANALYZE. + * </ul> + */ public enum ExplainOutputFormat { GRAPHVIZ, TEXT, diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/statistics/FragmentInstanceStatisticsJsonDrawer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/statistics/FragmentInstanceStatisticsJsonDrawer.java index 3c6ff7bfb41..2b00d6191fe 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/statistics/FragmentInstanceStatisticsJsonDrawer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/statistics/FragmentInstanceStatisticsJsonDrawer.java @@ -345,6 +345,7 @@ public class FragmentInstanceStatisticsJsonDrawer { childrenArray.add(childJson); } } + // JsonArray.isEmpty() is not available in all Gson versions if (childrenArray.size() > 0) { operatorJson.add("children", childrenArray); }
