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 14d31578f49aca7c57d14836dff8f03583783843 Author: JackieTien97 <[email protected]> AuthorDate: Thu Apr 2 16:06:15 2026 +0800 add IT --- .../it/query/recent/IoTExplainJsonFormatIT.java | 142 ++++++++++++++++++++- .../TableModelStatementMemorySourceVisitor.java | 2 +- 2 files changed, 142 insertions(+), 2 deletions(-) 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 e023d6ce386..0f3d0313124 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 @@ -26,6 +26,7 @@ import org.apache.iotdb.it.framework.IoTDBTestRunner; import org.apache.iotdb.itbase.category.TableLocalStandaloneIT; import org.apache.iotdb.itbase.env.BaseEnv; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.junit.AfterClass; @@ -52,7 +53,12 @@ public class IoTExplainJsonFormatIT { public static void setUp() { Locale.setDefault(Locale.ENGLISH); - EnvFactory.getEnv().getConfig().getCommonConfig().setPartitionInterval(1000); + EnvFactory.getEnv() + .getConfig() + .getCommonConfig() + .setPartitionInterval(1000) + .setMemtableSizeThreshold(10000) + .setMaxRowsInCteBuffer(100); EnvFactory.getEnv().initClusterEnvironment(); try (Connection connection = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); @@ -61,9 +67,12 @@ public class IoTExplainJsonFormatIT { statement.execute("USE " + DATABASE_NAME); statement.execute( "CREATE TABLE IF NOT EXISTS testtb(deviceid STRING TAG, voltage FLOAT FIELD)"); + // Insert data across multiple time partitions (partitionInterval=1000) statement.execute("INSERT INTO testtb VALUES(1000, 'd1', 100.0)"); statement.execute("INSERT INTO testtb VALUES(2000, 'd1', 200.0)"); + statement.execute("INSERT INTO testtb VALUES(3000, 'd1', 150.0)"); statement.execute("INSERT INTO testtb VALUES(1000, 'd2', 300.0)"); + statement.execute("INSERT INTO testtb VALUES(2000, 'd2', 250.0)"); } catch (Exception e) { fail(e.getMessage()); } @@ -254,4 +263,135 @@ public class IoTExplainJsonFormatIT { || e.getMessage().toUpperCase().contains("XML")); } } + + @Test + public void testExplainAnalyzeJsonMultipleFragmentInstances() { + String sql = "EXPLAIN ANALYZE (FORMAT JSON) SELECT * FROM testtb"; + try (Connection conn = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = conn.createStatement()) { + statement.execute("USE " + DATABASE_NAME); + ResultSet resultSet = statement.executeQuery(sql); + + Assert.assertTrue(resultSet.next()); + String jsonStr = resultSet.getString(1); + JsonObject root = JsonParser.parseString(jsonStr).getAsJsonObject(); + + // Verify fragmentInstancesCount matches the size of fragmentInstances array + int declaredCount = root.get("fragmentInstancesCount").getAsInt(); + JsonArray fragmentInstances = root.getAsJsonArray("fragmentInstances"); + Assert.assertNotNull("fragmentInstances array should be present", fragmentInstances); + Assert.assertEquals( + "fragmentInstancesCount should match fragmentInstances array size", + declaredCount, + fragmentInstances.size()); + Assert.assertTrue( + "Should have at least 2 fragment instances for multi-partition data", declaredCount >= 2); + + // Verify each fragment instance has required fields + for (int i = 0; i < fragmentInstances.size(); i++) { + JsonObject fi = fragmentInstances.get(i).getAsJsonObject(); + Assert.assertTrue("Fragment instance should have 'id'", fi.has("id")); + Assert.assertTrue("Fragment instance should have 'state'", fi.has("state")); + Assert.assertTrue("Fragment instance should have 'dataRegion'", fi.has("dataRegion")); + } + + resultSet.close(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + + @Test + public void testExplainJsonWithCte() { + try (Connection conn = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = conn.createStatement()) { + statement.execute("USE " + DATABASE_NAME); + statement.execute( + "CREATE TABLE IF NOT EXISTS cte_tb(deviceid STRING TAG, voltage FLOAT FIELD)"); + statement.execute("INSERT INTO cte_tb VALUES(1000, 'd1', 50.0)"); + + String sql = + "EXPLAIN (FORMAT JSON) WITH cte1 AS MATERIALIZED (SELECT * FROM cte_tb) " + + "SELECT * FROM testtb WHERE testtb.deviceid IN (SELECT deviceid FROM cte1)"; + ResultSet resultSet = statement.executeQuery(sql); + + Assert.assertTrue(resultSet.next()); + String jsonStr = resultSet.getString(1); + JsonObject root = JsonParser.parseString(jsonStr).getAsJsonObject(); + + // When CTEs are present, the JSON should have cteQueries and mainQuery + Assert.assertTrue("JSON with CTE should have 'cteQueries' field", root.has("cteQueries")); + Assert.assertTrue("JSON with CTE should have 'mainQuery' field", root.has("mainQuery")); + + JsonArray cteQueries = root.getAsJsonArray("cteQueries"); + Assert.assertEquals("Should have exactly 1 CTE query", 1, cteQueries.size()); + + JsonObject cte = cteQueries.get(0).getAsJsonObject(); + Assert.assertTrue("CTE should have 'name' field", cte.has("name")); + Assert.assertEquals("cte1", cte.get("name").getAsString()); + Assert.assertTrue("CTE should have 'plan' field", cte.has("plan")); + + // The main query plan should be a JSON object with 'name' field (plan node) + JsonObject mainQuery = root.getAsJsonObject("mainQuery"); + Assert.assertTrue("Main query plan should have 'name' field", mainQuery.has("name")); + + statement.execute("DROP TABLE IF EXISTS cte_tb"); + resultSet.close(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + + @Test + public void testExplainJsonWithScalarSubquery() { + String sql = + "EXPLAIN (FORMAT JSON) SELECT * FROM testtb " + + "WHERE voltage > (SELECT avg(voltage) FROM testtb)"; + try (Connection conn = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = conn.createStatement()) { + statement.execute("USE " + DATABASE_NAME); + ResultSet resultSet = statement.executeQuery(sql); + + Assert.assertTrue(resultSet.next()); + String jsonStr = resultSet.getString(1); + JsonObject root = JsonParser.parseString(jsonStr).getAsJsonObject(); + + // Verify it's a valid plan tree with children (subquery creates a more complex plan) + Assert.assertTrue("JSON should have 'name' field", root.has("name")); + Assert.assertTrue("JSON should have 'id' field", root.has("id")); + Assert.assertTrue("Plan with scalar subquery should have 'children'", root.has("children")); + + resultSet.close(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + + @Test + public void testExplainAnalyzeJsonWithScalarSubquery() { + String sql = + "EXPLAIN ANALYZE (FORMAT JSON) SELECT * FROM testtb " + + "WHERE voltage > (SELECT avg(voltage) FROM testtb)"; + try (Connection conn = EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT); + Statement statement = conn.createStatement()) { + statement.execute("USE " + DATABASE_NAME); + ResultSet resultSet = statement.executeQuery(sql); + + Assert.assertTrue(resultSet.next()); + String jsonStr = resultSet.getString(1); + JsonObject root = JsonParser.parseString(jsonStr).getAsJsonObject(); + + Assert.assertTrue(root.has("planStatistics")); + Assert.assertTrue(root.has("fragmentInstances")); + Assert.assertTrue(root.has("fragmentInstancesCount")); + + int declaredCount = root.get("fragmentInstancesCount").getAsInt(); + JsonArray fragmentInstances = root.getAsJsonArray("fragmentInstances"); + Assert.assertEquals(declaredCount, fragmentInstances.size()); + + resultSet.close(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } } 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 2633805074d..35ec7bcb1c4 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 @@ -182,7 +182,7 @@ public class TableModelStatementMemorySourceVisitor for (Map.Entry<NodeRef<Table>, Pair<Integer, List<String>>> entry : cteExplainResults.entrySet()) { JsonObject cte = new JsonObject(); - cte.addProperty("name", entry.getKey().getNode().getName()); + cte.addProperty("name", entry.getKey().getNode().getName().toString()); cte.add("plan", JsonParser.parseString(entry.getValue().getRight().get(0))); cteArray.add(cte); }
