This is an automated email from the ASF dual-hosted git repository.

englefly pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new f054492cbb9 [improvement](parser) Factor common EXPLAIN and CTE 
prefixes (#67427)
f054492cbb9 is described below

commit f054492cbb9fb76a0ac636ff4454a7303bae91c8
Author: morrySnow <[email protected]>
AuthorDate: Thu Sep 3 00:15:25 2026 +0800

    [improvement](parser) Factor common EXPLAIN and CTE prefixes (#67427)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: The SQL grammar repeatedly predicts the optional
    `EXPLAIN` and CTE prefixes in `queryOrDmlStatement`, `dmlStatement`, and
    `query`, causing long adaptive lookahead for CTE DML. This change
    consumes the common prefix once, dispatches the remaining query or DML
    body, and passes the prefix contexts to the FE plan builder. It
    preserves non-explainable LOAD/EXPORT/REPLAY/COPY/TRUNCATE branches,
    nested source CTEs, error positions, and FE command semantics.
    
    This is now an independent PR based directly on `master`. The seven
    functional, test, and benchmark files have the same stable patch ID
    (`db03fbcb3a0ca4cfee8a1422bc78e7f753e6ae51`) as the original P1 commit.
    Only the TODO status update that depended on the earlier roadmap PR was
    omitted.
    
    ### Benchmark
    
    The benchmark results below are reused from the original P1 validation
    because the replayed functional patch is identical. The benchmark
    measures the public parser facade and a pre-tokenized parser-only path;
    lower latency is better.
    
    - Host: MacBookPro17,1, Apple M1 (8 cores, 16 GB), macOS 15.0.1
    - Runtime: OpenJDK 17.0.20.1, ANTLR 4.13.1, JMH 1.37, 1 thread, 1 GB
    heap, `-prof gc`
    - Standard run: 3 forks, 4 x 300 ms warmup, 7 x 400 ms measurement
    - Longer target run: 3 forks, 6 x 500 ms warmup, 10 x 700 ms measurement
    - Measurement baseline: `d7f44fcfedd`; benchmark jar SHA-256
    `2e33becac22a27d8c40eec7c821c506cbc9bcedfda277725fa8fee40c6d35116`
    - Measurement candidate: `5e0eadb13e9`; benchmark jar SHA-256
    `743c133321dbb1a2283ea1836e5a9568a14ecc2ae350b2fc239d354d7988b423`
    - Harness:
    
`fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrDmlCommonPrefixBenchmark.java`
    
    #### Longer parser-only target run
    
    | Workload | Baseline us/op | Candidate us/op | Latency change |
    Baseline B/op | Candidate B/op | Allocation change |
    |---|---:|---:|---:|---:|---:|---:|
    | CTE SELECT | 43.273 +/- 1.101 | 43.498 +/- 1.528 | +0.52% | 73,453.8 |
    73,195.1 | -0.35% |
    | CTE INSERT | 48.439 +/- 1.233 | 42.907 +/- 1.236 | -11.42% | 74,096.5
    | 74,560.4 | +0.63% |
    | EXPLAIN CTE INSERT | 48.032 +/- 1.446 | 42.356 +/- 0.580 | -11.82% |
    74,360.5 | 74,963.1 | +0.81% |
    
    #### Public-facade control cases from the standard run
    
    | Workload | Baseline us/op | Candidate us/op | Latency change |
    Baseline B/op | Candidate B/op |
    |---|---:|---:|---:|---:|---:|
    | SELECT control | 2.636 +/- 0.187 | 2.388 +/- 0.167 | -9.42% | 5,200.0
    | 4,928.0 |
    | EXPLAIN SELECT | 7.094 +/- 1.151 | 6.316 +/- 0.274 | -10.97% |
    10,730.8 | 10,728.1 |
    
    The previous grammar performs about 56 tokens of top-level lookahead for
    a long CTE and then about 62 more tokens in the DML decision. Factoring
    the prefix removes the repeated prediction, which accounts for the CTE
    INSERT gains. CTE SELECT remains flat within overlapping confidence
    intervals, and target allocation changes remain below 1%. The standard
    end-to-end long-CTE forks were noisy, so this PR makes no precise
    end-to-end long-CTE or whole-FE latency claim.
    
    ### Semantic differential
    
    - Baseline: current `master` at `049410596f4d`; parser jar SHA-256
    `923ed2a22142ee9b5dcfefbba5766b9696a653a4218e8208e42a61270e9d986f`
    - Candidate parser jar SHA-256:
    `3e43f2e025f4152d109a692ba68985c512a155fc2f21345a550911b1ef29af8f`
    - Corpus: all 4,610 tracked `*.sql` files; manifest SHA-256
    `567e209d57e5eaf6546ff03bf887437b8d647ed5f7ecb85bc657b987dd04be10`
    - Legacy and ANSI results: 4,275 parsed and 335 rejected in both
    artifacts
    - Per-file statement count, exception type, first error position, and
    full error message are byte-identical; result SHA-256
    `897b6167aa718d5d14887c1752d4bd849cb1c55f7926b474e5dd2f9562a26fbd`
    - Lexer and token behavior are unchanged by construction.
    
    ### Release note
---
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  66 +++++++-----
 .../parser/LogicalPlanBuilderForEncryption.java    |  25 +++--
 .../parser/ParseInsertPartitionSpecTest.java       |   5 +-
 .../parser/QueryOrDmlCommonPrefixPlanTest.java     |  95 +++++++++++++++++
 .../benchmark/QueryOrDmlCommonPrefixBenchmark.java | 116 +++++++++++++++++++++
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |  27 +++--
 .../sqlparser/QueryOrDmlCommonPrefixTest.java      | 106 +++++++++++++++++++
 7 files changed, 400 insertions(+), 40 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 55e92d7c0d8..0382d4641b9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -221,6 +221,8 @@ import org.apache.doris.nereids.DorisParser.ExceptContext;
 import org.apache.doris.nereids.DorisParser.ExceptOrReplaceContext;
 import org.apache.doris.nereids.DorisParser.ExistContext;
 import org.apache.doris.nereids.DorisParser.ExplainContext;
+import org.apache.doris.nereids.DorisParser.ExplainableDmlStatementContext;
+import org.apache.doris.nereids.DorisParser.ExplainableStatementContext;
 import org.apache.doris.nereids.DorisParser.ExportContext;
 import org.apache.doris.nereids.DorisParser.ExpressionWithEofContext;
 import org.apache.doris.nereids.DorisParser.ExpressionWithOrderContext;
@@ -458,7 +460,6 @@ import 
org.apache.doris.nereids.DorisParser.SortClauseContext;
 import org.apache.doris.nereids.DorisParser.SortItemContext;
 import org.apache.doris.nereids.DorisParser.SpecifiedPartitionContext;
 import org.apache.doris.nereids.DorisParser.StarContext;
-import org.apache.doris.nereids.DorisParser.StatementDefaultContext;
 import org.apache.doris.nereids.DorisParser.StatementScopeContext;
 import org.apache.doris.nereids.DorisParser.StepPartitionDefContext;
 import org.apache.doris.nereids.DorisParser.StringLiteralContext;
@@ -1229,14 +1230,24 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
     }
 
     @Override
-    public LogicalPlan visitStatementDefault(StatementDefaultContext ctx) {
-        LogicalPlan plan = plan(ctx.query());
+    public LogicalPlan visitExplainableStatement(ExplainableStatementContext 
ctx) {
+        if (ctx.dmlStatementBody() != null) {
+            return plan(ctx.dmlStatementBody());
+        }
+        LogicalPlan plan = ParserUtils.withOrigin(
+                ctx.cteContext != null ? ctx.cteContext : ctx.queryTerm(),
+                () -> withCte(buildQuery(ctx.queryTerm(), 
ctx.queryOrganization()), ctx.cteContext));
         if (ctx.outFileClause() != null) {
             plan = withOutFile(plan, ctx.outFileClause());
         } else {
             plan = new UnboundResultSink<>(plan);
         }
-        return withExplain(plan, ctx.explain());
+        return withExplain(plan, ctx.explainContext);
+    }
+
+    @Override
+    public LogicalPlan 
visitExplainableDmlStatement(ExplainableDmlStatementContext ctx) {
+        return plan(ctx.dmlStatementBody());
     }
 
     @Override
@@ -1451,12 +1462,12 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                 tvfName, properties, DMLCommandType.INSERT, plan);
 
         Optional<LogicalPlan> cte = Optional.empty();
-        if (ctx.cte() != null) {
-            cte = Optional.ofNullable(withCte(plan, ctx.cte()));
+        if (ctx.cteContext != null) {
+            cte = Optional.ofNullable(withCte(plan, ctx.cteContext));
         }
 
         LogicalPlan command = new InsertIntoTVFCommand(sink, labelName, cte);
-        return withExplain(command, ctx.explain());
+        return withExplain(command, ctx.explainContext);
     }
 
     /**
@@ -1507,8 +1518,8 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                 plan,
                 partitionSpec.isStaticPartition() ? 
partitionSpec.getStaticPartitionValues() : null);
         Optional<LogicalPlan> cte = Optional.empty();
-        if (ctx.cte() != null) {
-            cte = Optional.ofNullable(withCte(plan, ctx.cte()));
+        if (ctx.cteContext != null) {
+            cte = Optional.ofNullable(withCte(plan, ctx.cteContext));
         }
         LogicalPlan command;
         if (isOverwrite) {
@@ -1525,12 +1536,14 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                 command = new InsertIntoTableCommand(sink, labelName, 
Optional.empty(), cte, true, branchName);
             }
         }
-        return withExplain(command, ctx.explain());
+        return withExplain(command, ctx.explainContext);
     }
 
     @Override
     public Object visitMergeInto(MergeIntoContext ctx) {
-        return ParserUtils.withOrigin(ctx, () -> {
+        ParserRuleContext originContext = ctx.explainContext != null
+                ? ctx.explainContext : ctx.cteContext != null ? ctx.cteContext 
: ctx;
+        return ParserUtils.withOrigin(originContext, () -> {
             List<String> targetNameParts = 
visitMultipartIdentifier(ctx.targetTable);
             Optional<String> targetAlias = Optional.ofNullable(
                     ctx.identifier() != null ? ctx.identifier().getText() : 
null);
@@ -1540,11 +1553,11 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
             List<MergeNotMatchedClause> notMatchedClauses = 
visit(ctx.mergeNotMatchedClause(),
                     MergeNotMatchedClause.class);
             Optional<LogicalPlan> cte = Optional.empty();
-            if (ctx.cte() != null) {
-                cte = Optional.ofNullable(withCte(source, ctx.cte()));
+            if (ctx.cteContext != null) {
+                cte = Optional.ofNullable(withCte(source, ctx.cteContext));
             }
             return withExplain(new MergeIntoCommand(targetNameParts, 
targetAlias, cte,
-                    source, onClause, matchedClauses, notMatchedClauses), 
ctx.explain());
+                    source, onClause, matchedClauses, notMatchedClauses), 
ctx.explainContext);
         });
     }
 
@@ -2077,11 +2090,11 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
             tableAlias = ctx.tableAlias().strictIdentifier().getText();
         }
         Optional<LogicalPlan> cte = Optional.empty();
-        if (ctx.cte() != null) {
-            cte = Optional.ofNullable(withCte(query, ctx.cte()));
+        if (ctx.cteContext != null) {
+            cte = Optional.ofNullable(withCte(query, ctx.cteContext));
         }
         return withExplain(new 
UpdateCommand(visitMultipartIdentifier(ctx.tableName), tableAlias,
-                visitUpdateAssignmentSeq(ctx.updateAssignmentSeq()), query, 
cte), ctx.explain());
+                visitUpdateAssignmentSeq(ctx.updateAssignmentSeq()), query, 
cte), ctx.explainContext);
     }
 
     @Override
@@ -2106,7 +2119,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                 && (ctx.queryOrganization().sortClause() != null
                         || ctx.queryOrganization().limitClause() != null);
         Command deleteCommand;
-        if (ctx.USING() == null && ctx.cte() == null && !hasQueryOrganization) 
{
+        if (ctx.USING() == null && ctx.cteContext == null && 
!hasQueryOrganization) {
             query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
             deleteCommand = new DeleteFromCommand(tableName, tableAlias, 
partitionSpec.first,
                     partitionSpec.second, query);
@@ -2119,14 +2132,14 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
             query = withQueryOrganization(query, ctx.queryOrganization());
             query = convertSortOrdinalsToUnboundSlot(query);
             Optional<LogicalPlan> cte = Optional.empty();
-            if (ctx.cte() != null) {
-                cte = Optional.ofNullable(withCte(query, ctx.cte()));
+            if (ctx.cteContext != null) {
+                cte = Optional.ofNullable(withCte(query, ctx.cteContext));
             }
             deleteCommand = new DeleteFromUsingCommand(tableName, tableAlias,
                     partitionSpec.first, partitionSpec.second, query, cte, 
hasQueryOrganization);
         }
-        if (ctx.explain() != null) {
-            return withExplain(deleteCommand, ctx.explain());
+        if (ctx.explainContext != null) {
+            return withExplain(deleteCommand, ctx.explainContext);
         } else {
             return deleteCommand;
         }
@@ -2596,12 +2609,15 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
     public LogicalPlan visitQuery(QueryContext ctx) {
         return ParserUtils.withOrigin(ctx, () -> {
             // TODO: need to add withQueryResultClauses and withCTE
-            LogicalPlan query = plan(ctx.queryTerm());
-            query = withQueryOrganization(query, ctx.queryOrganization());
-            return withCte(query, ctx.cte());
+            return withCte(buildQuery(ctx.queryTerm(), 
ctx.queryOrganization()), ctx.cte());
         });
     }
 
+    private LogicalPlan buildQuery(QueryTermContext queryTerm, 
QueryOrganizationContext queryOrganization) {
+        LogicalPlan query = plan(queryTerm);
+        return withQueryOrganization(query, queryOrganization);
+    }
+
     @Override
     public LogicalPlan visitSetOperation(SetOperationContext ctx) {
         return ParserUtils.withOrigin(ctx, () -> {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
index 1e074463d94..c1552b58f83 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
@@ -22,6 +22,9 @@ import org.apache.doris.analysis.UserDesc;
 import org.apache.doris.common.Pair;
 import org.apache.doris.common.util.DatasourcePrintableMap;
 import org.apache.doris.nereids.DorisParser;
+import org.apache.doris.nereids.DorisParser.DmlStatementBodyContext;
+import org.apache.doris.nereids.DorisParser.DmlStatementContext;
+import org.apache.doris.nereids.DorisParser.ExplainableDmlStatementContext;
 import org.apache.doris.nereids.DorisParser.InsertTableContext;
 import org.apache.doris.nereids.DorisParser.JobFromToClauseContext;
 import org.apache.doris.nereids.trees.plans.commands.info.SetVarOp;
@@ -47,14 +50,14 @@ public class LogicalPlanBuilderForEncryption extends 
LogicalPlanBuilder {
 
     // select into outfile clause
     @Override
-    public LogicalPlan 
visitStatementDefault(DorisParser.StatementDefaultContext ctx) {
+    public LogicalPlan 
visitExplainableStatement(DorisParser.ExplainableStatementContext ctx) {
         if (ctx.outFileClause() != null && 
ctx.outFileClause().propertyClause() != null) {
             DorisParser.PropertyClauseContext propertyClauseContext = 
ctx.outFileClause().propertyClause();
             encryptProperty(visitPropertyClause(propertyClauseContext),
                     propertyClauseContext.fileProperties.start.getStartIndex(),
                     propertyClauseContext.fileProperties.stop.getStopIndex());
         }
-        return super.visitStatementDefault(ctx);
+        return super.visitExplainableStatement(ctx);
     }
 
     // export into outfile clause
@@ -227,8 +230,9 @@ public class LogicalPlanBuilderForEncryption extends 
LogicalPlanBuilder {
     // create job select tvf
     @Override
     public LogicalPlan 
visitCreateScheduledJob(DorisParser.CreateScheduledJobContext ctx) {
-        if (ctx.dmlStatement() instanceof InsertTableContext) {
-            visitInsertTable((InsertTableContext) ctx.dmlStatement());
+        InsertTableContext insertTableContext = 
getInsertTableContext(ctx.dmlStatement());
+        if (insertTableContext != null) {
+            visitInsertTable(insertTableContext);
         } else if (ctx.jobFromToClause() != null) {
             JobFromToClauseContext jobFromToClauseContext = 
ctx.jobFromToClause();
             
encryptProperty(visitPropertyItemList(jobFromToClauseContext.sourceProperties),
@@ -242,8 +246,9 @@ public class LogicalPlanBuilderForEncryption extends 
LogicalPlanBuilder {
     // alter job select tvf
     @Override
     public LogicalPlan visitAlterJob(DorisParser.AlterJobContext ctx) {
-        if (ctx.dmlStatement() instanceof InsertTableContext) {
-            visitInsertTable((InsertTableContext) ctx.dmlStatement());
+        InsertTableContext insertTableContext = 
getInsertTableContext(ctx.dmlStatement());
+        if (insertTableContext != null) {
+            visitInsertTable(insertTableContext);
         } else if (ctx.jobFromToClause() != null) {
             JobFromToClauseContext jobFromToClauseContext = 
ctx.jobFromToClause();
             
encryptProperty(visitPropertyItemList(jobFromToClauseContext.sourceProperties),
@@ -254,6 +259,14 @@ public class LogicalPlanBuilderForEncryption extends 
LogicalPlanBuilder {
         return super.visitAlterJob(ctx);
     }
 
+    private InsertTableContext getInsertTableContext(DmlStatementContext ctx) {
+        if (!(ctx instanceof ExplainableDmlStatementContext)) {
+            return null;
+        }
+        DmlStatementBodyContext body = ((ExplainableDmlStatementContext) 
ctx).dmlStatementBody();
+        return body instanceof InsertTableContext ? (InsertTableContext) body 
: null;
+    }
+
     @Override
     public LogicalPlan visitCreateResource(DorisParser.CreateResourceContext 
ctx) {
         if (ctx.properties != null) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/ParseInsertPartitionSpecTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/ParseInsertPartitionSpecTest.java
index 70a3d0e6375..e1b74cd9d2c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/ParseInsertPartitionSpecTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/ParseInsertPartitionSpecTest.java
@@ -83,8 +83,11 @@ public class ParseInsertPartitionSpecTest {
      * Helper method to parse SQL and extract PartitionSpecContext.
      */
     private DorisParser.PartitionSpecContext parsePartitionSpec(String 
insertSql) {
-        DorisParser.InsertTableContext insertTableContext = 
(DorisParser.InsertTableContext) NereidsParser.toAst(
+        DorisParser.DmlStatementContext dmlStatementContext = 
(DorisParser.DmlStatementContext) NereidsParser.toAst(
                 insertSql, DorisParser::dmlStatement);
+        DorisParser.DmlStatementBodyContext dmlStatementBody =
+                ((DorisParser.ExplainableDmlStatementContext) 
dmlStatementContext).dmlStatementBody();
+        DorisParser.InsertTableContext insertTableContext = 
(DorisParser.InsertTableContext) dmlStatementBody;
         return insertTableContext.partitionSpec();
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/QueryOrDmlCommonPrefixPlanTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/QueryOrDmlCommonPrefixPlanTest.java
new file mode 100644
index 00000000000..259b04478da
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/QueryOrDmlCommonPrefixPlanTest.java
@@ -0,0 +1,95 @@
+// 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.doris.nereids.parser;
+
+import org.apache.doris.nereids.analyzer.UnboundResultSink;
+import org.apache.doris.nereids.exceptions.ParseException;
+import org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand;
+import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTE;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+public class QueryOrDmlCommonPrefixPlanTest extends ParserTestBase {
+    private final NereidsParser parser = new NereidsParser();
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("statementPlans")
+    public void buildsExpectedPlanAfterCommonPrefix(
+            String description, String sql, Class<? extends LogicalPlan> 
expectedClass) {
+        Assertions.assertInstanceOf(expectedClass, parser.parseSingle(sql));
+    }
+
+    private static Stream<Arguments> statementPlans() {
+        return Stream.of(
+                Arguments.of("CTE query", "WITH c AS (SELECT 1) SELECT * FROM 
c", UnboundResultSink.class),
+                Arguments.of("explain CTE query", "EXPLAIN WITH c AS (SELECT 
1) SELECT * FROM c",
+                        ExplainCommand.class),
+                Arguments.of("CTE insert", "WITH c AS (SELECT 1) INSERT INTO 
db.t SELECT * FROM c",
+                        InsertIntoTableCommand.class),
+                Arguments.of("CTE update",
+                        "WITH c AS (SELECT 1 AS id) UPDATE db.t SET v = 1 FROM 
c WHERE t.id = c.id",
+                        UpdateCommand.class),
+                Arguments.of("CTE delete",
+                        "WITH c AS (SELECT 1 AS id) DELETE FROM db.t USING c 
WHERE t.id = c.id",
+                        DeleteFromUsingCommand.class),
+                Arguments.of("CTE merge",
+                        "WITH c AS (SELECT 1 AS id) MERGE INTO db.t USING c ON 
t.id = c.id "
+                                + "WHEN MATCHED THEN UPDATE SET v = 1",
+                        MergeIntoCommand.class));
+    }
+
+    @Test
+    public void preservesExplainAndCtePlanShape() {
+        ExplainCommand explain = (ExplainCommand) parser.parseSingle(
+                "EXPLAIN WITH c AS (SELECT 1) SELECT * FROM c");
+        UnboundResultSink<?> sink = (UnboundResultSink<?>) 
explain.getLogicalPlan();
+        Assertions.assertInstanceOf(LogicalCTE.class, sink.child());
+    }
+
+    @Test
+    public void preservesExplainAndCteForInsert() {
+        ExplainCommand explain = (ExplainCommand) parser.parseSingle(
+                "EXPLAIN WITH c AS (SELECT 1) INSERT INTO db.t SELECT * FROM 
c");
+        Assertions.assertInstanceOf(InsertIntoTableCommand.class, 
explain.getLogicalPlan());
+        
Assertions.assertTrue(explain.getLogicalPlan().toDigest().contains("WITH\n"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("invalidPrefixCombinations")
+    public void rejectsInvalidPrefixCombinations(String sql) {
+        Assertions.assertThrows(ParseException.class, () -> 
parser.parseSingle(sql));
+    }
+
+    private static Stream<String> invalidPrefixCombinations() {
+        return Stream.of(
+                "EXPLAIN TRUNCATE TABLE t",
+                "WITH c AS (SELECT 1) TRUNCATE TABLE t",
+                "WITH c AS (SELECT 1)");
+    }
+}
diff --git 
a/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrDmlCommonPrefixBenchmark.java
 
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrDmlCommonPrefixBenchmark.java
new file mode 100644
index 00000000000..b2700dce7f8
--- /dev/null
+++ 
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrDmlCommonPrefixBenchmark.java
@@ -0,0 +1,116 @@
+// 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.doris.sqlparser.benchmark;
+
+import org.apache.doris.nereids.DorisParser;
+import org.apache.doris.nereids.parser.ParseErrorListener;
+import org.apache.doris.nereids.parser.PostProcessor;
+import org.apache.doris.sqlparser.DorisSqlParser;
+
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.ListTokenSource;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.atn.PredictionMode;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/** Measures common EXPLAIN/CTE prefix dispatch in isolation and through the 
public parser facade. */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Fork(value = 3, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
+@Warmup(iterations = 4, time = 300, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 7, time = 400, timeUnit = TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+public class QueryOrDmlCommonPrefixBenchmark {
+    @Param({"control", "explain", "cteSelect", "cteInsert", 
"explainCteInsert"})
+    public String workload;
+
+    private final DorisSqlParser facade = new DorisSqlParser();
+    private final PostProcessor postProcessor = new PostProcessor();
+    private final ParseErrorListener errorListener = new ParseErrorListener();
+
+    private String sql;
+    private List<Token> tokens;
+
+    @Setup(Level.Trial)
+    public void setUp() {
+        String cte = buildCte(12);
+        switch (workload) {
+            case "control":
+                sql = "SELECT 1";
+                break;
+            case "explain":
+                sql = "EXPLAIN SELECT a, b FROM t WHERE a > 1";
+                break;
+            case "cteSelect":
+                sql = cte + " SELECT * FROM c11";
+                break;
+            case "cteInsert":
+                sql = cte + " INSERT INTO target_table SELECT * FROM c11";
+                break;
+            case "explainCteInsert":
+                sql = "EXPLAIN " + cte + " INSERT INTO target_table SELECT * 
FROM c11";
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown workload: " + 
workload);
+        }
+
+        CommonTokenStream stream = new CommonTokenStream(facade.newLexer(sql));
+        stream.fill();
+        tokens = List.copyOf(stream.getTokens());
+    }
+
+    @Benchmark
+    public Object parseEndToEnd() {
+        return facade.parseStatement(sql);
+    }
+
+    @Benchmark
+    public Object parsePreTokenized() {
+        CommonTokenStream stream = new CommonTokenStream(new 
ListTokenSource(tokens));
+        DorisParser parser = new DorisParser(stream);
+        parser.addParseListener(postProcessor);
+        parser.removeErrorListeners();
+        parser.addErrorListener(errorListener);
+        parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
+        return parser.singleStatement();
+    }
+
+    private static String buildCte(int count) {
+        return "WITH " + IntStream.range(0, count)
+                .mapToObj(index -> index == 0
+                        ? "c0 AS (SELECT 0 AS k)"
+                        : "c" + index + " AS (SELECT k + 1 AS k FROM c" + 
(index - 1) + ")")
+                .collect(Collectors.joining(", "));
+    }
+}
diff --git 
a/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 
b/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index b2cdefd8901..148563daf8e 100644
--- a/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -105,8 +105,10 @@ statementBase
     ;
 
 queryOrDmlStatement
-    : explain? query outFileClause?     #statementDefault
-    | dmlStatement                      #dmlStatementAlias
+    : explainContext=explain? cteContext=cte?
+        (queryTerm queryOrganization outFileClause?
+        | dmlStatementBody[$explainContext.ctx, $cteContext.ctx])    
#explainableStatement
+    | nonExplainableDmlStatement        #dmlStatementAlias
     | describeStatement                 #describeStatementAlias
     | otherStatement                    #otherStatementAlias
     | loadDmlStatement                  #loadStatementAlias
@@ -274,31 +276,40 @@ optSpecBranch
     ;
 
 dmlStatement
-    : explain? cte? INSERT INTO tvfName=identifier
+    : explainContext=explain? cteContext=cte?
+        dmlStatementBody[$explainContext.ctx, $cteContext.ctx]      
#explainableDmlStatement
+    | nonExplainableDmlStatement                                   
#nonExplainableDmlStatementAlias
+    ;
+
+dmlStatementBody[ExplainContext explainContext, CteContext cteContext]
+    : INSERT INTO tvfName=identifier
         LEFT_PAREN tvfProperties=propertyItemList RIGHT_PAREN
         (WITH LABEL labelName=identifier)?
         query                                                          
#insertIntoTVF
-    | explain? cte? INSERT (INTO | OVERWRITE TABLE)
+    | INSERT (INTO | OVERWRITE TABLE)
         (tableName=multipartIdentifier (optSpecBranch)? | 
DORIS_INTERNAL_TABLE_ID LEFT_PAREN tableId=INTEGER_VALUE RIGHT_PAREN)
         partitionSpec?  // partition define
         (WITH LABEL labelName=identifier)? cols=identifierList?  // label and 
columns define
         (LEFT_BRACKET hints=identifierSeq RIGHT_BRACKET)?  // hint define
         query                                                          
#insertTable
-    | explain? cte? UPDATE tableName=multipartIdentifier tableAlias
+    | UPDATE tableName=multipartIdentifier tableAlias
         SET updateAssignmentSeq
         fromClause?
         whereClause?
         queryOrganization                                              #update
-    | explain? cte? DELETE FROM tableName=multipartIdentifier
+    | DELETE FROM tableName=multipartIdentifier
         partitionSpec? tableAlias
         (USING relations)?
         whereClause?
         queryOrganization                                              #delete
-    | explain? cte? MERGE INTO targetTable=multipartIdentifier
+    | MERGE INTO targetTable=multipartIdentifier
         (AS? identifier)? USING srcRelation=relationPrimary
         ON expression
         (mergeMatchedClause | mergeNotMatchedClause)+                   
#mergeInto
-    | LOAD LABEL lableName=multipartIdentifier
+    ;
+
+nonExplainableDmlStatement
+    : LOAD LABEL lableName=multipartIdentifier
         LEFT_PAREN dataDescs+=dataDesc (COMMA dataDescs+=dataDesc)* RIGHT_PAREN
         (withRemoteStorageSystem)?
         propertyClause?
diff --git 
a/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/QueryOrDmlCommonPrefixTest.java
 
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/QueryOrDmlCommonPrefixTest.java
new file mode 100644
index 00000000000..cb69398ddd5
--- /dev/null
+++ 
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/QueryOrDmlCommonPrefixTest.java
@@ -0,0 +1,106 @@
+// 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.doris.sqlparser;
+
+import org.apache.doris.nereids.DorisParser.SingleStatementContext;
+import org.apache.doris.nereids.exceptions.ParseException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+class QueryOrDmlCommonPrefixTest {
+    private final DorisSqlParser parser = new DorisSqlParser();
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("explainableStatements")
+    void parsesExplainAndCtePrefixes(String description, String sql) {
+        SingleStatementContext context = parser.parseStatement(sql);
+        Assertions.assertNotNull(context.statement());
+    }
+
+    private static Stream<Arguments> explainableStatements() {
+        String longCte = buildCte(20);
+        return Stream.of(
+                Arguments.of("query", "SELECT 1"),
+                Arguments.of("explain query", "EXPLAIN SELECT 1"),
+                Arguments.of("CTE query", "WITH c AS (SELECT 1) SELECT * FROM 
c"),
+                Arguments.of("explain CTE query", "EXPLAIN WITH c AS (SELECT 
1) SELECT * FROM c"),
+                Arguments.of("long CTE query", longCte + " SELECT * FROM c19"),
+                Arguments.of("CTE insert", "WITH c AS (SELECT 1) INSERT INTO t 
SELECT * FROM c"),
+                Arguments.of("long CTE insert", longCte + " INSERT INTO t 
SELECT * FROM c19"),
+                Arguments.of("explain CTE insert",
+                        "EXPLAIN WITH c AS (SELECT 1) INSERT OVERWRITE TABLE t 
SELECT * FROM c"),
+                Arguments.of("outer and source CTE",
+                        "WITH c AS (SELECT 1) INSERT INTO t WITH d AS (SELECT 
* FROM c) SELECT * FROM d"),
+                Arguments.of("CTE update",
+                        "WITH c AS (SELECT 1 AS id) UPDATE t SET v = 1 FROM c 
WHERE t.id = c.id"),
+                Arguments.of("CTE delete",
+                        "WITH c AS (SELECT 1 AS id) DELETE FROM t USING c 
WHERE t.id = c.id"),
+                Arguments.of("CTE merge",
+                        "WITH c AS (SELECT 1 AS id) MERGE INTO t USING c ON 
t.id = c.id "
+                                + "WHEN MATCHED THEN UPDATE SET v = 1"),
+                Arguments.of("job DML prefix",
+                        "CREATE JOB db.job ON SCHEDULE AT '2026-01-01 
00:00:00' DO "
+                                + "EXPLAIN WITH c AS (SELECT 1) INSERT INTO t 
SELECT * FROM c"),
+                Arguments.of("non-explainable DML", "TRUNCATE TABLE t"),
+                Arguments.of("warm-up explain", "EXPLAIN WARM UP SELECT * FROM 
t"),
+                Arguments.of("describe", "DESC t"));
+    }
+
+    @ParameterizedTest(name = "rejects: {0}")
+    @MethodSource("invalidPrefixCombinations")
+    void rejectsPrefixesThatWereNotPreviouslyAccepted(String sql) {
+        Assertions.assertThrows(ParseException.class, () -> 
parser.parseStatement(sql));
+    }
+
+    private static Stream<String> invalidPrefixCombinations() {
+        return Stream.of(
+                "EXPLAIN TRUNCATE TABLE t",
+                "WITH c AS (SELECT 1) TRUNCATE TABLE t",
+                "CREATE JOB db.job ON SCHEDULE AT '2026-01-01 00:00:00' DO "
+                        + "WITH c AS (SELECT 1) SELECT * FROM c");
+    }
+
+    @ParameterizedTest(name = "truncated: {0}")
+    @MethodSource("truncatedPrefixes")
+    void reportsTruncatedPrefixesAtEndOfInput(String sql) {
+        ParseException exception = 
Assertions.assertThrows(ParseException.class, () -> parser.parseStatement(sql));
+        Assertions.assertTrue(exception.getMessage().contains("line 1, pos " + 
sql.length()), exception::getMessage);
+    }
+
+    private static Stream<String> truncatedPrefixes() {
+        return Stream.of(
+                "EXPLAIN",
+                "WITH c AS (SELECT 1)",
+                "EXPLAIN WITH c AS (SELECT 1) INSERT INTO t");
+    }
+
+    private static String buildCte(int count) {
+        return "WITH " + IntStream.range(0, count)
+                .mapToObj(index -> index == 0
+                        ? "c0 AS (SELECT 0 AS k)"
+                        : "c" + index + " AS (SELECT k + 1 AS k FROM c" + 
(index - 1) + ")")
+                .collect(Collectors.joining(", "));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to