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

morrySnow 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 146d3595566 [improvement](parser) Assign query organization to a 
single owner (#67429)
146d3595566 is described below

commit 146d35955664cc86abb87b99130425444b2d18d7
Author: morrySnow <[email protected]>
AuthorDate: Fri Sep 4 14:35:53 2026 +0800

    [improvement](parser) Assign query organization to a single owner (#67429)
    
    ### What problem does this PR solve?
    
    Problem Summary: The grammar allowed both `querySpecification` and its
    outer `query` to consume `ORDER BY` and `LIMIT`, so ANTLR repeatedly
    entered `queryOrganization` and reported full-context ambiguities. This
    change gives every clause group one owner according to ANSI mode and
    query level, makes the rule structurally non-empty, and preserves the
    existing accepted/rejected SQL matrix. It intentionally uses ANTLR's
    default error reporting; three incomplete `ORDER BY`/`LIMIT` forms now
    report the end-of-input position instead of re-anchoring the error to
    the clause keyword.
    
    ### Benchmark
    
    The original P2 benchmark is reused without rerunning it. After
    measurement, the follow-up cleanup only removed error-position
    compatibility state and grammar actions; it added no parsing decisions
    and removed work from the valid-SQL path. Lower latency is better. The
    target input is `SELECT a, b, c FROM t WHERE a > 1 ORDER BY a, b DESC
    LIMIT 20 OFFSET 10`; the control input has the same SELECT without
    query-organization clauses.
    
    - 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
    - JMH: 3 forks, 4 x 300 ms warmup, 7 x 400 ms measurement; C1-B1-C2
    interleaving
    - Measurement baseline: `5e0eadb13e9`; parser jar SHA-256
    `530f0ed45c4bac3a096373a1e932aae0025c603a6f905c3d975a2ad53bd0e7ca`;
    benchmark jar SHA-256
    `7543595b2a87f55b9b53538336cfedf55c13484302ffb487efcfb4fdd66b1c0b`
    - Measurement candidate: `773c4ee1027`; parser jar SHA-256
    `65730a22a5262370a832bda7a04ae4b8e0de5c107be65c5ae80f95c711842f59`;
    benchmark jar SHA-256
    `47284ffd6779e9d534a19a682de5a4d468f83a62a98e048982807497f1f665b6`
    - Harness:
    
`fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrganizationBenchmark.java`
    
    #### Ordered SELECT target
    
    The candidate change is calculated from the mean of C1 and C2 against
    B1. Values are us/op with JMH's 99.9% error estimate.
    
    | Path                    | Mode   | Baseline B1       | Candidate C1      
| Candidate C2      | Latency Improvement |
    
|:------------------------|:-------|------------------:|------------------:|------------------:|--------------------:|
    | End-to-end              | Legacy | 9.926 ± 0.347     | 9.303 ± 0.333     
| 9.122 ± 0.267     | **7.2% faster**     |
    | End-to-end              | ANSI   | 9.170 ± 0.437     | 8.709 ± 0.646     
| 8.492 ± 0.241     | **6.2% faster**     |
    | Pre-tokenized parser    | Legacy | 7.653 ± 0.266     | 7.246 ± 0.244     
| 7.069 ± 0.090     | **6.5% faster**     |
    | Pre-tokenized parser    | ANSI   | 7.344 ± 0.105     | 7.181 ± 0.099     
| 7.099 ± 0.076     | **2.8% faster**     |
    
    #### Plain SELECT control
    
    | Path                 | Mode   | Baseline (us/op) | Candidate (us/op) | 
Latency Improvement |
    
|:---------------------|:-------|-----------------:|------------------:|--------------------:|
    | End-to-end           | Legacy | 6.580 ± 0.710    | 6.657 ± 0.809     | 
1.2% slower         |
    | End-to-end           | ANSI   | 6.944 ± 0.648    | 6.448 ± 0.440     | 
**7.1% faster**     |
    | Pre-tokenized parser | Legacy | 5.684 ± 1.205    | 5.084 ± 0.054     | 
**10.6% faster**    |
    | Pre-tokenized parser | ANSI   | 5.313 ± 0.125    | 5.193 ± 0.089     | 
**2.3% faster**     |
    
    No control-path latency regression exceeds the 3% threshold.
    
    #### Allocation
    
    The same artifacts were measured separately with `-prof gc`. Timing
    under the profiler was noisy, so this table uses only normalized
    allocation.
    
    | Path                 | Mode   | Baseline (B/op) | Candidate (B/op) | 
Allocation Change |
    
|:---------------------|:-------|----------------:|------------------:|------------------:|
    | End-to-end           | Legacy | 15,473.6        | 15,719.6          | 
+246.0 (+1.59%)   |
    | End-to-end           | ANSI   | 15,426.8        | 15,536.1          | 
+109.3 (+0.71%)   |
    | Pre-tokenized parser | Legacy | 12,368.1        | 12,381.4          | 
+13.3 (+0.11%)    |
    | Pre-tokenized parser | ANSI   | 12,317.4        | 12,421.4          | 
+104.0 (+0.84%)   |
    
    All allocation changes are below the 3% threshold.
    
    #### Profile attribution
    
    - Before this PR, tracked SQL entered `queryOrganization` 13,196 times
    and examined 49,902 lookahead tokens; SSB and Trino profiling reported
    40 and 20 related ambiguities.
    - After this PR, the 4,275 parseable tracked SQL files enter the target
    rule 2,794 times and examine 8,645 lookahead tokens, a reduction of
    about 79% and 83% respectively.
    - LL_EXACT reports zero fallback and zero ambiguity for both the
    `querySpecification` and `queryOrganization` target decisions.
    - The gain comes from eliminating duplicate ownership and adaptive
    lookahead, not from lexer or token changes.
    
    ### Semantic differential
    
    - Original corpus: all 4,610 tracked `*.sql` files; manifest SHA-256
    `567e209d57e5eaf6546ff03bf887437b8d647ed5f7ecb85bc657b987dd04be10`
    - Original result: 4,275 parsed and 335 rejected in both artifacts and
    in both ANSI modes
    - The follow-up cleanup does not change any grammar decision or error
    occurrence, so accepted/rejected behavior remains unchanged; the full
    corpus was not rerun
    - Three deliberately changed first-error positions are covered by unit
    tests:
      - ANSI `SELECT 1 ORDER BY`: pos 9 -> 17
      - ANSI `SELECT 1 LIMIT`: pos 9 -> 14
      - Legacy `SELECT 1 LIMIT 1 ORDER BY`: pos 17 -> 25
    - Lexer and token behavior are unchanged by construction
    
    ### Release note
    
    Malformed `ORDER BY` and `LIMIT` clauses now use ANTLR's default error
    positions.
---
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  14 +-
 .../nereids/parser/QueryOrganizationPlanTest.java  |  84 ++++++++++++
 .../benchmark/QueryOrganizationBenchmark.java      | 119 +++++++++++++++++
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |  19 ++-
 .../sqlparser/QueryOrganizationBehaviorTest.java   | 142 +++++++++++++++++++++
 5 files changed, 364 insertions(+), 14 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 c296637e70c..22415dff1df 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
@@ -1241,7 +1241,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         }
         LogicalPlan plan = ParserUtils.withOrigin(
                 ctx.cteContext != null ? ctx.cteContext : ctx.queryTerm(),
-                () -> withCte(buildQuery(ctx.queryTerm(), 
ctx.queryOrganization()), ctx.cteContext));
+                () -> withCte(buildQuery(ctx.queryTerm(), ctx.organization), 
ctx.cteContext));
         if (ctx.outFileClause() != null) {
             plan = withOutFile(plan, ctx.outFileClause());
         } else {
@@ -2144,7 +2144,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
             query = withRelations(query, ((FromRelationsContext) 
ctx.fromClause()).relations().relation());
         }
         query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
-        query = withQueryOrganization(query, ctx.queryOrganization());
+        query = withQueryOrganization(query, ctx.organization);
         query = convertSortOrdinalsToUnboundSlot(query);
         String tableAlias = null;
         if (ctx.tableAlias().strictIdentifier() != null) {
@@ -2176,9 +2176,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
             tableAlias = ctx.tableAlias().strictIdentifier().getText();
         }
 
-        boolean hasQueryOrganization = ctx.queryOrganization() != null
-                && (ctx.queryOrganization().sortClause() != null
-                        || ctx.queryOrganization().limitClause() != null);
+        boolean hasQueryOrganization = ctx.organization != null;
         Command deleteCommand;
         if (ctx.USING() == null && ctx.cteContext == null && 
!hasQueryOrganization) {
             query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
@@ -2190,7 +2188,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                 query = withRelations(query, ctx.relations().relation());
             }
             query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
-            query = withQueryOrganization(query, ctx.queryOrganization());
+            query = withQueryOrganization(query, ctx.organization);
             query = convertSortOrdinalsToUnboundSlot(query);
             Optional<LogicalPlan> cte = Optional.empty();
             if (ctx.cteContext != null) {
@@ -2670,7 +2668,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
     public LogicalPlan visitQuery(QueryContext ctx) {
         return ParserUtils.withOrigin(ctx, () -> {
             // TODO: need to add withQueryResultClauses and withCTE
-            return withCte(buildQuery(ctx.queryTerm(), 
ctx.queryOrganization()), ctx.cte());
+            return withCte(buildQuery(ctx.queryTerm(), ctx.organization), 
ctx.cte());
         });
     }
 
@@ -2778,7 +2776,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
                     Optional.ofNullable(ctx.aggClause()),
                     Optional.ofNullable(ctx.havingClause()),
                     Optional.ofNullable(ctx.qualifyClause()));
-            selectPlan = withQueryOrganization(selectPlan, 
ctx.queryOrganization());
+            selectPlan = withQueryOrganization(selectPlan, ctx.organization);
             if ((selectHintMap == null) || selectHintMap.isEmpty()) {
                 return selectPlan;
             }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/QueryOrganizationPlanTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/QueryOrganizationPlanTest.java
new file mode 100644
index 00000000000..8745088a2a2
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/QueryOrganizationPlanTest.java
@@ -0,0 +1,84 @@
+// 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.exceptions.ParseException;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSort;
+import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.qe.GlobalVariable;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class QueryOrganizationPlanTest extends ParserTestBase {
+    private final NereidsParser parser = new NereidsParser();
+
+    @Test
+    void legacyModeAppliesClausesToSetOperands() {
+        withAnsiMode(false, () -> {
+            LogicalUnion unionWithRightLimit = 
Assertions.assertInstanceOf(LogicalUnion.class,
+                    parseQuery("SELECT 1 UNION ALL SELECT 2 LIMIT 1"));
+            Assertions.assertInstanceOf(LogicalLimit.class, 
unionWithRightLimit.child(1));
+
+            LogicalUnion unionWithLeftSort = 
Assertions.assertInstanceOf(LogicalUnion.class,
+                    parseQuery("SELECT 1 ORDER BY 1 UNION ALL SELECT 2"));
+            Assertions.assertInstanceOf(LogicalSort.class, 
unionWithLeftSort.child(0));
+        });
+    }
+
+    @Test
+    void ansiModeAppliesClausesToWholeSetOperation() {
+        withAnsiMode(true, () -> {
+            LogicalLimit<?> limit = 
Assertions.assertInstanceOf(LogicalLimit.class,
+                    parseQuery("SELECT 1 UNION ALL SELECT 2 LIMIT 1"));
+            Assertions.assertInstanceOf(LogicalUnion.class, limit.child());
+            Assertions.assertThrows(ParseException.class,
+                    () -> parser.parseSingle("SELECT 1 ORDER BY 1 UNION ALL 
SELECT 2"));
+        });
+    }
+
+    @Test
+    void preservesParenthesizedAndInlineTableClauses() {
+        for (boolean ansi : new boolean[] {false, true}) {
+            withAnsiMode(ansi, () -> {
+                Assertions.assertInstanceOf(LogicalLimit.class,
+                        parseQuery("(SELECT 1 UNION ALL SELECT 2) LIMIT 1"));
+                Assertions.assertInstanceOf(LogicalLimit.class,
+                        parseQuery("VALUES (1), (2) ORDER BY 1 LIMIT 1"));
+            });
+        }
+    }
+
+    private Plan parseQuery(String sql) {
+        LogicalPlan statement = parser.parseSingle(sql);
+        return statement.child(0);
+    }
+
+    private void withAnsiMode(boolean enabled, Runnable test) {
+        boolean previous = 
GlobalVariable.enable_ansi_query_organization_behavior;
+        try {
+            GlobalVariable.enable_ansi_query_organization_behavior = enabled;
+            test.run();
+        } finally {
+            GlobalVariable.enable_ansi_query_organization_behavior = previous;
+        }
+    }
+}
diff --git 
a/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrganizationBenchmark.java
 
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrganizationBenchmark.java
new file mode 100644
index 00000000000..8a907de7abc
--- /dev/null
+++ 
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/QueryOrganizationBenchmark.java
@@ -0,0 +1,119 @@
+// 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 query-organization ownership in ANSI and legacy modes. */
+@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 QueryOrganizationBenchmark {
+    @Param({"false", "true"})
+    public boolean ansi;
+
+    @Param({"plainSelect", "orderedSelect", "unionTail", "parenthesizedUnion", 
"inlineValues"})
+    public String workload;
+
+    private final PostProcessor postProcessor = new PostProcessor();
+    private final ParseErrorListener errorListener = new ParseErrorListener();
+
+    private DorisSqlParser facade;
+    private String sql;
+    private List<Token> tokens;
+
+    @Setup(Level.Trial)
+    public void setUp() {
+        facade = new DorisSqlParser(false, ansi);
+        String union = buildUnion(12);
+        switch (workload) {
+            case "plainSelect":
+                sql = "SELECT a, b, c FROM t WHERE a > 1";
+                break;
+            case "orderedSelect":
+                sql = "SELECT a, b, c FROM t WHERE a > 1 ORDER BY a, b DESC 
LIMIT 20 OFFSET 10";
+                break;
+            case "unionTail":
+                sql = union + " ORDER BY 1 LIMIT 10";
+                break;
+            case "parenthesizedUnion":
+                sql = "(" + union + ") ORDER BY 1 LIMIT 10";
+                break;
+            case "inlineValues":
+                sql = "VALUES (1, 2), (3, 4), (5, 6), (7, 8) ORDER BY 1 LIMIT 
3";
+                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.ansiSQLSyntax = ansi;
+        parser.addParseListener(postProcessor);
+        parser.removeErrorListeners();
+        parser.addErrorListener(errorListener);
+        parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
+        return parser.singleStatement();
+    }
+
+    private static String buildUnion(int count) {
+        return IntStream.range(0, count)
+                .mapToObj(index -> "SELECT " + index + " AS k")
+                .collect(Collectors.joining(" UNION ALL "));
+    }
+}
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 2686fd330fb..e8d71c7be1e 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
@@ -49,6 +49,11 @@ options { tokenVocab = DorisLexer; }
                 ctx.getParent().getText(), ctx);
     }
 
+    private boolean isQueryOrganizationStart() {
+        int tokenType = _input.LA(1);
+        return tokenType == ORDER || tokenType == LIMIT;
+    }
+
     private boolean isTupleLambdaBody() {
         if (_input.LA(1) != LEFT_PAREN) {
             return false;
@@ -131,7 +136,7 @@ statementBase
 
 queryOrDmlStatement
     : explainContext=explain? cteContext=cte?
-        (queryTerm queryOrganization outFileClause?
+        (queryTerm organization=queryOrganization? outFileClause?
         | dmlStatementBody[$explainContext.ctx, $cteContext.ctx])    
#explainableStatement
     | nonExplainableDmlStatement        #dmlStatementAlias
     | describeStatement                 #describeStatementAlias
@@ -326,12 +331,12 @@ dmlStatementBody[ExplainContext explainContext, 
CteContext cteContext]
         SET updateAssignmentSeq
         fromClause?
         whereClause?
-        queryOrganization                                              #update
+        organization=queryOrganization?                                #update
     | DELETE FROM tableName=multipartIdentifier
         partitionSpec? tableAlias
         (USING relations)?
         whereClause?
-        queryOrganization                                              #delete
+        organization=queryOrganization?                                #delete
     | MERGE INTO targetTable=multipartIdentifier
         (AS? identifier)? USING srcRelation=relationPrimary
         ON expression
@@ -1496,7 +1501,7 @@ outFileClause
     ;
 
 query
-    : cte? queryTerm queryOrganization
+    : cte? queryTerm organization=queryOrganization?
     ;
 
 queryTerm
@@ -1524,7 +1529,8 @@ querySpecification
       aggClause?
       havingClause?
       qualifyClause?
-      ({!ansiSQLSyntax}? queryOrganization | {ansiSQLSyntax}?)         
#regularQuerySpecification
+      ({!ansiSQLSyntax}? organization=queryOrganization
+      | {ansiSQLSyntax || !isQueryOrganizationStart()}?)                 
#regularQuerySpecification
     ;
 
 cte
@@ -1660,7 +1666,8 @@ unnest:
     )?;
 
 queryOrganization
-    : sortClause? limitClause?
+    : sortClause (limitClause | {_input.LA(1) != LIMIT}?)
+    | limitClause
     ;
 
 sortClause
diff --git 
a/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/QueryOrganizationBehaviorTest.java
 
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/QueryOrganizationBehaviorTest.java
new file mode 100644
index 00000000000..e81e7853dd9
--- /dev/null
+++ 
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/QueryOrganizationBehaviorTest.java
@@ -0,0 +1,142 @@
+// 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.QueryOrganizationContext;
+import org.apache.doris.nereids.DorisParser.SingleStatementContext;
+import org.apache.doris.nereids.exceptions.ParseException;
+
+import org.antlr.v4.runtime.tree.ParseTree;
+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.Stream;
+
+class QueryOrganizationBehaviorTest {
+
+    @ParameterizedTest(name = "owners: {0}, ansi={1}")
+    @MethodSource("organizationOwners")
+    void createsOneNonEmptyOwnerPerClauseGroup(String description, boolean 
ansi, String sql, int expectedOwners) {
+        SingleStatementContext statement = new DorisSqlParser(false, 
ansi).parseStatement(sql);
+        Assertions.assertEquals(expectedOwners, 
countOrganizationOwners(statement));
+    }
+
+    private static Stream<Arguments> organizationOwners() {
+        return Stream.of(
+                Arguments.of("plain select", false, "SELECT 1", 0),
+                Arguments.of("plain select", true, "SELECT 1", 0),
+                Arguments.of("ordered select", false, "SELECT 1 ORDER BY 1", 
1),
+                Arguments.of("ordered select", true, "SELECT 1 ORDER BY 1", 1),
+                Arguments.of("set tail", false, "SELECT 1 UNION ALL SELECT 2 
LIMIT 1", 1),
+                Arguments.of("set tail", true, "SELECT 1 UNION ALL SELECT 2 
LIMIT 1", 1),
+                Arguments.of("nested clauses", false, "(SELECT 1 ORDER BY 1) 
LIMIT 1", 2),
+                Arguments.of("nested clauses", true, "(SELECT 1 ORDER BY 1) 
LIMIT 1", 2),
+                Arguments.of("legacy split clauses", false, "SELECT 1 LIMIT 1 
ORDER BY 1", 2));
+    }
+
+    private static int countOrganizationOwners(ParseTree tree) {
+        int owners = 0;
+        if (tree instanceof QueryOrganizationContext) {
+            QueryOrganizationContext organization = (QueryOrganizationContext) 
tree;
+            Assertions.assertTrue(organization.sortClause() != null || 
organization.limitClause() != null);
+            owners++;
+        }
+        for (int index = 0; index < tree.getChildCount(); index++) {
+            owners += countOrganizationOwners(tree.getChild(index));
+        }
+        return owners;
+    }
+
+    @ParameterizedTest(name = "{0}, ansi={1}")
+    @MethodSource("acceptedStatements")
+    void preservesAcceptedQueryOrganizationForms(String description, boolean 
ansi, String sql) {
+        Assertions.assertNotNull(new DorisSqlParser(false, 
ansi).parseStatement(sql));
+    }
+
+    private static Stream<Arguments> acceptedStatements() {
+        return Stream.of(
+                Arguments.of("ordered select", false, "SELECT 1 ORDER BY 1"),
+                Arguments.of("ordered select", true, "SELECT 1 ORDER BY 1"),
+                Arguments.of("simple limit", false, "SELECT 1 LIMIT 3"),
+                Arguments.of("simple limit", true, "SELECT 1 LIMIT 3"),
+                Arguments.of("limit offset", false, "SELECT 1 LIMIT 3 OFFSET 
2"),
+                Arguments.of("limit offset", true, "SELECT 1 LIMIT 3 OFFSET 
2"),
+                Arguments.of("comma limit", false, "SELECT 1 LIMIT 2, 3"),
+                Arguments.of("comma limit", true, "SELECT 1 LIMIT 2, 3"),
+                Arguments.of("union tail", false, "SELECT 1 UNION ALL SELECT 2 
LIMIT 1"),
+                Arguments.of("union tail", true, "SELECT 1 UNION ALL SELECT 2 
LIMIT 1"),
+                Arguments.of("intersect tail", false, "SELECT 1 INTERSECT 
SELECT 2 ORDER BY 1"),
+                Arguments.of("intersect tail", true, "SELECT 1 INTERSECT 
SELECT 2 ORDER BY 1"),
+                Arguments.of("except tail", false, "SELECT 1 EXCEPT SELECT 2 
LIMIT 1"),
+                Arguments.of("except tail", true, "SELECT 1 EXCEPT SELECT 2 
LIMIT 1"),
+                Arguments.of("parenthesized set", false,
+                        "(SELECT 1 UNION ALL SELECT 2) ORDER BY 1 LIMIT 1"),
+                Arguments.of("parenthesized set", true,
+                        "(SELECT 1 UNION ALL SELECT 2) ORDER BY 1 LIMIT 1"),
+                Arguments.of("parenthesized operand", false,
+                        "SELECT 1 UNION ALL (SELECT 2 ORDER BY 1 LIMIT 1)"),
+                Arguments.of("parenthesized operand", true,
+                        "SELECT 1 UNION ALL (SELECT 2 ORDER BY 1 LIMIT 1)"),
+                Arguments.of("inline values", false,
+                        "VALUES (1), (2) ORDER BY 1 LIMIT 1"),
+                Arguments.of("inline values", true,
+                        "VALUES (1), (2) ORDER BY 1 LIMIT 1"),
+                Arguments.of("CTE", false,
+                        "WITH c AS (SELECT 1 ORDER BY 1 LIMIT 1) SELECT * FROM 
c LIMIT 1"),
+                Arguments.of("CTE", true,
+                        "WITH c AS (SELECT 1 ORDER BY 1 LIMIT 1) SELECT * FROM 
c LIMIT 1"),
+                Arguments.of("derived query", false,
+                        "SELECT * FROM (SELECT 1 ORDER BY 1 LIMIT 1) t ORDER 
BY 1"),
+                Arguments.of("derived query", true,
+                        "SELECT * FROM (SELECT 1 ORDER BY 1 LIMIT 1) t ORDER 
BY 1"),
+                Arguments.of("IN subquery", false,
+                        "SELECT * FROM t WHERE id IN (SELECT id FROM t ORDER 
BY id LIMIT 1)"),
+                Arguments.of("IN subquery", true,
+                        "SELECT * FROM t WHERE id IN (SELECT id FROM t ORDER 
BY id LIMIT 1)"),
+                Arguments.of("legacy operand clauses", false,
+                        "SELECT 1 ORDER BY 1 UNION ALL SELECT 2 LIMIT 1"),
+                Arguments.of("legacy split clauses", false,
+                        "SELECT 1 LIMIT 1 ORDER BY 1"));
+    }
+
+    @ParameterizedTest(name = "rejects: {0}, ansi={1}")
+    @MethodSource("rejectedStatements")
+    void preservesRejectedQueryOrganizationForms(String description, boolean 
ansi, String sql, int errorPosition) {
+        ParseException exception = 
Assertions.assertThrows(ParseException.class,
+                () -> new DorisSqlParser(false, ansi).parseStatement(sql));
+        Assertions.assertTrue(exception.getMessage().contains("line 1, pos " + 
errorPosition), exception::getMessage);
+    }
+
+    private static Stream<Arguments> rejectedStatements() {
+        return Stream.of(
+                Arguments.of("ANSI unparenthesized operand order", true,
+                        "SELECT 1 ORDER BY 1 UNION ALL SELECT 2", 9),
+                Arguments.of("order after limit", true, "SELECT 1 LIMIT 1 
ORDER BY 1", 9),
+                Arguments.of("truncated order", false, "SELECT 1 ORDER BY", 9),
+                Arguments.of("truncated order", true, "SELECT 1 ORDER BY", 17),
+                Arguments.of("truncated limit", false, "SELECT 1 LIMIT", 9),
+                Arguments.of("truncated limit", true, "SELECT 1 LIMIT", 14),
+                Arguments.of("truncated trailing limit", false, "SELECT 1 
ORDER BY 1 LIMIT", 20),
+                Arguments.of("truncated trailing limit", true, "SELECT 1 ORDER 
BY 1 LIMIT", 20),
+                Arguments.of("truncated VALUES limit", false, "VALUES (1) 
ORDER BY 1 LIMIT", 22),
+                Arguments.of("truncated VALUES limit", true, "VALUES (1) ORDER 
BY 1 LIMIT", 22),
+                Arguments.of("truncated legacy second clause", false, "SELECT 
1 LIMIT 1 ORDER BY", 25));
+    }
+}


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

Reply via email to