diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitor.java index d05d1a1ff2a88..ab2f2ec6e2fe5 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitor.java @@ -29,16 +29,20 @@ import org.apache.tsfile.write.schema.IMeasurementSchema; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.TreeMap; import static org.apache.iotdb.commons.schema.SchemaConstant.NON_TEMPLATE; public abstract class SchemaTreeVisitor extends AbstractTreeVisitor { private Map templateMap; + // Reuse the ordered snapshot for devices that share a template. + private final Map> templateMeasurements = new HashMap<>(); protected SchemaTreeVisitor() {} @@ -63,6 +67,7 @@ public List getAllResult() { public void setTemplateMap(Map templateMap) { this.templateMap = templateMap; + templateMeasurements.clear(); } @Override @@ -123,10 +128,12 @@ protected Iterator getChildrenIterator(SchemaNode parent) { && parent.getAsEntityNode().getTemplateId() != NON_TEMPLATE) { return new Iterator() { private final Iterator templateChildrenIterator = - templateMap - .get(parent.getAsEntityNode().getTemplateId()) - .getSchemaMap() - .values() + templateMeasurements + .computeIfAbsent( + parent.getAsEntityNode().getTemplateId(), + templateId -> + new ArrayList<>( + new TreeMap<>(templateMap.get(templateId).getSchemaMap()).values())) .iterator(); private final Iterator directChildrenIterator = parent.getChildrenIterator(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitorWithLimitOffsetWrapper.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitorWithLimitOffsetWrapper.java index 208b63a62ae40..5d7382e79570e 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitorWithLimitOffsetWrapper.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/schematree/visitor/SchemaTreeVisitorWithLimitOffsetWrapper.java @@ -42,13 +42,6 @@ public SchemaTreeVisitorWithLimitOffsetWrapper( this.limit = limit; this.offset = offset; hasLimit = limit > 0 || offset > 0; - - if (hasLimit) { - while (curOffset < offset && visitor.hasNext()) { - visitor.next(); - curOffset++; - } - } } public void setTemplateMap(Map templateMap) { @@ -57,11 +50,12 @@ public void setTemplateMap(Map templateMap) { @Override public boolean hasNext() { - if (hasLimit) { - return count < limit && visitor.hasNext(); - } else { - return visitor.hasNext(); + // Template metadata is set after construction, so defer traversal until iteration starts. + while (curOffset < offset && visitor.hasNext()) { + visitor.next(); + curOffset++; } + return (limit == 0 || count < limit) && visitor.hasNext(); } @Override @@ -119,12 +113,6 @@ public void reset() { visitor.reset(); count = 0; curOffset = 0; - if (hasLimit) { - while (curOffset < offset && visitor.hasNext()) { - visitor.next(); - curOffset++; - } - } } public int getNextOffset() { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java index 018f171b13252..a81a8bcc767b2 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java @@ -1214,7 +1214,7 @@ private void analyzeGroupByTag( List tagKeys = queryStatement.getGroupByTagComponent().getTagKeys(); Map, LinkedHashMap>> - tagValuesToGroupedTimeseriesOperands = new HashMap<>(); + tagValuesToGroupedTimeseriesOperands = new LinkedHashMap<>(); LinkedHashMap> outputExpressionToRawExpressionsMap = new LinkedHashMap<>(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java index 0de197964e245..ffec2dd67bef4 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAggregationAnalyze.java @@ -45,6 +45,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import static org.apache.iotdb.calc.utils.constant.SqlConstant.COUNT_TIME; import static org.apache.iotdb.commons.schema.column.ColumnHeaderConstant.ENDTIME; @@ -106,6 +107,11 @@ static boolean canBuildAggregationPlanUseTemplate( return false; } + if (outputExpressions.isEmpty()) { + analysis.setFinishQueryAfterAnalyze(true); + return true; + } + analyzeDeviceToWhere(analysis, queryStatement); if (deviceList.isEmpty()) { analysis.setFinishQueryAfterAnalyze(true); @@ -180,6 +186,13 @@ private static boolean analyzeSelect( if (selectExpression instanceof FunctionExpression && COUNT_TIME.equalsIgnoreCase( ((FunctionExpression) selectExpression).getFunctionName())) { + if (paginationController.hasCurOffset()) { + paginationController.consumeOffset(); + continue; + } + if (!paginationController.hasCurLimit()) { + break; + } FunctionExpression countTimeExpression = (FunctionExpression) selectExpression; // The parsed count_time(*) expression belongs to the statement and may be analyzed again // after a dispatch failure. Build a template-only expression instead of replacing its @@ -203,6 +216,7 @@ private static boolean analyzeSelect( analysis .getExpressionTypes() .put(NodeRef.of(templatedCountTimeExpression), TSDataType.INT64); + paginationController.consumeLimit(); continue; } @@ -214,7 +228,9 @@ private static boolean analyzeSelect( && "*".equalsIgnoreCase(selectExpression.getExpressions().get(0).getOutputSymbol())) { subExpressions = new ArrayList<>(); FunctionExpression functionExpression = (FunctionExpression) selectExpression; - for (Map.Entry entry : template.getSchemaMap().entrySet()) { + // Use the same stable wildcard order as raw-data queries, before applying pagination. + for (Map.Entry entry : + new TreeMap<>(template.getSchemaMap()).entrySet()) { FunctionExpression subFunctionExpression = new FunctionExpression( functionExpression.getFunctionName(), @@ -241,6 +257,7 @@ private static boolean analyzeSelect( selectExpressions.add(expression); aggregationExpressions.add(expression); analyzeExpressionType(analysis, expression); + paginationController.consumeLimit(); } else { break; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java index 350cc5d090ea5..287b2e6460142 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java @@ -56,6 +56,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; import static org.apache.iotdb.db.queryengine.metric.QueryPlanCostMetricSet.PARTITION_FETCHER; @@ -134,7 +135,9 @@ public static boolean canBuildPlanUseTemplate( for (ResultColumn resultColumn : queryStatement.getSelectComponent().getResultColumns()) { Expression expression = resultColumn.getExpression(); if ("*".equals(expression.getOutputSymbol())) { - for (Map.Entry entry : template.getSchemaMap().entrySet()) { + // Template map iteration can change after serialization. Order columns before pagination. + for (Map.Entry entry : + new TreeMap<>(template.getSchemaMap()).entrySet()) { if (paginationController.hasCurOffset()) { paginationController.consumeOffset(); } else if (paginationController.hasCurLimit()) { @@ -168,6 +171,7 @@ public static boolean canBuildPlanUseTemplate( // reserve memory for this expression context.reserveMemoryForFrontEnd(measurementPath.ramBytesUsed()); outputExpressions.add(new Pair<>(measurementPath, resultColumn.getAlias())); + paginationController.consumeLimit(); } else { break; } @@ -183,6 +187,11 @@ public static boolean canBuildPlanUseTemplate( analyzeSelect(queryStatement, analysis, outputExpressions, template); + if (outputExpressions.isEmpty()) { + analysis.setFinishQueryAfterAnalyze(true); + return true; + } + analyzeDeviceToWhere(analysis, queryStatement); if (analysis.getWhereExpression() != null && analysis.getWhereExpression().equals(ConstantOperand.FALSE)) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java index b976ce4ba2c66..e3b7566cb5754 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilder.java @@ -118,7 +118,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -820,7 +819,7 @@ private PlanNode createGroupByTagNode( GroupByTimeParameter groupByTimeParameter, Ordering scanOrder) { Map, List> tagValuesToAggregationDescriptors = - new HashMap<>(); + new LinkedHashMap<>(); for (List tagValues : tagValuesToGroupedTimeseriesOperands.keySet()) { LinkedHashMap> groupedTimeseriesOperands = tagValuesToGroupedTimeseriesOperands.get(tagValues); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/process/GroupByTagNode.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/process/GroupByTagNode.java index d96100bfac81a..b5485738f536f 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/process/GroupByTagNode.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/process/GroupByTagNode.java @@ -38,7 +38,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -253,7 +253,7 @@ public static GroupByTagNode deserialize(ByteBuffer byteBuffer) { // Tag values to aggregation descriptors. int numOfEntries = ReadWriteIOUtils.readInt(byteBuffer); Map, List> tagValuesToAggregationDescriptors = - new HashMap<>(); + new LinkedHashMap<>(); while (numOfEntries > 0) { List tagValues = ReadWriteIOUtils.readStringList(byteBuffer); List aggregationDescriptors = new ArrayList<>(); diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/GroupByTagPlanningOrderTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/GroupByTagPlanningOrderTest.java new file mode 100644 index 0000000000000..0f2140403cb67 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/GroupByTagPlanningOrderTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.queryengine.plan.analyze; + +import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.path.PathPatternTree; +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.db.queryengine.common.MPPQueryContext; +import org.apache.iotdb.db.queryengine.common.QueryId; +import org.apache.iotdb.db.queryengine.common.schematree.ClusterSchemaTree; +import org.apache.iotdb.db.queryengine.plan.expression.Expression; +import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator; +import org.apache.iotdb.db.queryengine.plan.planner.LogicalPlanBuilder; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.GroupByTagNode; +import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering; + +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.junit.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class GroupByTagPlanningOrderTest { + + @Test + public void testAnalysisPreservesGroupEncounterOrder() throws Exception { + Analysis analysis = analyze(createQueryContext()); + assertEquals( + Arrays.asList(Collections.singletonList("v1"), Collections.singletonList("v0")), + new ArrayList<>(analysis.getTagValuesToGroupedTimeseriesOperands().keySet())); + } + + @Test + public void testLogicalPlanPreservesAnalyzedGroupOrder() throws Exception { + MPPQueryContext context = createQueryContext(); + Analysis analysis = analyze(context); + Map, LinkedHashMap>> groups = new LinkedHashMap<>(); + for (String value : Arrays.asList("v1", "v0")) { + List tagValues = Collections.singletonList(value); + groups.put(tagValues, analysis.getTagValuesToGroupedTimeseriesOperands().get(tagValues)); + } + + GroupByTagNode node = + (GroupByTagNode) + new LogicalPlanBuilder(analysis, context) + .planRawDataSource(analysis.getSourceExpressions(), Ordering.ASC, 0, 0, false) + .planGroupByTag( + analysis.getCrossGroupByExpressions(), + analysis.getTagKeys(), + groups, + null, + Ordering.ASC) + .getRoot(); + assertEquals( + new ArrayList<>(groups.keySet()), + new ArrayList<>(node.getTagValuesToAggregationDescriptors().keySet())); + } + + private Analysis analyze(MPPQueryContext context) throws Exception { + ClusterSchemaTree testSchemaTree = new ClusterSchemaTree(); + testSchemaTree.setDatabases(Collections.singleton("root.sg")); + for (int i = 1; i <= 2; i++) { + testSchemaTree.appendSingleMeasurement( + new PartialPath("root.sg.d" + i + ".s1"), + new MeasurementSchema("s1", TSDataType.INT32), + Collections.singletonMap("key1", i == 1 ? "v1" : "v0"), + null, + null, + false); + } + FakeSchemaFetcherImpl schemaFetcher = + new FakeSchemaFetcherImpl() { + @Override + public ClusterSchemaTree fetchSchemaWithTags( + PathPatternTree patternTree, + boolean withTemplate, + MPPQueryContext queryContext, + boolean canSeeAuditDB) { + return testSchemaTree; + } + }; + return new Analyzer(context, new FakePartitionFetcherImpl(), schemaFetcher) + .analyze( + StatementGenerator.createStatement( + "select count(s1) from root.sg.* group by tags(key1)", + ZonedDateTime.now().getOffset())); + } + + private MPPQueryContext createQueryContext() { + return new MPPQueryContext( + "", + new QueryId("test_query"), + new SessionInfo(0, "root", ZoneId.systemDefault()), + new TEndPoint(), + new TEndPoint()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplateQueryOrderTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplateQueryOrderTest.java new file mode 100644 index 0000000000000..6e8ef9f019e37 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplateQueryOrderTest.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.queryengine.plan.analyze; + +import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.commons.path.MeasurementPath; +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.commons.schema.template.Template; +import org.apache.iotdb.db.queryengine.common.MPPQueryContext; +import org.apache.iotdb.db.queryengine.common.QueryId; +import org.apache.iotdb.db.queryengine.common.schematree.ClusterSchemaTree; +import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator; +import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement; + +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.enums.CompressionType; +import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.junit.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TemplateQueryOrderTest { + + @Test + public void testRawWildcardOrderAfterSerialization() throws Exception { + assertQueryOrder("*", false); + } + + @Test + public void testAggregationWildcardOrderAfterSerialization() throws Exception { + assertQueryOrder("count(*)", true); + } + + @Test + public void testSchemaTreeWildcardOrderAfterSerialization() throws Exception { + for (Template template : createTemplateVersions()) { + List expected = sortedMeasurements(template); + ClusterSchemaTree schemaTree = createSchemaTree(template); + PartialPath pattern = new PartialPath("root.sg.d1.*"); + assertEquals( + expected, + schemaTree.searchMeasurementPaths(pattern).left.stream() + .map(MeasurementPath::getMeasurement) + .collect(Collectors.toList())); + assertEquals( + expected.subList(3, 8), + schemaTree.searchMeasurementPaths(pattern, 5, 3, false).left.stream() + .map(MeasurementPath::getMeasurement) + .collect(Collectors.toList())); + } + } + + @Test + public void testExplicitColumnsHonorSeriesLimit() throws Exception { + for (Template template : createTemplateVersions()) { + List measurements = sortedMeasurements(template); + assertEquals( + measurements.subList(3, 8), + analyzeOutputSymbols( + template, + "select " + + String.join(", ", measurements) + + " from root.sg.* slimit 5 soffset 3 align by device")); + } + } + + @Test + public void testExplicitAggregationsHonorSeriesLimit() throws Exception { + for (Template template : createTemplateVersions()) { + List aggregations = + sortedMeasurements(template).stream() + .map(name -> "count(" + name + ")") + .collect(Collectors.toList()); + assertEquals( + aggregations.subList(3, 8), + analyzeOutputSymbols( + template, + "select " + + String.join(", ", aggregations) + + " from root.sg.* slimit 5 soffset 3 align by device")); + } + } + + @Test + public void testSchemaTreeOffsetWithoutLimit() throws Exception { + for (Template template : createTemplateVersions()) { + List expected = sortedMeasurements(template); + assertEquals( + expected.subList(3, expected.size()), + createSchemaTree(template) + .searchMeasurementPaths(new PartialPath("root.sg.d1.*"), 0, 3, false) + .left + .stream() + .map(MeasurementPath::getMeasurement) + .collect(Collectors.toList())); + } + } + + @Test + public void testCountTimeHonorsSeriesOffset() throws Exception { + for (Template template : createTemplateVersions()) { + assertEmptyProjectionFinishesQuery( + template, "select count_time(*) from root.sg.* slimit 1 soffset 1 align by device"); + } + } + + @Test + public void testRawSeriesOffsetBeyondMeasurementsFinishesQuery() throws Exception { + for (Template template : createTemplateVersions()) { + assertEmptyProjectionFinishesQuery( + template, "select * from root.sg.* slimit 1 soffset 16 align by device"); + } + } + + @Test + public void testAggregationSeriesOffsetBeyondMeasurementsFinishesQuery() throws Exception { + for (Template template : createTemplateVersions()) { + assertEmptyProjectionFinishesQuery( + template, "select count(*) from root.sg.* slimit 1 soffset 16 align by device"); + } + } + + private void assertEmptyProjectionFinishesQuery(Template template, String sql) throws Exception { + Analysis analysis = + analyze( + template, + sql, + Arrays.asList(new PartialPath("root.sg.d1"), new PartialPath("root.sg.d2"))); + assertTrue(analysis.getOutputExpressions().isEmpty()); + assertTrue(analysis.isFinishQueryAfterAnalyze()); + } + + private void assertQueryOrder(String projection, boolean aggregation) throws Exception { + for (Template template : createTemplateVersions()) { + List expected = sortedMeasurements(template); + if (aggregation) { + expected = + expected.stream().map(name -> "count(" + name + ")").collect(Collectors.toList()); + } + assertEquals( + expected, + analyzeOutputSymbols( + template, "select " + projection + " from root.sg.* align by device")); + assertEquals( + expected.subList(3, 8), + analyzeOutputSymbols( + template, + "select " + projection + " from root.sg.* slimit 5 soffset 3 align by device")); + } + } + + private List analyzeOutputSymbols(Template template, String sql) throws Exception { + return analyze(template, sql, Collections.emptyList()).getOutputExpressions().stream() + .map(pair -> pair.left.getOutputSymbol()) + .collect(Collectors.toList()); + } + + private Analysis analyze(Template template, String sql, List devices) + throws Exception { + QueryStatement statement = + (QueryStatement) StatementGenerator.createStatement(sql, ZonedDateTime.now().getOffset()); + statement.semanticCheck(); + Analysis analysis = new Analysis(); + MPPQueryContext context = + new MPPQueryContext( + sql, + new QueryId("test_query"), + new SessionInfo(0, "test", ZoneId.systemDefault()), + new TEndPoint(), + new TEndPoint()); + assertTrue( + TemplatedAnalyze.canBuildPlanUseTemplate( + analysis, + statement, + new FakePartitionFetcherImpl(), + createSchemaTree(template), + context, + devices, + new TreeAnalysisMutationJournal())); + return analysis; + } + + private ClusterSchemaTree createSchemaTree(Template template) throws Exception { + ClusterSchemaTree schemaTree = new ClusterSchemaTree(); + schemaTree.setDatabases(Collections.singleton("root.sg")); + for (String device : Arrays.asList("root.sg.d1", "root.sg.d2")) { + schemaTree.appendTemplateDevice(new PartialPath(device), true, template.getId(), template); + } + return schemaTree; + } + + private List sortedMeasurements(Template template) { + List measurements = new ArrayList<>(template.getSchemaMap().keySet()); + Collections.sort(measurements); + return measurements; + } + + private List