This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/dev/1.3 by this push:
new 21c9f9d7f8f [To dev/1.3] Fix retry bug in tree model query which
ignores the time filter (#18201)
21c9f9d7f8f is described below
commit 21c9f9d7f8f6809e0a7b782b61e4424c8d631103
Author: Jackie Tien <[email protected]>
AuthorDate: Wed Jul 15 10:41:51 2026 +0800
[To dev/1.3] Fix retry bug in tree model query which ignores the time
filter (#18201)
---
.../queryengine/plan/analyze/AnalyzeVisitor.java | 74 +++---
.../db/queryengine/plan/analyze/Analyzer.java | 16 +-
.../plan/analyze/ConcatPathRewriter.java | 23 +-
.../queryengine/plan/analyze/PredicateUtils.java | 175 ++++++++------
.../plan/analyze/TemplatedAggregationAnalyze.java | 71 ++++--
.../queryengine/plan/analyze/TemplatedAnalyze.java | 20 +-
.../plan/analyze/TreeAnalysisMutationJournal.java | 255 +++++++++++++++++++++
.../queryengine/plan/execution/QueryExecution.java | 42 +++-
.../visitor/predicate/PredicateSimplifier.java | 83 ++++---
.../db/queryengine/plan/planner/IPlanner.java | 6 +
.../plan/planner/LogicalPlanBuilder.java | 16 +-
.../queryengine/plan/planner/TreeModelPlanner.java | 20 +-
.../plan/statement/component/GroupByComponent.java | 5 +
.../plan/statement/component/OrderByComponent.java | 30 +++
.../plan/statement/component/WhereCondition.java | 7 +
.../plan/analyze/PredicateUtilsTest.java | 172 ++++++++++++++
.../plan/analyze/TemplatedAnalyzeRetryTest.java | 127 ++++++++++
.../plan/execution/QueryExecutionRetryTest.java | 218 ++++++++++++++++++
.../plan/planner/LogicalPlanBuilderTest.java | 113 +++++++++
19 files changed, 1309 insertions(+), 164 deletions(-)
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 ab81236e9ab..1536c3cc865 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
@@ -244,14 +244,19 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
private final IPartitionFetcher partitionFetcher;
private final ISchemaFetcher schemaFetcher;
private final IModelFetcher modelFetcher;
+ private final TreeAnalysisMutationJournal mutationJournal;
private static final PerformanceOverviewMetrics PERFORMANCE_OVERVIEW_METRICS
=
PerformanceOverviewMetrics.getInstance();
- public AnalyzeVisitor(IPartitionFetcher partitionFetcher, ISchemaFetcher
schemaFetcher) {
+ public AnalyzeVisitor(
+ IPartitionFetcher partitionFetcher,
+ ISchemaFetcher schemaFetcher,
+ TreeAnalysisMutationJournal mutationJournal) {
this.partitionFetcher = partitionFetcher;
this.schemaFetcher = schemaFetcher;
this.modelFetcher = ModelFetcher.getInstance();
+ this.mutationJournal = mutationJournal;
}
@Override
@@ -289,7 +294,15 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
try {
// check for semantic errors
- queryStatement.semanticCheck();
+ boolean originalCountTimeAggregation =
queryStatement.isCountTimeAggregation();
+ try {
+ queryStatement.semanticCheck();
+ } finally {
+ if (queryStatement.isCountTimeAggregation() !=
originalCountTimeAggregation) {
+ mutationJournal.recordCountTimeAggregationChange(
+ queryStatement, originalCountTimeAggregation);
+ }
+ }
context.initResultSetColumnMemoryTracking(
queryStatement.getSeriesLimit(),
@@ -319,7 +332,13 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
if (deviceList.size() > 1
&& TemplatedAnalyze.canBuildPlanUseTemplate(
- analysis, queryStatement, partitionFetcher, schemaTree,
context, deviceList)) {
+ analysis,
+ queryStatement,
+ partitionFetcher,
+ schemaTree,
+ context,
+ deviceList,
+ mutationJournal)) {
// when device size is less than 1, there is no need to use template
optimization, i.e. no
// need to extract common variables
return analysis;
@@ -327,6 +346,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
if (canPushDownLimitOffsetInGroupByTimeForDevice(queryStatement)) {
// remove the device which won't appear in resultSet after
limit/offset
+ mutationJournal.recordLimitOffsetPushDown(queryStatement);
deviceList =
pushDownLimitOffsetInGroupByTimeForDevice(
deviceList, queryStatement, context.getZoneId());
@@ -511,7 +531,10 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
queryStatement =
(QueryStatement)
concatPathRewriter.rewrite(
- queryStatement, new
PathPatternTree(queryStatement.useWildcard()), context);
+ queryStatement,
+ new PathPatternTree(queryStatement.useWildcard()),
+ context,
+ mutationJournal);
analysis.setStatement(queryStatement);
// request schema fetch API
@@ -572,24 +595,23 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
Expression globalTimePredicate = null;
boolean hasValueFilter = false;
if (queryStatement.getWhereCondition() != null) {
- WhereCondition whereCondition = queryStatement.getWhereCondition();
- Expression predicate = whereCondition.getPredicate();
-
- Pair<Expression, Boolean> resultPair =
- PredicateUtils.extractGlobalTimePredicate(predicate, true, true);
- globalTimePredicate = resultPair.left;
+ Expression predicate = queryStatement.getWhereCondition().getPredicate();
+ PredicateUtils.GlobalTimePredicateExtractionResult extractionResult =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+ globalTimePredicate = extractionResult.getGlobalTimePredicate();
if (globalTimePredicate != null) {
globalTimePredicate =
PredicateUtils.predicateRemoveNot(globalTimePredicate);
}
- hasValueFilter = resultPair.right;
-
- predicate = PredicateUtils.simplifyPredicate(predicate);
+ hasValueFilter = extractionResult.hasValueFilter();
+ Expression residualPredicate =
+
PredicateUtils.simplifyPredicate(extractionResult.getResidualPredicate());
// set where condition to null if predicate is true or time filter.
- if (!hasValueFilter || predicate.equals(ConstantOperand.TRUE)) {
- queryStatement.setWhereCondition(null);
- } else {
- whereCondition.setPredicate(predicate);
+ if (!hasValueFilter || residualPredicate.equals(ConstantOperand.TRUE)) {
+ mutationJournal.replaceWhereCondition(queryStatement, null);
+ } else if (residualPredicate != predicate) {
+ mutationJournal.replaceWhereCondition(
+ queryStatement,
WhereCondition.fromNormalizedPredicate(residualPredicate));
}
}
analysis.setGlobalTimePredicate(globalTimePredicate);
@@ -1897,7 +1919,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
orderByExpressions.add(orderByExpression);
}
analysis.setOrderByExpressions(orderByExpressions);
- queryStatement.updateSortItems(orderByExpressions);
+ mutationJournal.updateSortItems(queryStatement, orderByExpressions);
}
private void analyzeOrderBy(
@@ -2067,7 +2089,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
}
analysis.setOrderByExpressions(deviceViewOrderByExpression);
- queryStatement.updateSortItems(deviceViewOrderByExpression);
+ mutationJournal.updateSortItems(queryStatement,
deviceViewOrderByExpression);
analysis.setDeviceToSortItems(deviceToSortItems);
analysis.setDeviceToOrderByExpressions(deviceToOrderByExpressions);
}
@@ -2387,7 +2409,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
if (!queryStatement.isSelectInto()) {
return;
}
- queryStatement.setOrderByComponent(null);
+ mutationJournal.clearOrderByComponent(queryStatement);
List<PartialPath> sourceDevices = new ArrayList<>(deviceSet);
List<Expression> sourceColumns =
@@ -2442,7 +2464,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
if (!queryStatement.isSelectInto()) {
return;
}
- queryStatement.setOrderByComponent(null);
+ mutationJournal.clearOrderByComponent(queryStatement);
List<Expression> sourceColumns =
outputExpressions.stream()
@@ -3400,17 +3422,17 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
private void analyzeGlobalTimeConditionInShowMetaData(
WhereCondition timeCondition, Analysis analysis) {
Expression predicate = timeCondition.getPredicate();
- Pair<Expression, Boolean> resultPair =
- PredicateUtils.extractGlobalTimePredicate(predicate, true, true);
- if (resultPair.right) {
+ PredicateUtils.GlobalTimePredicateExtractionResult extractionResult =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+ if (extractionResult.hasValueFilter()) {
throw new SemanticException(
"Value Filter can't exist in the condition of SHOW/COUNT clause,
only time condition supported");
}
- if (resultPair.left == null) {
+ if (extractionResult.getGlobalTimePredicate() == null) {
throw new SemanticException(
"Time condition can't be empty in the condition of SHOW/COUNT
clause");
}
- Expression globalTimePredicate = resultPair.left;
+ Expression globalTimePredicate = extractionResult.getGlobalTimePredicate();
globalTimePredicate =
PredicateUtils.predicateRemoveNot(globalTimePredicate);
analysis.setGlobalTimePredicate(globalTimePredicate);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
index 3dfaa0a1137..3f4f692bbfa 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/Analyzer.java
@@ -33,17 +33,28 @@ public class Analyzer {
private final IPartitionFetcher partitionFetcher;
private final ISchemaFetcher schemaFetcher;
+ private final TreeAnalysisMutationJournal mutationJournal;
public Analyzer(
MPPQueryContext context, IPartitionFetcher partitionFetcher,
ISchemaFetcher schemaFetcher) {
+ this(context, partitionFetcher, schemaFetcher, new
TreeAnalysisMutationJournal());
+ }
+
+ public Analyzer(
+ MPPQueryContext context,
+ IPartitionFetcher partitionFetcher,
+ ISchemaFetcher schemaFetcher,
+ TreeAnalysisMutationJournal mutationJournal) {
this.context = context;
this.partitionFetcher = partitionFetcher;
this.schemaFetcher = schemaFetcher;
+ this.mutationJournal = mutationJournal;
}
public Analysis analyze(Statement statement) {
long startTime = System.nanoTime();
- AnalyzeVisitor visitor = new AnalyzeVisitor(partitionFetcher,
schemaFetcher);
+ mutationJournal.prepareForAnalyze();
+ AnalyzeVisitor visitor = new AnalyzeVisitor(partitionFetcher,
schemaFetcher, mutationJournal);
Analysis analysis = null;
context.setReserveMemoryForSchemaTreeFunc(
mem -> {
@@ -53,6 +64,9 @@ public class Analyzer {
});
try {
analysis = visitor.process(statement, context);
+ } catch (RuntimeException | Error e) {
+ mutationJournal.rollback();
+ throw e;
} finally {
if (analysis != null && context.releaseSchemaTreeAfterAnalyzing()) {
analysis.setSchemaTree(null);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
index 041f08e1b5c..a1dd315517e 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/ConcatPathRewriter.java
@@ -50,7 +50,10 @@ public class ConcatPathRewriter {
}
public Statement rewrite(
- Statement statement, PathPatternTree patternTree, MPPQueryContext
queryContext)
+ Statement statement,
+ PathPatternTree patternTree,
+ MPPQueryContext queryContext,
+ TreeAnalysisMutationJournal mutationJournal)
throws StatementAnalyzeException {
QueryStatement queryStatement = (QueryStatement) statement;
this.patternTree = patternTree;
@@ -78,20 +81,20 @@ public class ConcatPathRewriter {
// concat SELECT with FROM
List<ResultColumn> resultColumns =
concatSelectWithFrom(queryStatement.getSelectComponent(),
prefixPaths, queryContext);
- queryStatement.getSelectComponent().setResultColumns(resultColumns);
+ mutationJournal.replaceResultColumns(queryStatement, resultColumns);
// concat GROUP BY with FROM
if (queryStatement.hasGroupByExpression()) {
- queryStatement
- .getGroupByComponent()
- .setControlColumnExpression(
- contactGroupByWithFrom(
-
queryStatement.getGroupByComponent().getControlColumnExpression(),
- prefixPaths,
- queryContext));
+ mutationJournal.replaceGroupByControlExpression(
+ queryStatement,
+ contactGroupByWithFrom(
+
queryStatement.getGroupByComponent().getControlColumnExpression(),
+ prefixPaths,
+ queryContext));
}
if (queryStatement.hasOrderByExpression()) {
- List<Expression> sortItemExpressions =
queryStatement.getExpressionSortItemList();
+ List<Expression> sortItemExpressions =
+
mutationJournal.getMutableOrderByComponent(queryStatement).getExpressionSortItemList();
sortItemExpressions.replaceAll(
expression -> contactOrderByWithFrom(expression, prefixPaths,
queryContext));
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
index 4a65ebbfe72..5eb4d4f4334 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
@@ -44,7 +44,6 @@ import
org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate.Reverse
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.read.filter.basic.Filter;
-import org.apache.tsfile.utils.Pair;
import java.time.ZoneId;
import java.util.ArrayList;
@@ -61,88 +60,85 @@ public class PredicateUtils {
}
/**
- * Extract global time predicate from query predicate.
+ * Extracts the global time predicate and the predicate that still needs to
be evaluated.
+ *
+ * <p>This method never modifies {@code predicate}. A {@code null} global
time predicate
+ * represents {@code TRUE}. The returned predicates satisfy the following
invariant:
+ *
+ * <pre>
+ * predicate == globalTimePredicate AND residualPredicate
+ * </pre>
+ *
+ * where a {@code null} global time predicate is omitted from the
conjunction.
*
* @param predicate raw query predicate
- * @param canRewrite determined by the father of current expression
- * @param isFirstOr whether it is the first LogicOrExpression encountered
- * @return global time predicate
+ * @return the extracted global time predicate, residual predicate and
value-filter flag
* @throws UnknownExpressionTypeException unknown expression type
*/
- public static Pair<Expression, Boolean> extractGlobalTimePredicate(
- Expression predicate, boolean canRewrite, boolean isFirstOr) {
+ public static GlobalTimePredicateExtractionResult extractGlobalTimePredicate(
+ Expression predicate) {
if (predicate.getExpressionType().equals(ExpressionType.LOGIC_AND)) {
- Pair<Expression, Boolean> leftResultPair =
- extractGlobalTimePredicate(
- ((BinaryExpression) predicate).getLeftExpression(), canRewrite,
isFirstOr);
- Pair<Expression, Boolean> rightResultPair =
- extractGlobalTimePredicate(
- ((BinaryExpression) predicate).getRightExpression(), canRewrite,
isFirstOr);
-
- // rewrite predicate to avoid duplicate calculation on time filter
- // If Left-child or Right-child does not contain value filter
- // We can set it to true in Predicate Tree
- if (canRewrite) {
- if (leftResultPair.left != null && !leftResultPair.right) {
- ((BinaryExpression) predicate)
- .setLeftExpression(new ConstantOperand(TSDataType.BOOLEAN,
"true"));
- }
- if (rightResultPair.left != null && !rightResultPair.right) {
- ((BinaryExpression) predicate)
- .setRightExpression(new ConstantOperand(TSDataType.BOOLEAN,
"true"));
- }
- }
+ GlobalTimePredicateExtractionResult leftResult =
+ extractGlobalTimePredicate(((BinaryExpression)
predicate).getLeftExpression());
+ GlobalTimePredicateExtractionResult rightResult =
+ extractGlobalTimePredicate(((BinaryExpression)
predicate).getRightExpression());
- if (leftResultPair.left != null && rightResultPair.left != null) {
- return new Pair<>(
- ExpressionFactory.and(leftResultPair.left, rightResultPair.left),
- leftResultPair.right || rightResultPair.right);
- } else if (leftResultPair.left != null) {
- return new Pair<>(leftResultPair.left, true);
- } else if (rightResultPair.left != null) {
- return new Pair<>(rightResultPair.left, true);
- }
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(
+ combineConjuncts(
+ leftResult.getGlobalTimePredicate(),
rightResult.getGlobalTimePredicate(), null),
+ combineConjuncts(
+ leftResult.getResidualPredicate(),
+ rightResult.getResidualPredicate(),
+ ConstantOperand.TRUE),
+ leftResult.hasValueFilter() || rightResult.hasValueFilter());
} else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_OR)) {
- Pair<Expression, Boolean> leftResultPair =
- extractGlobalTimePredicate(
- ((BinaryExpression) predicate).getLeftExpression(), false,
false);
- Pair<Expression, Boolean> rightResultPair =
- extractGlobalTimePredicate(
- ((BinaryExpression) predicate).getRightExpression(), false,
false);
-
- if (leftResultPair.left != null && rightResultPair.left != null) {
- if (Boolean.TRUE.equals(isFirstOr && !leftResultPair.right &&
!rightResultPair.right)) {
- ((BinaryExpression) predicate)
- .setLeftExpression(new ConstantOperand(TSDataType.BOOLEAN,
"true"));
- ((BinaryExpression) predicate)
- .setRightExpression(new ConstantOperand(TSDataType.BOOLEAN,
"true"));
+ GlobalTimePredicateExtractionResult leftResult =
+ extractGlobalTimePredicate(((BinaryExpression)
predicate).getLeftExpression());
+ GlobalTimePredicateExtractionResult rightResult =
+ extractGlobalTimePredicate(((BinaryExpression)
predicate).getRightExpression());
+
+ if (leftResult.getGlobalTimePredicate() != null
+ && rightResult.getGlobalTimePredicate() != null) {
+ Expression globalTimePredicate =
+ ExpressionFactory.or(
+ leftResult.getGlobalTimePredicate(),
rightResult.getGlobalTimePredicate());
+ boolean hasValueFilter = leftResult.hasValueFilter() ||
rightResult.hasValueFilter();
+ if (!hasValueFilter) {
+ return new GlobalTimePredicateExtractionResult(
+ globalTimePredicate, ConstantOperand.TRUE, false);
}
- return new Pair<>(
- ExpressionFactory.or(leftResultPair.left, rightResultPair.left),
- leftResultPair.right || rightResultPair.right);
+
+ // (G1 AND R1) OR (G2 AND R2) implies G1 OR G2, but R1 OR R2 is not an
+ // equivalent residual. Keeping the original OR predicate preserves
+ // P == (G1 OR G2) AND P without copying or mutating its subtrees.
+ return new GlobalTimePredicateExtractionResult(globalTimePredicate,
predicate, true);
}
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_NOT))
{
- Pair<Expression, Boolean> childResultPair =
- extractGlobalTimePredicate(
- ((UnaryExpression) predicate).getExpression(), canRewrite,
isFirstOr);
- if (childResultPair.left != null) {
- return new Pair<>(ExpressionFactory.not(childResultPair.left),
childResultPair.right);
+ GlobalTimePredicateExtractionResult childResult =
+ extractGlobalTimePredicate(((UnaryExpression)
predicate).getExpression());
+ if (childResult.getGlobalTimePredicate() != null &&
!childResult.hasValueFilter()) {
+ return new GlobalTimePredicateExtractionResult(
+ ExpressionFactory.not(childResult.getGlobalTimePredicate()),
+ ConstantOperand.TRUE,
+ false);
}
- return new Pair<>(null, true);
+ // NOT(G AND R) cannot in general be represented as NOT(G) AND NOT(R). Do
+ // not extract a time predicate when the negated subtree contains a value
+ // filter.
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.isCompareBinaryExpression()) {
Expression leftExpression = ((BinaryExpression)
predicate).getLeftExpression();
Expression rightExpression = ((BinaryExpression)
predicate).getRightExpression();
if (checkIsTimeFilter(leftExpression, rightExpression)
|| checkIsTimeFilter(rightExpression, leftExpression)) {
- return new Pair<>(predicate, false);
+ return new GlobalTimePredicateExtractionResult(predicate,
ConstantOperand.TRUE, false);
}
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.LIKE)
|| predicate.getExpressionType().equals(ExpressionType.REGEXP)) {
// time filter don't support LIKE and REGEXP
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.BETWEEN)) {
Expression firstExpression = ((TernaryExpression)
predicate).getFirstExpression();
Expression secondExpression = ((TernaryExpression)
predicate).getSecondExpression();
@@ -157,31 +153,68 @@ public class PredicateUtils {
isTimeFilter = checkBetweenConstantSatisfy(secondExpression,
firstExpression);
}
if (isTimeFilter) {
- return new Pair<>(predicate, false);
+ return new GlobalTimePredicateExtractionResult(predicate,
ConstantOperand.TRUE, false);
}
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.IS_NULL)) {
// time filter don't support IS_NULL
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.IN)) {
Expression timeExpression = ((InExpression) predicate).getExpression();
if (timeExpression.getExpressionType().equals(ExpressionType.TIMESTAMP))
{
- return new Pair<>(predicate, false);
+ return new GlobalTimePredicateExtractionResult(predicate,
ConstantOperand.TRUE, false);
}
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.TIMESERIES)
|| predicate.getExpressionType().equals(ExpressionType.CONSTANT)
|| predicate.getExpressionType().equals(ExpressionType.NULL)) {
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if
(predicate.getExpressionType().equals(ExpressionType.CASE_WHEN_THEN)) {
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (ExpressionType.FUNCTION.equals(predicate.getExpressionType())) {
- return new Pair<>(null, true);
+ return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else {
throw new UnknownExpressionTypeException(predicate.getExpressionType());
}
}
+ private static Expression combineConjuncts(
+ Expression leftExpression, Expression rightExpression, Expression
identity) {
+ if (leftExpression == null || leftExpression.equals(identity)) {
+ return rightExpression;
+ }
+ if (rightExpression == null || rightExpression.equals(identity)) {
+ return leftExpression;
+ }
+ return ExpressionFactory.and(leftExpression, rightExpression);
+ }
+
+ public static final class GlobalTimePredicateExtractionResult {
+
+ private final Expression globalTimePredicate;
+ private final Expression residualPredicate;
+ private final boolean hasValueFilter;
+
+ private GlobalTimePredicateExtractionResult(
+ Expression globalTimePredicate, Expression residualPredicate, boolean
hasValueFilter) {
+ this.globalTimePredicate = globalTimePredicate;
+ this.residualPredicate = residualPredicate;
+ this.hasValueFilter = hasValueFilter;
+ }
+
+ public Expression getGlobalTimePredicate() {
+ return globalTimePredicate;
+ }
+
+ public Expression getResidualPredicate() {
+ return residualPredicate;
+ }
+
+ public boolean hasValueFilter() {
+ return hasValueFilter;
+ }
+ }
+
private static boolean checkIsTimeFilter(Expression timeExpression,
Expression valueExpression) {
return timeExpression.getExpressionType().equals(ExpressionType.TIMESTAMP)
&& valueExpression instanceof ConstantOperand
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 67837a6b78c..43c810ac808 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
@@ -39,6 +39,7 @@ import org.apache.tsfile.write.schema.IMeasurementSchema;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -68,6 +69,26 @@ public class TemplatedAggregationAnalyze {
MPPQueryContext context,
Template template,
List<PartialPath> deviceList) {
+ return canBuildAggregationPlanUseTemplate(
+ analysis,
+ queryStatement,
+ partitionFetcher,
+ schemaTree,
+ context,
+ template,
+ deviceList,
+ new TreeAnalysisMutationJournal());
+ }
+
+ static boolean canBuildAggregationPlanUseTemplate(
+ Analysis analysis,
+ QueryStatement queryStatement,
+ IPartitionFetcher partitionFetcher,
+ ISchemaTree schemaTree,
+ MPPQueryContext context,
+ Template template,
+ List<PartialPath> deviceList,
+ TreeAnalysisMutationJournal mutationJournal) {
// not support order by expression and non-aligned template
if (queryStatement.hasOrderByExpression() || !template.isDirectAligned()) {
@@ -76,13 +97,6 @@ public class TemplatedAggregationAnalyze {
analysis.setNoWhereAndAggregation(false);
- if (canPushDownLimitOffsetInGroupByTimeForDevice(queryStatement)) {
- // remove the device which won't appear in resultSet after limit/offset
- deviceList =
- pushDownLimitOffsetInGroupByTimeForDevice(
- deviceList, queryStatement, context.getZoneId());
- }
-
List<Pair<Expression, String>> outputExpressions = new ArrayList<>();
boolean valid = analyzeSelect(queryStatement, analysis, outputExpressions,
template);
if (!valid) {
@@ -110,6 +124,21 @@ public class TemplatedAggregationAnalyze {
return false;
}
+ if (canPushDownLimitOffsetInGroupByTimeForDevice(queryStatement)) {
+ // Pushdown rewrites LIMIT/OFFSET and the GROUP BY time range in the
statement. Do it only
+ // after every template eligibility check succeeds, because the regular
analyzer consumes the
+ // same statement when this optimization falls back.
+ mutationJournal.recordLimitOffsetPushDown(queryStatement);
+ deviceList =
+ pushDownLimitOffsetInGroupByTimeForDevice(
+ deviceList, queryStatement, context.getZoneId());
+ if (deviceList.isEmpty()) {
+ analysis.setFinishQueryAfterAnalyze(true);
+ return true;
+ }
+ analysis.setDeviceList(deviceList);
+ }
+
analyzeDeviceToExpressions(analysis);
analyzeDeviceViewOutput(analysis, queryStatement);
@@ -149,13 +178,29 @@ public class TemplatedAggregationAnalyze {
if (selectExpression instanceof FunctionExpression
&& COUNT_TIME.equalsIgnoreCase(
((FunctionExpression) selectExpression).getFunctionName())) {
- outputExpressions.add(new Pair<>(selectExpression,
resultColumn.getAlias()));
- selectExpressions.add(selectExpression);
- aggregationExpressions.add(selectExpression);
+ 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
+ // wildcard child in place. Prime the copy's expression string before
replacing the child:
+ // the result header must remain count_time(*), while the planning
output symbol must be
+ // count_time(Time), just as it was when the parsed expression was
mutated in place.
+ FunctionExpression templatedCountTimeExpression =
+ new FunctionExpression(
+ countTimeExpression.getFunctionName(),
+ new
LinkedHashMap<>(countTimeExpression.getFunctionAttributes()),
+ new ArrayList<>(countTimeExpression.getExpressions()));
+ templatedCountTimeExpression.getExpressionString();
+ templatedCountTimeExpression.setExpressions(
+ Collections.singletonList(new TimestampOperand()));
+
templatedCountTimeExpression.setFunctionType(countTimeExpression.getFunctionType());
+
+ outputExpressions.add(new Pair<>(templatedCountTimeExpression,
resultColumn.getAlias()));
+ selectExpressions.add(templatedCountTimeExpression);
+ aggregationExpressions.add(templatedCountTimeExpression);
- analysis.getExpressionTypes().put(NodeRef.of(selectExpression),
TSDataType.INT64);
- ((FunctionExpression) selectExpression)
- .setExpressions(Collections.singletonList(new TimestampOperand()));
+ analysis
+ .getExpressionTypes()
+ .put(NodeRef.of(templatedCountTimeExpression), TSDataType.INT64);
continue;
}
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 157ddd723cd..a1a69aa2ae5 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
@@ -97,7 +97,8 @@ public class TemplatedAnalyze {
IPartitionFetcher partitionFetcher,
ISchemaTree schemaTree,
MPPQueryContext context,
- List<PartialPath> deviceList) {
+ List<PartialPath> deviceList,
+ TreeAnalysisMutationJournal mutationJournal) {
if (queryStatement.getGroupByComponent() != null
|| queryStatement.isSelectInto()
|| queryStatement.hasFill()
@@ -114,7 +115,14 @@ public class TemplatedAnalyze {
if (queryStatement.isAggregationQuery()) {
return canBuildAggregationPlanUseTemplate(
- analysis, queryStatement, partitionFetcher, schemaTree, context,
template, deviceList);
+ analysis,
+ queryStatement,
+ partitionFetcher,
+ schemaTree,
+ context,
+ template,
+ deviceList,
+ mutationJournal);
}
List<Pair<Expression, String>> outputExpressions = new ArrayList<>();
@@ -189,7 +197,8 @@ public class TemplatedAnalyze {
}
analysis.setDeviceList(deviceList);
- analyzeDeviceToOrderBy(analysis, queryStatement, schemaTree, deviceList,
context);
+ analyzeDeviceToOrderBy(
+ analysis, queryStatement, schemaTree, deviceList, context,
mutationJournal);
analyzeDeviceToSourceTransform(analysis);
analyzeDeviceToSource(analysis);
@@ -277,7 +286,8 @@ public class TemplatedAnalyze {
QueryStatement queryStatement,
ISchemaTree schemaTree,
List<PartialPath> deviceSet,
- MPPQueryContext queryContext) {
+ MPPQueryContext queryContext,
+ TreeAnalysisMutationJournal mutationJournal) {
if (!queryStatement.hasOrderByExpression()) {
return;
}
@@ -322,7 +332,7 @@ public class TemplatedAnalyze {
}
analysis.setOrderByExpressions(deviceViewOrderByExpression);
- queryStatement.updateSortItems(deviceViewOrderByExpression);
+ mutationJournal.updateSortItems(queryStatement,
deviceViewOrderByExpression);
analysis.setDeviceToSortItems(deviceToSortItems);
analysis.setDeviceToOrderByExpressions(deviceToOrderByExpressions);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TreeAnalysisMutationJournal.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TreeAnalysisMutationJournal.java
new file mode 100644
index 00000000000..767afd2d215
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TreeAnalysisMutationJournal.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.db.queryengine.plan.expression.Expression;
+import
org.apache.iotdb.db.queryengine.plan.statement.component.GroupByTimeComponent;
+import
org.apache.iotdb.db.queryengine.plan.statement.component.OrderByComponent;
+import org.apache.iotdb.db.queryengine.plan.statement.component.ResultColumn;
+import org.apache.iotdb.db.queryengine.plan.statement.component.WhereCondition;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Records the parser-owned state changed by one tree-query analysis attempt.
+ *
+ * <p>A retryable dispatch failure causes {@code QueryExecution} to analyze
the same statement
+ * object again. The journal records only the references and primitive values
that an analysis
+ * attempt replaces. Objects reachable from a recorded reference must
therefore remain untouched;
+ * callers that need to change a nested mutable object must first obtain a
copy-on-write working
+ * object from this class.
+ *
+ * <p>The journal belongs to the planner for one query execution. It is not a
general AST snapshot
+ * and must not be used by cached relational prepared-statement ASTs.
+ */
+public final class TreeAnalysisMutationJournal {
+
+ private static final int SELECT_RESULT_COLUMNS = 1;
+ private static final int WHERE_CONDITION = 1 << 1;
+ private static final int GROUP_BY_CONTROL_EXPRESSION = 1 << 2;
+ private static final int ORDER_BY_COMPONENT = 1 << 3;
+ private static final int LIMIT_OFFSET = 1 << 4;
+ private static final int COUNT_TIME_AGGREGATION = 1 << 5;
+
+ private boolean active;
+ private int recordedFields;
+ private QueryStatement statement;
+
+ private List<ResultColumn> originalResultColumns;
+ private WhereCondition originalWhereCondition;
+ private Expression originalGroupByControlExpression;
+ private OrderByComponent originalOrderByComponent;
+ private boolean orderByDetached;
+
+ private long originalRowLimit;
+ private long originalRowOffset;
+ private boolean originalResultSetEmpty;
+ private GroupByTimeComponent originalGroupByTimeComponent;
+ private long originalGroupByStartTime;
+ private long originalGroupByEndTime;
+ private boolean originalCountTimeAggregation;
+
+ /** Starts a new attempt, rolling back an unfinished preceding attempt if
necessary. */
+ public void begin() {
+ if (active) {
+ rollback();
+ }
+ active = true;
+ }
+
+ /**
+ * Makes repeated direct Analyzer invocations behave like QueryExecution
retry attempts.
+ *
+ * <p>Production code starts and finishes attempts through the planner
lifecycle. Unit tests and a
+ * few internal callers invoke Analyzer directly, so Analyzer calls this
method before walking the
+ * statement.
+ */
+ public void prepareForAnalyze() {
+ if (!active) {
+ active = true;
+ } else if (recordedFields != 0) {
+ rollback();
+ active = true;
+ }
+ }
+
+ /** Restores the parser-owned state and releases all references held by this
attempt. */
+ public void rollback() {
+ if (!active) {
+ return;
+ }
+ if (statement != null) {
+ if (isRecorded(SELECT_RESULT_COLUMNS)) {
+ statement.getSelectComponent().setResultColumns(originalResultColumns);
+ }
+ if (isRecorded(WHERE_CONDITION)) {
+ statement.setWhereCondition(originalWhereCondition);
+ }
+ if (isRecorded(GROUP_BY_CONTROL_EXPRESSION)) {
+ statement
+ .getGroupByComponent()
+
.setControlColumnExpressionForAnalyze(originalGroupByControlExpression);
+ }
+ if (isRecorded(ORDER_BY_COMPONENT)) {
+ statement.setOrderByComponent(originalOrderByComponent);
+ }
+ if (isRecorded(LIMIT_OFFSET)) {
+ statement.setRowLimit(originalRowLimit);
+ statement.setRowOffset(originalRowOffset);
+ statement.setResultSetEmpty(originalResultSetEmpty);
+ originalGroupByTimeComponent.setStartTime(originalGroupByStartTime);
+ originalGroupByTimeComponent.setEndTime(originalGroupByEndTime);
+ }
+ if (isRecorded(COUNT_TIME_AGGREGATION)) {
+ statement.setCountTimeAggregation(originalCountTimeAggregation);
+ }
+ }
+ clear();
+ }
+
+ /**
+ * Commits an attempt by discarding its undo information.
+ *
+ * <p>The working statement is still referenced by Analysis and the
generated plans, so a
+ * successful attempt is not restored after the dispatch retry window has
closed.
+ */
+ public void commit() {
+ clear();
+ }
+
+ public void replaceResultColumns(
+ QueryStatement queryStatement, List<ResultColumn> resultColumns) {
+ ensureActive(queryStatement);
+ if (!isRecorded(SELECT_RESULT_COLUMNS)) {
+ originalResultColumns =
queryStatement.getSelectComponent().getResultColumns();
+ recordedFields |= SELECT_RESULT_COLUMNS;
+ }
+ queryStatement.getSelectComponent().setResultColumns(resultColumns);
+ }
+
+ public void replaceWhereCondition(QueryStatement queryStatement,
WhereCondition whereCondition) {
+ ensureActive(queryStatement);
+ if (!isRecorded(WHERE_CONDITION)) {
+ originalWhereCondition = queryStatement.getWhereCondition();
+ recordedFields |= WHERE_CONDITION;
+ }
+ queryStatement.setWhereCondition(whereCondition);
+ }
+
+ public void replaceGroupByControlExpression(
+ QueryStatement queryStatement, Expression controlExpression) {
+ ensureActive(queryStatement);
+ if (!isRecorded(GROUP_BY_CONTROL_EXPRESSION)) {
+ originalGroupByControlExpression =
+ queryStatement.getGroupByComponent().getControlColumnExpression();
+ recordedFields |= GROUP_BY_CONTROL_EXPRESSION;
+ }
+
queryStatement.getGroupByComponent().setControlColumnExpressionForAnalyze(controlExpression);
+ }
+
+ /**
+ * Returns an attempt-local ORDER BY component whose nested lists and
SortItems may be changed.
+ */
+ public OrderByComponent getMutableOrderByComponent(QueryStatement
queryStatement) {
+ ensureActive(queryStatement);
+ if (queryStatement.getOrderByComponent() == null) {
+ return null;
+ }
+ recordOrderByComponent(queryStatement);
+ if (!orderByDetached) {
+ queryStatement.setOrderByComponent(
+ OrderByComponent.copyOf(queryStatement.getOrderByComponent()));
+ orderByDetached = true;
+ }
+ return queryStatement.getOrderByComponent();
+ }
+
+ public void clearOrderByComponent(QueryStatement queryStatement) {
+ ensureActive(queryStatement);
+ recordOrderByComponent(queryStatement);
+ queryStatement.setOrderByComponent(null);
+ }
+
+ public void updateSortItems(QueryStatement queryStatement, Set<Expression>
orderByExpressions) {
+ if (getMutableOrderByComponent(queryStatement) != null) {
+ queryStatement.updateSortItems(orderByExpressions);
+ }
+ }
+
+ /** Records all values changed together by LIMIT/OFFSET pushdown. */
+ public void recordLimitOffsetPushDown(QueryStatement queryStatement) {
+ ensureActive(queryStatement);
+ if (isRecorded(LIMIT_OFFSET)) {
+ return;
+ }
+ originalRowLimit = queryStatement.getRowLimit();
+ originalRowOffset = queryStatement.getRowOffset();
+ originalResultSetEmpty = queryStatement.isResultSetEmpty();
+ originalGroupByTimeComponent = queryStatement.getGroupByTimeComponent();
+ originalGroupByStartTime = originalGroupByTimeComponent.getStartTime();
+ originalGroupByEndTime = originalGroupByTimeComponent.getEndTime();
+ recordedFields |= LIMIT_OFFSET;
+ }
+
+ /** Records the derived flag that semantic validation sets for {@code
count_time(*)}. */
+ public void recordCountTimeAggregationChange(
+ QueryStatement queryStatement, boolean originalValue) {
+ ensureActive(queryStatement);
+ if (!isRecorded(COUNT_TIME_AGGREGATION)) {
+ originalCountTimeAggregation = originalValue;
+ recordedFields |= COUNT_TIME_AGGREGATION;
+ }
+ }
+
+ private void recordOrderByComponent(QueryStatement queryStatement) {
+ if (!isRecorded(ORDER_BY_COMPONENT)) {
+ originalOrderByComponent = queryStatement.getOrderByComponent();
+ recordedFields |= ORDER_BY_COMPONENT;
+ }
+ }
+
+ private boolean isRecorded(int field) {
+ return (recordedFields & field) != 0;
+ }
+
+ private void ensureActive(QueryStatement queryStatement) {
+ if (!active) {
+ active = true;
+ }
+ if (statement == null) {
+ statement = queryStatement;
+ } else if (statement != queryStatement) {
+ throw new IllegalStateException();
+ }
+ }
+
+ private void clear() {
+ active = false;
+ recordedFields = 0;
+ statement = null;
+ originalResultColumns = null;
+ originalWhereCondition = null;
+ originalGroupByControlExpression = null;
+ originalOrderByComponent = null;
+ orderByDetached = false;
+ originalGroupByTimeComponent = null;
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
index 5af143bfca8..75598e46791 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
@@ -124,7 +124,13 @@ public class QueryExecution implements IQueryExecution {
public QueryExecution(IPlanner planner, MPPQueryContext context,
ExecutorService executor) {
this.context = context;
this.planner = planner;
- this.analysis = analyze(context);
+ planner.beginAnalysisAttempt();
+ try {
+ this.analysis = analyze(context);
+ } catch (RuntimeException | Error e) {
+ planner.rollbackAnalysisAttempt();
+ throw e;
+ }
context.setNeedSetHighestPriority(analysis.needSetHighestPriority());
this.stateMachine = new QueryStateMachine(context.getQueryId(), executor);
@@ -159,6 +165,15 @@ public class QueryExecution implements IQueryExecution {
}
public void start() {
+ try {
+ startInternal();
+ } catch (RuntimeException | Error e) {
+ finishAnalysisAttemptForCurrentState();
+ throw e;
+ }
+ }
+
+ private void startInternal() {
final long startTime = System.nanoTime();
if (skipExecute()) {
LOGGER.debug("[SkipExecute]");
@@ -168,6 +183,7 @@ public class QueryExecution implements IQueryExecution {
constructResultForMemorySource();
stateMachine.transitionToRunning();
}
+ finishAnalysisAttemptForCurrentState();
return;
}
@@ -200,6 +216,7 @@ public class QueryExecution implements IQueryExecution {
if (context.getQueryType() == QueryType.WRITE && analysis.isFailed()) {
stateMachine.transitionToFailed(analysis.getFailStatus());
}
+ finishAnalysisAttemptForCurrentState();
}
private void checkTimeOutForQuery() {
@@ -217,12 +234,15 @@ public class QueryExecution implements IQueryExecution {
private ExecutionResult retry() {
if (retryCount >= MAX_RETRY_COUNT) {
LOGGER.warn("[ReachMaxRetryCount]");
+ this.stopAndCleanup(stateMachine.getFailureException());
+ planner.rollbackAnalysisAttempt();
stateMachine.transitionToFailed();
return getStatus();
}
LOGGER.warn("error when executing query. {}",
stateMachine.getFailureMessage());
// stop and clean up resources the QueryExecution used
this.stopAndCleanup(stateMachine.getFailureException());
+ planner.rollbackAnalysisAttempt();
LOGGER.info("[WaitBeforeRetry] wait {}ms.", RETRY_INTERVAL_IN_MS);
try {
Thread.sleep(RETRY_INTERVAL_IN_MS);
@@ -241,12 +261,30 @@ public class QueryExecution implements IQueryExecution {
this.stopped.compareAndSet(true, false);
this.resultHandleCleanUp.compareAndSet(true, false);
// re-analyze the query
- this.analysis = analyze(context);
+ planner.beginAnalysisAttempt();
+ try {
+ this.analysis = analyze(context);
+ } catch (RuntimeException | Error e) {
+ planner.rollbackAnalysisAttempt();
+ throw e;
+ }
// re-start the QueryExecution
this.start();
return getStatus();
}
+ private void finishAnalysisAttemptForCurrentState() {
+ QueryState state = stateMachine.getState();
+ if (state == QueryState.PENDING_RETRY) {
+ return;
+ }
+ if (state == QueryState.RUNNING || state == QueryState.FINISHED) {
+ planner.commitAnalysisAttempt();
+ } else {
+ planner.rollbackAnalysisAttempt();
+ }
+ }
+
private boolean skipExecute() {
return analysis.canSkipExecute(context);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
index be81a33587c..b2518f7a7e6 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/PredicateSimplifier.java
@@ -22,6 +22,7 @@ package
org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate;
import org.apache.iotdb.db.queryengine.plan.expression.Expression;
import org.apache.iotdb.db.queryengine.plan.expression.ExpressionType;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.ArithmeticBinaryExpression;
+import org.apache.iotdb.db.queryengine.plan.expression.binary.BinaryExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.CompareBinaryExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicAndExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicOrExpression;
@@ -45,6 +46,10 @@ import
org.apache.iotdb.db.queryengine.plan.expression.visitor.ExpressionAnalyze
import java.util.ArrayList;
import java.util.List;
+import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructBinaryExpression;
+import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructFunctionExpression;
+import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructTernaryExpression;
+import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructUnaryExpression;
import static
org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate.ConvertPredicateToFilterVisitor.getValue;
public class PredicateSimplifier extends ExpressionAnalyzeVisitor<Expression,
Void> {
@@ -59,8 +64,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
return ConstantOperand.TRUE;
}
}
- isNullExpression.setExpression(simplifiedInputExpression);
- return isNullExpression;
+ return reconstructUnaryIfNeeded(isNullExpression,
simplifiedInputExpression);
}
@Override
@@ -83,8 +87,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
if
(simplifiedInputExpression.getExpressionType().equals(ExpressionType.NULL)) {
return ConstantOperand.FALSE;
}
- unaryExpression.setExpression(simplifiedInputExpression);
- return unaryExpression;
+ return reconstructUnaryIfNeeded(unaryExpression,
simplifiedInputExpression);
}
@Override
@@ -96,8 +99,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
if (simplifiedInputExpression.equals(ConstantOperand.FALSE)) {
return ConstantOperand.TRUE;
}
- logicNotExpression.setExpression(simplifiedInputExpression);
- return logicNotExpression;
+ return reconstructUnaryIfNeeded(logicNotExpression,
simplifiedInputExpression);
}
@Override
@@ -106,8 +108,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
if
(simplifiedInputExpression.getExpressionType().equals(ExpressionType.NULL)) {
return new NullOperand();
}
- negationExpression.setExpression(simplifiedInputExpression);
- return negationExpression;
+ return reconstructUnaryIfNeeded(negationExpression,
simplifiedInputExpression);
}
@Override
@@ -121,9 +122,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
// null calculation will return null
return new NullOperand();
}
- arithmeticBinaryExpression.setLeftExpression(simplifiedLeft);
- arithmeticBinaryExpression.setRightExpression(simplifiedRight);
- return arithmeticBinaryExpression;
+ return reconstructBinaryIfNeeded(arithmeticBinaryExpression,
simplifiedLeft, simplifiedRight);
}
@Override
@@ -144,9 +143,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
if (isLeftFalse || isRightFalse) {
return ConstantOperand.FALSE;
}
- logicAndExpression.setLeftExpression(simplifiedLeft);
- logicAndExpression.setRightExpression(simplifiedRight);
- return logicAndExpression;
+ return reconstructBinaryIfNeeded(logicAndExpression, simplifiedLeft,
simplifiedRight);
}
@Override
@@ -167,9 +164,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
} else if (isRightFalse) {
return simplifiedLeft;
}
- logicOrExpression.setLeftExpression(simplifiedLeft);
- logicOrExpression.setRightExpression(simplifiedRight);
- return logicOrExpression;
+ return reconstructBinaryIfNeeded(logicOrExpression, simplifiedLeft,
simplifiedRight);
}
@Override
@@ -183,9 +178,7 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
// null comparison will return false
return ConstantOperand.FALSE;
}
- compareBinaryExpression.setLeftExpression(simplifiedLeft);
- compareBinaryExpression.setRightExpression(simplifiedRight);
- return compareBinaryExpression;
+ return reconstructBinaryIfNeeded(compareBinaryExpression, simplifiedLeft,
simplifiedRight);
}
@Override
@@ -228,10 +221,16 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
return ConstantOperand.FALSE;
}
}
- betweenExpression.setFirstExpression(simplifiedFirstExpression);
- betweenExpression.setSecondExpression(simplifiedSecondExpression);
- betweenExpression.setThirdExpression(simplifiedThirdExpression);
- return betweenExpression;
+ if (simplifiedFirstExpression == betweenExpression.getFirstExpression()
+ && simplifiedSecondExpression ==
betweenExpression.getSecondExpression()
+ && simplifiedThirdExpression ==
betweenExpression.getThirdExpression()) {
+ return betweenExpression;
+ }
+ return reconstructTernaryExpression(
+ betweenExpression,
+ simplifiedFirstExpression,
+ simplifiedSecondExpression,
+ simplifiedThirdExpression);
}
private <T extends Comparable<T>> boolean lessThan(
@@ -243,16 +242,42 @@ public class PredicateSimplifier extends
ExpressionAnalyzeVisitor<Expression, Vo
@Override
public Expression visitFunctionExpression(FunctionExpression
functionExpression, Void context) {
- List<Expression> simplifiedInputExpressions = new ArrayList<>();
- for (Expression inputExpression : functionExpression.getExpressions()) {
+ List<Expression> inputExpressions = functionExpression.getExpressions();
+ List<Expression> simplifiedInputExpressions = null;
+ for (int i = 0; i < inputExpressions.size(); i++) {
+ Expression inputExpression = inputExpressions.get(i);
Expression simplifiedInputExpression = process(inputExpression, context);
if
(simplifiedInputExpression.getExpressionType().equals(ExpressionType.NULL)) {
return new NullOperand();
}
- simplifiedInputExpressions.add(simplifiedInputExpression);
+ if (simplifiedInputExpression != inputExpression) {
+ if (simplifiedInputExpressions == null) {
+ simplifiedInputExpressions = new ArrayList<>(inputExpressions);
+ }
+ simplifiedInputExpressions.set(i, simplifiedInputExpression);
+ }
+ }
+ if (simplifiedInputExpressions == null) {
+ return functionExpression;
+ }
+ return reconstructFunctionExpression(functionExpression,
simplifiedInputExpressions);
+ }
+
+ private Expression reconstructUnaryIfNeeded(
+ UnaryExpression originalExpression, Expression simplifiedChild) {
+ if (simplifiedChild == originalExpression.getExpression()) {
+ return originalExpression;
+ }
+ return reconstructUnaryExpression(originalExpression, simplifiedChild);
+ }
+
+ private Expression reconstructBinaryIfNeeded(
+ BinaryExpression originalExpression, Expression simplifiedLeft,
Expression simplifiedRight) {
+ if (simplifiedLeft == originalExpression.getLeftExpression()
+ && simplifiedRight == originalExpression.getRightExpression()) {
+ return originalExpression;
}
- functionExpression.setExpressions(simplifiedInputExpressions);
- return functionExpression;
+ return reconstructBinaryExpression(originalExpression, simplifiedLeft,
simplifiedRight);
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
index b9b239b5e26..9273203cc4d 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/IPlanner.java
@@ -34,6 +34,12 @@ public interface IPlanner {
IAnalysis analyze(MPPQueryContext context);
+ default void beginAnalysisAttempt() {}
+
+ default void rollbackAnalysisAttempt() {}
+
+ default void commitAnalysisAttempt() {}
+
LogicalQueryPlan doLogicalPlan(IAnalysis analysis, MPPQueryContext context);
DistributedQueryPlan doDistributionPlan(IAnalysis analysis, LogicalQueryPlan
logicalPlan);
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 32d7b312257..d348f2ba3d1 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
@@ -486,11 +486,11 @@ public class LogicalPlanBuilder {
.map(Expression::getExpressionString)
.collect(Collectors.toList());
- List<SortItem> sortItemList = queryStatement.getSortItemList();
-
- if (sortItemList.isEmpty()) {
- sortItemList = new ArrayList<>();
- }
+ // DEVICE and TIME are implicit merge keys for DeviceView. Append them to
a planning-owned copy
+ // so rebuilding the plan after a dispatch failure does not mutate the
parsed statement or
+ // accumulate duplicate keys. Keep the complete parameter in Analysis
because a downstream
+ // SortNode must use the same implicit keys without reading a mutated
QueryStatement.
+ List<SortItem> sortItemList = new
ArrayList<>(queryStatement.getSortItemList());
if (!queryStatement.isOrderByDevice()) {
sortItemList.add(new SortItem(OrderByKey.DEVICE, Ordering.ASC));
}
@@ -499,6 +499,7 @@ public class LogicalPlanBuilder {
}
OrderByParameter orderByParameter = new OrderByParameter(sortItemList);
+ analysis.setMergeOrderParameter(orderByParameter);
long limitValue =
queryStatement.hasOffset()
@@ -1345,7 +1346,10 @@ public class LogicalPlanBuilder {
Set<Expression> orderByExpressions = analysis.getOrderByExpressions();
updateTypeProvider(orderByExpressions);
- OrderByParameter orderByParameter = new
OrderByParameter(queryStatement.getSortItemList());
+ OrderByParameter orderByParameter =
+ queryStatement.isAlignByDevice()
+ ? analysis.getMergeOrderParameter()
+ : new OrderByParameter(queryStatement.getSortItemList());
if (orderByParameter.isEmpty()) {
return this;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
index 2812d81cf27..62aa7385707 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TreeModelPlanner.java
@@ -30,6 +30,7 @@ import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
import org.apache.iotdb.db.queryengine.plan.analyze.Analyzer;
import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
import org.apache.iotdb.db.queryengine.plan.analyze.IPartitionFetcher;
+import
org.apache.iotdb.db.queryengine.plan.analyze.TreeAnalysisMutationJournal;
import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaFetcher;
import
org.apache.iotdb.db.queryengine.plan.planner.distribution.DistributionPlanner;
import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
@@ -53,6 +54,7 @@ import java.util.concurrent.ScheduledExecutorService;
public class TreeModelPlanner implements IPlanner {
private final Statement statement;
+ private final TreeAnalysisMutationJournal mutationJournal = new
TreeAnalysisMutationJournal();
private final ScheduledExecutorService scheduledExecutor;
@@ -84,7 +86,23 @@ public class TreeModelPlanner implements IPlanner {
@Override
public IAnalysis analyze(MPPQueryContext context) {
- return new Analyzer(context, partitionFetcher,
schemaFetcher).analyze(statement);
+ return new Analyzer(context, partitionFetcher, schemaFetcher,
mutationJournal)
+ .analyze(statement);
+ }
+
+ @Override
+ public void beginAnalysisAttempt() {
+ mutationJournal.begin();
+ }
+
+ @Override
+ public void rollbackAnalysisAttempt() {
+ mutationJournal.rollback();
+ }
+
+ @Override
+ public void commitAnalysisAttempt() {
+ mutationJournal.commit();
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
index ec45a90a4b8..b1d5fefde52 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/GroupByComponent.java
@@ -45,6 +45,11 @@ public abstract class GroupByComponent extends StatementNode
{
ExpressionAnalyzer.toLowerCaseExpression(controlColumnExpression);
}
+ /** Sets an expression that has already been normalized by the analyzer. */
+ public void setControlColumnExpressionForAnalyze(Expression
controlColumnExpression) {
+ this.controlColumnExpression = controlColumnExpression;
+ }
+
public Expression getControlColumnExpression() {
return controlColumnExpression;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
index 8774db378f5..777410c8b0c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/OrderByComponent.java
@@ -48,6 +48,36 @@ public class OrderByComponent extends StatementNode {
this.sortItemExpressionList = new ArrayList<>();
}
+ /**
+ * Creates a structurally independent component for analysis-time
copy-on-write updates.
+ *
+ * <p>Expression nodes are shared because analysis replaces them instead of
mutating them. Lists
+ * and SortItems are copied because both are changed in place while
resolving ORDER BY
+ * expressions.
+ */
+ public static OrderByComponent copyOf(OrderByComponent source) {
+ OrderByComponent result = new OrderByComponent();
+ int expressionIndex = 0;
+ for (SortItem sortItem : source.sortItemList) {
+ if (sortItem.isExpression()) {
+ SortItem copiedSortItem =
+ new SortItem(
+ sortItem.getExpression(), sortItem.getOrdering(),
sortItem.getNullOrdering());
+ result.sortItemList.add(copiedSortItem);
+ // SortItem intentionally keeps the parser-facing expression for SQL
rendering, while the
+ // parallel expression list contains the lower-case normalized AST
consumed by analysis.
+ // They are not interchangeable, so preserve both references
independently in the COW
+ // component.
+
result.sortItemExpressionList.add(source.sortItemExpressionList.get(expressionIndex++));
+ } else {
+ result.addSortItem(
+ new SortItem(
+ sortItem.getSortKey(), sortItem.getOrdering(),
sortItem.getNullOrdering()));
+ }
+ }
+ return result;
+ }
+
public void addSortItem(SortItem sortItem) {
this.sortItemList.add(sortItem);
switch (sortItem.getSortKey()) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
index 7352c1e96fc..d80677fd830 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/component/WhereCondition.java
@@ -35,6 +35,13 @@ public class WhereCondition extends StatementNode {
this.predicate = ExpressionAnalyzer.toLowerCaseExpression(predicate);
}
+ /** Wraps an expression that has already been normalized without rebuilding
its AST. */
+ public static WhereCondition fromNormalizedPredicate(Expression predicate) {
+ WhereCondition whereCondition = new WhereCondition();
+ whereCondition.predicate = predicate;
+ return whereCondition;
+ }
+
public Expression getPredicate() {
return predicate;
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
new file mode 100644
index 00000000000..bc9d252d30e
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.commons.exception.IllegalPathException;
+import
org.apache.iotdb.db.queryengine.plan.analyze.PredicateUtils.GlobalTimePredicateExtractionResult;
+import org.apache.iotdb.db.queryengine.plan.expression.Expression;
+import org.apache.iotdb.db.queryengine.plan.expression.binary.BinaryExpression;
+import org.apache.iotdb.db.queryengine.plan.expression.leaf.ConstantOperand;
+import
org.apache.iotdb.db.queryengine.plan.expression.multi.FunctionExpression;
+import
org.apache.iotdb.db.queryengine.plan.expression.unary.LogicNotExpression;
+
+import org.junit.Test;
+
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.and;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.function;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.gt;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.intValue;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.longValue;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.lt;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.not;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.or;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.time;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.timeSeries;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class PredicateUtilsTest {
+
+ @Test
+ public void testExtractTimePredicateFromAndWithoutMutatingInput() throws
IllegalPathException {
+ Expression timePredicate = gt(time(), longValue(1));
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ BinaryExpression predicate = and(timePredicate, valuePredicate);
+ String originalExpressionString = predicate.getExpressionString();
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertSame(timePredicate, result.getGlobalTimePredicate());
+ assertSame(valuePredicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ assertSame(timePredicate, predicate.getLeftExpression());
+ assertSame(valuePredicate, predicate.getRightExpression());
+ assertEquals(originalExpressionString, predicate.getExpressionString());
+ }
+
+ @Test
+ public void testExtractPureTimeOrWithoutMutatingInput() {
+ Expression leftTimePredicate = gt(time(), longValue(1));
+ Expression rightTimePredicate = lt(time(), longValue(10));
+ BinaryExpression predicate = or(leftTimePredicate, rightTimePredicate);
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertEquals(predicate, result.getGlobalTimePredicate());
+ assertSame(ConstantOperand.TRUE, result.getResidualPredicate());
+ assertFalse(result.hasValueFilter());
+ assertSame(leftTimePredicate, predicate.getLeftExpression());
+ assertSame(rightTimePredicate, predicate.getRightExpression());
+ }
+
+ @Test
+ public void testMixedOrKeepsOriginalPredicateAsResidual() throws
IllegalPathException {
+ Expression leftTimePredicate = gt(time(), longValue(1));
+ Expression rightTimePredicate = gt(time(), longValue(10));
+ Expression leftValuePredicate = gt(timeSeries("root.sg.d.s1"),
intValue("1"));
+ Expression rightValuePredicate = gt(timeSeries("root.sg.d.s2"),
intValue("2"));
+ BinaryExpression left = and(leftTimePredicate, leftValuePredicate);
+ BinaryExpression right = and(rightTimePredicate, rightValuePredicate);
+ BinaryExpression predicate = or(left, right);
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertEquals(or(leftTimePredicate, rightTimePredicate),
result.getGlobalTimePredicate());
+ assertSame(predicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ assertSame(left, predicate.getLeftExpression());
+ assertSame(right, predicate.getRightExpression());
+ }
+
+ @Test
+ public void testMixedNotDoesNotExtractTimePredicate() throws
IllegalPathException {
+ Expression timePredicate = gt(time(), longValue(1));
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ BinaryExpression child = and(timePredicate, valuePredicate);
+ LogicNotExpression predicate = not(child);
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertNull(result.getGlobalTimePredicate());
+ assertSame(predicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ assertSame(child, predicate.getExpression());
+ }
+
+ @Test
+ public void testPureTimeNotCanBeExtracted() {
+ Expression timePredicate = gt(time(), longValue(1));
+ LogicNotExpression predicate = not(timePredicate);
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertEquals(predicate, result.getGlobalTimePredicate());
+ assertSame(ConstantOperand.TRUE, result.getResidualPredicate());
+ assertFalse(result.hasValueFilter());
+ assertSame(timePredicate, predicate.getExpression());
+ }
+
+ @Test
+ public void testSimplifierUsesCopyOnWriteForUnaryExpression() throws
IllegalPathException {
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ BinaryExpression reducibleChild = and(ConstantOperand.TRUE,
valuePredicate);
+ LogicNotExpression predicate = not(reducibleChild);
+
+ Expression simplified = PredicateUtils.simplifyPredicate(predicate);
+
+ assertNotSame(predicate, simplified);
+ assertTrue(simplified instanceof LogicNotExpression);
+ assertSame(valuePredicate, ((LogicNotExpression)
simplified).getExpression());
+ assertSame(reducibleChild, predicate.getExpression());
+ assertSame(ConstantOperand.TRUE, reducibleChild.getLeftExpression());
+ assertSame(valuePredicate, reducibleChild.getRightExpression());
+ }
+
+ @Test
+ public void testSimplifierUsesCopyOnWriteForFunctionExpression() throws
IllegalPathException {
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ BinaryExpression reducibleChild = and(ConstantOperand.TRUE,
valuePredicate);
+ FunctionExpression functionExpression = function("test", reducibleChild);
+
+ Expression simplified =
PredicateUtils.simplifyPredicate(functionExpression);
+
+ assertNotSame(functionExpression, simplified);
+ assertTrue(simplified instanceof FunctionExpression);
+ assertSame(valuePredicate, ((FunctionExpression)
simplified).getExpressions().get(0));
+ assertSame(reducibleChild, functionExpression.getExpressions().get(0));
+ assertSame(ConstantOperand.TRUE, reducibleChild.getLeftExpression());
+ }
+
+ @Test
+ public void testSimplifierReusesUnchangedExpression() throws
IllegalPathException {
+ Expression predicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+
+ assertSame(predicate, PredicateUtils.simplifyPredicate(predicate));
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyzeRetryTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyzeRetryTest.java
new file mode 100644
index 00000000000..773da476242
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyzeRetryTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.common.SessionInfo;
+import
org.apache.iotdb.db.queryengine.plan.expression.multi.FunctionExpression;
+import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
+import org.apache.iotdb.db.schemaengine.template.Template;
+
+import org.junit.Test;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+public class TemplatedAnalyzeRetryTest {
+
+ @Test
+ public void testCountTimeTemplateAnalysisDoesNotMutateStatementForRetry()
throws Exception {
+ QueryStatement queryStatement =
+ (QueryStatement)
+ StatementGenerator.createStatement(
+ "select count_time(*) from root.sg.* align by device",
+ ZonedDateTime.now().getOffset());
+ queryStatement.semanticCheck();
+
+ Analysis analysis = new Analysis();
+ boolean canBuildPlan =
+ TemplatedAggregationAnalyze.canBuildAggregationPlanUseTemplate(
+ analysis,
+ queryStatement,
+ null,
+ null,
+ createQueryContext(),
+ createAlignedTemplate(),
+ Collections.emptyList());
+
+ assertTrue(canBuildPlan);
+ FunctionExpression countTimeExpression =
+ (FunctionExpression)
+
queryStatement.getSelectComponent().getResultColumns().get(0).getExpression();
+ assertEquals("*",
countTimeExpression.getExpressions().get(0).getOutputSymbol());
+ FunctionExpression analyzedCountTimeExpression =
+ (FunctionExpression)
analysis.getAggregationExpressions().iterator().next();
+ assertNotSame(countTimeExpression, analyzedCountTimeExpression);
+ assertEquals("count_time(*)",
analyzedCountTimeExpression.getExpressionString());
+ assertEquals("count_time(Time)",
analyzedCountTimeExpression.getOutputSymbol());
+
+ // QueryExecution analyzes the same statement again when retrying a
dispatch failure.
+ queryStatement.semanticCheck();
+ }
+
+ @Test
+ public void testFailedTemplateAttemptDoesNotConsumeLimitOffset() throws
Exception {
+ QueryStatement queryStatement =
+ (QueryStatement)
+ StatementGenerator.createStatement(
+ "select __endTime, count(s1) from root.sg.* "
+ + "group by ([0, 100), 10ms) limit 5 offset 12 align by
device",
+ ZonedDateTime.now().getOffset());
+
+ boolean canBuildPlan =
+ TemplatedAggregationAnalyze.canBuildAggregationPlanUseTemplate(
+ new Analysis(),
+ queryStatement,
+ null,
+ null,
+ createQueryContext(),
+ createAlignedTemplate(),
+ Arrays.asList(new PartialPath("root.sg.d1"), new
PartialPath("root.sg.d2")));
+
+ assertFalse(canBuildPlan);
+ assertEquals(
+ Arrays.asList(5L, 12L, 0L, 100L),
+ Arrays.asList(
+ queryStatement.getRowLimit(),
+ queryStatement.getRowOffset(),
+ queryStatement.getGroupByTimeComponent().getStartTime(),
+ queryStatement.getGroupByTimeComponent().getEndTime()));
+ }
+
+ private static MPPQueryContext createQueryContext() {
+ return new MPPQueryContext(
+ "",
+ new QueryId("test_query"),
+ new SessionInfo(0, "test", ZoneId.systemDefault(), ""),
+ new TEndPoint(),
+ new TEndPoint());
+ }
+
+ private static Template createAlignedTemplate() throws Exception {
+ return new Template(
+ "template",
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ true);
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecutionRetryTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecutionRetryTest.java
new file mode 100644
index 00000000000..139d9ee74f6
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecutionRetryTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.execution;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.common.SessionInfo;
+import org.apache.iotdb.db.queryengine.execution.QueryStateMachine;
+import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
+import org.apache.iotdb.db.queryengine.plan.analyze.QueryType;
+import org.apache.iotdb.db.queryengine.plan.planner.IPlanner;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
+import org.apache.iotdb.db.queryengine.plan.scheduler.IScheduler;
+import org.apache.iotdb.rpc.RpcUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class QueryExecutionRetryTest {
+
+ private ExecutorService stateMachineExecutor;
+
+ @Before
+ public void setUp() {
+ stateMachineExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @After
+ public void tearDown() {
+ stateMachineExecutor.shutdownNow();
+ }
+
+ @Test
+ public void testAnalysisAttemptIsRolledBackOnlyWhenRetryStarts() {
+ RecordingPlanner planner = new RecordingPlanner(false);
+ QueryExecution execution =
+ new QueryExecution(planner, createQueryContext("retry_succeeds"),
stateMachineExecutor);
+
+ assertEquals(Arrays.asList("begin", "analyze"),
planner.getAnalysisEvents());
+
+ execution.start();
+
+ assertEquals(1, planner.getScheduleAttempts());
+ // PENDING_RETRY must keep the journal alive until the plans from this
attempt have been
+ // stopped. In particular, returning from start() must not commit or roll
back the attempt.
+ assertEquals(Arrays.asList("begin", "analyze"),
planner.getAnalysisEvents());
+
+ ExecutionResult result = execution.getStatus();
+
+ assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(),
result.status.code);
+ assertEquals(2, planner.getScheduleAttempts());
+ assertEquals(
+ Arrays.asList("begin", "analyze", "rollback", "begin", "analyze",
"commit"),
+ planner.getAnalysisEvents());
+ }
+
+ @Test
+ public void testAnalysisAttemptIsRolledBackWhenRetryLimitIsReached() throws
Exception {
+ RecordingPlanner planner = new RecordingPlanner(true);
+ QueryExecution execution =
+ new QueryExecution(planner, createQueryContext("retry_exhausted"),
stateMachineExecutor);
+
+ execution.start();
+ assertEquals(Arrays.asList("begin", "analyze"),
planner.getAnalysisEvents());
+
+ setRetryCount(execution, Integer.MAX_VALUE);
+ ExecutionResult result = execution.getStatus();
+
+ assertEquals(TSStatusCode.DISPATCH_ERROR.getStatusCode(),
result.status.code);
+ assertEquals(Arrays.asList("begin", "analyze", "rollback"),
planner.getAnalysisEvents());
+ assertFalse(planner.getAnalysisEvents().contains("commit"));
+ }
+
+ private static MPPQueryContext createQueryContext(String queryId) {
+ MPPQueryContext context =
+ new MPPQueryContext(
+ "",
+ new QueryId(queryId),
+ new SessionInfo(0, "test", ZoneId.systemDefault(), ""),
+ new TEndPoint(),
+ new TEndPoint());
+ // Keeping this as a non-query avoids constructing a result exchange
handle. The fake scheduler
+ // still drives the same QueryStateMachine transitions used by a retryable
query dispatch.
+ context.setQueryType(QueryType.WRITE);
+ return context;
+ }
+
+ private static void setRetryCount(QueryExecution execution, int retryCount)
throws Exception {
+ Field retryCountField =
QueryExecution.class.getDeclaredField("retryCount");
+ retryCountField.setAccessible(true);
+ retryCountField.setInt(execution, retryCount);
+ }
+
+ private static class RecordingPlanner implements IPlanner {
+
+ private final IAnalysis analysis = mock(IAnalysis.class);
+ private final IScheduler scheduler = mock(IScheduler.class);
+ private final LogicalQueryPlan logicalPlan = mock(LogicalQueryPlan.class);
+ private final DistributedQueryPlan distributedPlan =
mock(DistributedQueryPlan.class);
+ private final List<String> analysisEvents = new ArrayList<>();
+ private final boolean alwaysFailDispatch;
+
+ private int scheduleAttempts;
+
+ private RecordingPlanner(boolean alwaysFailDispatch) {
+ this.alwaysFailDispatch = alwaysFailDispatch;
+ when(distributedPlan.getInstances()).thenReturn(Collections.emptyList());
+ }
+
+ @Override
+ public void beginAnalysisAttempt() {
+ analysisEvents.add("begin");
+ }
+
+ @Override
+ public IAnalysis analyze(MPPQueryContext context) {
+ analysisEvents.add("analyze");
+ return analysis;
+ }
+
+ @Override
+ public void rollbackAnalysisAttempt() {
+ analysisEvents.add("rollback");
+ }
+
+ @Override
+ public void commitAnalysisAttempt() {
+ analysisEvents.add("commit");
+ }
+
+ @Override
+ public LogicalQueryPlan doLogicalPlan(IAnalysis analysis, MPPQueryContext
context) {
+ return logicalPlan;
+ }
+
+ @Override
+ public DistributedQueryPlan doDistributionPlan(
+ IAnalysis analysis, LogicalQueryPlan logicalPlan) {
+ return distributedPlan;
+ }
+
+ @Override
+ public IScheduler doSchedule(
+ IAnalysis analysis,
+ DistributedQueryPlan distributedPlan,
+ MPPQueryContext context,
+ QueryStateMachine stateMachine) {
+ scheduleAttempts++;
+ stateMachine.transitionToDispatching();
+ if (alwaysFailDispatch || scheduleAttempts == 1) {
+ TSStatus dispatchFailure =
RpcUtils.getStatus(TSStatusCode.DISPATCH_ERROR);
+ stateMachine.transitionToPendingRetry(dispatchFailure);
+ } else {
+ stateMachine.transitionToRunning();
+ }
+ return scheduler;
+ }
+
+ @Override
+ public void invalidatePartitionCache() {
+ // No cache is used by this fake planner.
+ }
+
+ @Override
+ public ScheduledExecutorService getScheduledExecutorService() {
+ return null;
+ }
+
+ @Override
+ public void setRedirectInfo(IAnalysis analysis, TEndPoint localEndPoint,
TSStatus status) {
+ // Redirect information is irrelevant to the retry journal lifecycle.
+ }
+
+ private List<String> getAnalysisEvents() {
+ return analysisEvents;
+ }
+
+ private int getScheduleAttempts() {
+ return scheduleAttempts;
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilderTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilderTest.java
new file mode 100644
index 00000000000..07e7baa99fc
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LogicalPlanBuilderTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.planner;
+
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
+import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.SortNode;
+import org.apache.iotdb.db.queryengine.plan.statement.component.OrderByKey;
+import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
+import org.apache.iotdb.db.queryengine.plan.statement.component.SortItem;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
+
+import org.junit.Test;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class LogicalPlanBuilderTest {
+
+ @Test
+ public void testPlanDeviceViewDoesNotAppendDefaultSortItemsToStatement() {
+ QueryStatement queryStatement =
+ (QueryStatement)
+ StatementGenerator.createStatement(
+ "select s1 from root.sg.* order by time desc align by device",
+ ZonedDateTime.now().getOffset());
+ int originalSortItemCount = queryStatement.getSortItemList().size();
+
+ planDeviceView(queryStatement, "first_plan");
+ int sortItemCountAfterFirstPlan = queryStatement.getSortItemList().size();
+ planDeviceView(queryStatement, "retry_plan");
+
+ assertEquals(
+ Arrays.asList(originalSortItemCount, originalSortItemCount),
+ Arrays.asList(sortItemCountAfterFirstPlan,
queryStatement.getSortItemList().size()));
+ }
+
+ @Test
+ public void testPlanOrderByUsesDeviceViewMergeKeysWithoutMutatingStatement()
{
+ QueryStatement queryStatement =
+ (QueryStatement)
+ StatementGenerator.createStatement(
+ "select s1 from root.sg.* order by s1 desc align by device",
+ ZonedDateTime.now().getOffset());
+ List<SortItem> originalSortItems = new
ArrayList<>(queryStatement.getSortItemList());
+ String originalOrderByClause =
queryStatement.getOrderByComponent().toSQLString();
+ List<SortItem> expectedSortItems = new ArrayList<>(originalSortItems);
+ expectedSortItems.add(new SortItem(OrderByKey.DEVICE, Ordering.ASC));
+ expectedSortItems.add(new SortItem(OrderByKey.TIME, Ordering.ASC));
+
+ Analysis analysis = new Analysis();
+ analysis.setOrderByExpressions(Collections.emptySet());
+ analysis.setSelectExpressions(Collections.emptySet());
+ LogicalPlanBuilder builder =
+ new LogicalPlanBuilder(analysis, new MPPQueryContext(new
QueryId("two_stage_plan")))
+ .planDeviceView(
+ Collections.emptyMap(),
+ Collections.emptySet(),
+ Collections.emptyMap(),
+ Collections.emptySet(),
+ queryStatement,
+ analysis)
+ .planOrderBy(queryStatement, analysis);
+
+ assertTrue(builder.getRoot() instanceof SortNode);
+ assertTrue(
+ ((SortNode) builder.getRoot())
+ .getOrderByParameter()
+ .getSortItemList()
+ .get(0)
+ .isExpression());
+ assertEquals(
+ expectedSortItems, ((SortNode)
builder.getRoot()).getOrderByParameter().getSortItemList());
+ assertEquals(originalSortItems, queryStatement.getSortItemList());
+ assertEquals(originalOrderByClause,
queryStatement.getOrderByComponent().toSQLString());
+ }
+
+ private static void planDeviceView(QueryStatement queryStatement, String
queryId) {
+ Analysis analysis = new Analysis();
+ new LogicalPlanBuilder(analysis, new MPPQueryContext(new QueryId(queryId)))
+ .planDeviceView(
+ Collections.emptyMap(),
+ Collections.emptySet(),
+ Collections.emptyMap(),
+ Collections.emptySet(),
+ queryStatement,
+ analysis);
+ }
+}