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 a6a7e52a733 [improvement](parser) Add case-insensitive stream fast 
paths (#67452)
a6a7e52a733 is described below

commit a6a7e52a7336fcca8dd1564cbeb0f3d726304cc4
Author: morrySnow <[email protected]>
AuthorDate: Thu Sep 10 12:07:59 2026 +0800

    [improvement](parser) Add case-insensitive stream fast paths (#67452)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    
    Every SQL lexer currently copies its input through
    `CharStreams.fromString()` and calls `Character.toUpperCase()` for every
    case-insensitive lookahead. This is visible in lexer allocation and
    identifier-heavy parsing time.
    
    Add two strictly compatible fast paths:
    
    - Fold ASCII `a-z` with arithmetic and retain `Character.toUpperCase()`
    for every other code point.
    - Read strings without UTF-16 surrogates directly, avoiding ANTLR's
    copied code-point buffer. Inputs containing any surrogate retain the
    original ANTLR stream so code-point indices, `getText`, navigation, and
    errors remain unchanged.
    
    The public arbitrary-`CharStream` constructor is preserved. All
    production String entry points now use the factory. ANTLR's native
    `caseInsensitive` option was evaluated but rejected because it changed
    existing Unicode behavior such as the handling of `ſelect`.
    
    ### Benchmark
    
    Environment and method:
    
    - Baseline: `cf33a08bcd5`, artifact SHA-256
    `51700bef953361218912abc6aec348cecdec3fe61031eba10cc1ebc534bc9183`
    - Candidate: `7aa7b5e6487`, artifact SHA-256
    `6e8edf78befc0dc349be398aae454c172b5cb968ee98d88a0e056d4c21a9c5d0`
    - macOS 15.0.1 arm64, OpenJDK 17.0.20.1, 1 GiB heap, JMH 1.37
    - 2 forks, 4 x 300 ms warmup, 7 x 400 ms measurement, `-prof gc`
    - Interleaved baseline/candidate runs; one candidate run affected by
    unrelated host load was discarded and repeated. The table averages two
    valid runs per artifact. Individual JMH scores use 99.9% confidence
    intervals.
    
    ```shell
    java -Xms1g -Xmx1g -jar <benchmark.jar> \
      
'CaseInsensitiveStreamBenchmark.(createLexer|foldPrebuiltCharacters|parseStatement|tokenize)'
 \
      -p workload=shortQuery,lowercaseIdentifiers,stringAndComment,unicode \
      -f 2 -wi 4 -i 7 -w 300ms -r 400ms -prof gc -rf json
    ```
    
    | Benchmark                              |    Baseline us/op (B1 / B2) |   
Candidate us/op (C1 / C3) | Mean change | Baseline → candidate B/op |
    | -------------------------------------- | --------------------------: | 
--------------------------: | ----------: | ------------------------: |
    | Tokenize 64 lowercase identifiers      | 10.826±0.078 / 11.408±0.125 |   
8.708±0.071 / 8.704±0.096 |      -21.7% |  14,799 → 10,464 (-29.3%) |
    | Parse 64 lowercase identifiers         | 54.775±7.360 / 56.365±4.224 | 
49.860±0.191 / 52.340±1.160 |       -8.0% |   99,395 → 95,017 (-4.4%) |
    | Tokenize `select 1`                    |   0.451±0.004 / 0.485±0.011 |   
0.348±0.069 / 0.324±0.010 |      -28.2% |      1,008 → 816 (-19.0%) |
    | Parse strings/comments with Unicode    | 27.047±5.371 / 28.435±2.932 | 
24.744±0.401 / 24.743±0.425 |      -10.8% |   24,167 → 23,784 (-1.6%) |
    | Tokenize supplementary Unicode control | 32.034±1.398 / 33.910±0.752 | 
32.303±1.108 / 32.020±0.738 |       -2.5% |   24,630 → 24,594 (-0.1%) |
    
    
    The isolated `createLexer` measurement for the surrogate-containing
    string/comment workload regresses by 5.9% because the compatibility
    guard scans for surrogates before falling back. The corresponding
    complete tokenize and parse paths improve by 3.6% and 10.8%; no
    end-to-end control workload regressed. Prebuilt lowercase character
    folding improves by 10.6%.
    
    The performance gains come from eliminating the copied input buffer for
    BMP-only SQL, avoiding its allocation, and replacing the common
    lowercase ASCII `Character.toUpperCase()` call with an arithmetic
    branch.
    
    Correctness corpus:
    
    - 4,610 tracked SQL files; baseline and candidate parse signatures match
    in legacy and ANSI modes. SHA-256:
    `69c811d0d80c52b40d8cd854e925ff4930aa6601c7d1e66541f2eb29f35db50a`.
    - 9,220 lexer cases (4,610 SQL files x both `noBackslashEscapes` modes);
    complete token tuple and lexer-error signatures match. SHA-256:
    `66780d4e1d9224ea27c8f715ab33edd247faed1571030ba9e90a8ef3f6212180`.
---
 .../httpv2/websql/SingleStatementValidator.java    |   3 +-
 .../apache/doris/nereids/parser/NereidsParser.java |   7 +-
 .../benchmark/CaseInsensitiveStreamBenchmark.java  | 140 ++++++++++++
 .../nereids/parser/CaseInsensitiveStream.java      |  91 +++++++-
 .../org/apache/doris/sqlparser/DorisSqlParser.java |   3 +-
 .../doris/sqlparser/CaseInsensitiveStreamTest.java | 245 +++++++++++++++++++++
 6 files changed, 480 insertions(+), 9 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/websql/SingleStatementValidator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/websql/SingleStatementValidator.java
index a04746bc855..a2ea4c01338 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/websql/SingleStatementValidator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/websql/SingleStatementValidator.java
@@ -23,7 +23,6 @@ import 
org.apache.doris.nereids.DorisParser.MultiStatementsContext;
 import org.apache.doris.nereids.parser.CaseInsensitiveStream;
 import org.apache.doris.nereids.parser.NereidsParser;
 
-import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
 
 /**
@@ -45,7 +44,7 @@ public final class SingleStatementValidator {
         }
 
         try {
-            DorisLexer lexer = new DorisLexer(new 
CaseInsensitiveStream(CharStreams.fromString(sql)));
+            DorisLexer lexer = new 
DorisLexer(CaseInsensitiveStream.fromString(sql));
             lexer.isNoBackslashEscapes = noBackslashEscapes;
             CommonTokenStream tokens = new CommonTokenStream(lexer);
             MultiStatementsContext parsed = (MultiStatementsContext) 
NereidsParser.toAst(
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java
index 66ca0ecdc2f..4184c0f421c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java
@@ -42,7 +42,6 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
 import org.antlr.v4.runtime.ParserRuleContext;
 import org.antlr.v4.runtime.Recognizer;
@@ -143,7 +142,7 @@ public class NereidsParser {
      * for example: select id from tbl return Tokens: ['select', 'id', 'from', 
'tbl']
      */
     public static TokenSource scan(String sql) {
-        return new DorisLexer(new 
CaseInsensitiveStream(CharStreams.fromString(sql)));
+        return new DorisLexer(CaseInsensitiveStream.fromString(sql));
     }
 
     /**
@@ -392,7 +391,7 @@ public class NereidsParser {
         while (hintToken != null && hintToken.getType() != DorisLexer.EOF) {
             if (hintToken.getChannel() == 2 && 
sql.charAt(hintToken.getStartIndex() + 2) == '+') {
                 String hintSql = sql.substring(hintToken.getStartIndex() + 3, 
hintToken.getStopIndex() + 1);
-                DorisLexer newHintLexer = new DorisLexer(new 
CaseInsensitiveStream(CharStreams.fromString(hintSql)));
+                DorisLexer newHintLexer = new 
DorisLexer(CaseInsensitiveStream.fromString(hintSql));
                 CommonTokenStream newHintTokenStream = new 
CommonTokenStream(newHintLexer);
                 DorisParser hintParser = new DorisParser(newHintTokenStream);
                 ParserRuleContext hintContext = 
parseFunction.apply(hintParser);
@@ -477,7 +476,7 @@ public class NereidsParser {
     }
 
     private static CommonTokenStream parseTokens(String sql, boolean 
leanTokenMode) {
-        DorisLexer lexer = new DorisLexer(new 
CaseInsensitiveStream(CharStreams.fromString(sql)));
+        DorisLexer lexer = new 
DorisLexer(CaseInsensitiveStream.fromString(sql));
         lexer.isNoBackslashEscapes = SqlModeHelper.hasNoBackSlashEscapes();
         lexer.isLeanTokenMode = leanTokenMode;
         CommonTokenStream tokenStream = leanTokenMode
diff --git 
a/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/CaseInsensitiveStreamBenchmark.java
 
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/CaseInsensitiveStreamBenchmark.java
new file mode 100644
index 00000000000..a60b3e64b55
--- /dev/null
+++ 
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/CaseInsensitiveStreamBenchmark.java
@@ -0,0 +1,140 @@
+// 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.DorisLexer;
+import org.apache.doris.sqlparser.DorisSqlParser;
+
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.Token;
+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.Locale;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/** Measures case-folding stream construction, lookahead, lexing, and 
end-to-end parsing. */
+@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 CaseInsensitiveStreamBenchmark {
+    @Param({
+            "shortQuery",
+            "lowercaseIdentifiers",
+            "mixedCaseIdentifiers",
+            "uppercaseIdentifiers",
+            "stringAndComment",
+            "unicode"
+    })
+    public String workload;
+
+    private DorisSqlParser parser;
+    private String sql;
+    private CharStream prebuiltStream;
+
+    @Setup(Level.Trial)
+    public void setUp() {
+        parser = new DorisSqlParser();
+        switch (workload) {
+            case "shortQuery":
+                sql = "select 1";
+                break;
+            case "lowercaseIdentifiers":
+                sql = projection("lowercase_column_", false);
+                break;
+            case "mixedCaseIdentifiers":
+                sql = projection("mixedCaseColumn_", false);
+                break;
+            case "uppercaseIdentifiers":
+                sql = projection("UPPERCASE_COLUMN_", true);
+                break;
+            case "stringAndComment":
+                sql = "select 'lowercase MixedCase 中文😀' as text_value, 
lower_name, mixedName "
+                        + "from lower_table /* lowercase MixedCase 中文😀 */ "
+                        + "where lower_name = 'another string' -- trailing 
comment\nlimit 10";
+                break;
+            case "unicode":
+                String supplementary = new String(Character.toChars(0x10428));
+                sql = "select café_列, élève_列, " + supplementary + "_column, 
'ı ſ 中文 😀' "
+                        + "from 数据表 where café_列 > 10";
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown workload: " + 
workload);
+        }
+        prebuiltStream = parser.newLexer(sql).getInputStream();
+    }
+
+    @Benchmark
+    public Object createLexer() {
+        return parser.newLexer(sql);
+    }
+
+    @Benchmark
+    public int foldPrebuiltCharacters() {
+        prebuiltStream.seek(0);
+        int checksum = 1;
+        int character;
+        while ((character = prebuiltStream.LA(1)) != Token.EOF) {
+            checksum = 31 * checksum + character;
+            prebuiltStream.consume();
+        }
+        return checksum;
+    }
+
+    @Benchmark
+    public int tokenize() {
+        DorisLexer lexer = parser.newLexer(sql);
+        int checksum = 1;
+        Token token;
+        do {
+            token = lexer.nextToken();
+            checksum = 31 * checksum + token.getType();
+            checksum = 31 * checksum + token.getStartIndex();
+            checksum = 31 * checksum + token.getStopIndex();
+        } while (token.getType() != Token.EOF);
+        return checksum;
+    }
+
+    @Benchmark
+    public Object parseStatement() {
+        return parser.parseStatement(sql);
+    }
+
+    private static String projection(String prefix, boolean uppercase) {
+        String projection = IntStream.range(0, 64)
+                .mapToObj(index -> prefix + index)
+                .collect(Collectors.joining(", "));
+        String sql = "select " + projection + " from identifier_heavy_table 
where " + prefix + "0 > 10";
+        return uppercase ? sql.toUpperCase(Locale.ROOT) : sql;
+    }
+}
diff --git 
a/fe/fe-sql-parser/src/main/java/org/apache/doris/nereids/parser/CaseInsensitiveStream.java
 
b/fe/fe-sql-parser/src/main/java/org/apache/doris/nereids/parser/CaseInsensitiveStream.java
index 54fe54ab8ba..63190f2e0fc 100644
--- 
a/fe/fe-sql-parser/src/main/java/org/apache/doris/nereids/parser/CaseInsensitiveStream.java
+++ 
b/fe/fe-sql-parser/src/main/java/org/apache/doris/nereids/parser/CaseInsensitiveStream.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.parser;
 
 import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.IntStream;
 import org.antlr.v4.runtime.misc.Interval;
 
@@ -31,6 +32,19 @@ public class CaseInsensitiveStream implements CharStream {
         this.stream = stream;
     }
 
+    /**
+     * Avoid copying strings whose UTF-16 indices already equal code-point 
indices.
+     * Strings containing surrogates keep ANTLR's code-point stream and 
indexing semantics.
+     */
+    public static CharStream fromString(String input) {
+        for (int index = 0; index < input.length(); index++) {
+            if (Character.isSurrogate(input.charAt(index))) {
+                return new 
CaseInsensitiveStream(CharStreams.fromString(input));
+            }
+        }
+        return new CaseInsensitiveStringStream(input);
+    }
+
     @Override
     public String getText(Interval interval) {
         return stream.getText(interval);
@@ -43,8 +57,13 @@ public class CaseInsensitiveStream implements CharStream {
 
     @Override
     public int LA(int i) {
-        int result = stream.LA(i);
+        return toUpperCase(stream.LA(i));
+    }
 
+    private static int toUpperCase(int result) {
+        if (result >= 'a' && result <= 'z') {
+            return result - ('a' - 'A');
+        }
         switch (result) {
             case 0:
             case IntStream.EOF:
@@ -83,4 +102,74 @@ public class CaseInsensitiveStream implements CharStream {
     public String getSourceName() {
         return stream.getSourceName();
     }
+
+    private static final class CaseInsensitiveStringStream implements 
CharStream {
+        private final String input;
+        private int position;
+
+        CaseInsensitiveStringStream(String input) {
+            this.input = input;
+        }
+
+        @Override
+        public String getText(Interval interval) {
+            int start = Math.min(interval.a, input.length());
+            int length = Math.min(interval.b - interval.a + 1, input.length() 
- start);
+            return input.substring(start, start + length);
+        }
+
+        @Override
+        public void consume() {
+            if (position == input.length()) {
+                throw new IllegalStateException("cannot consume EOF");
+            }
+            position++;
+        }
+
+        @Override
+        public int LA(int offset) {
+            int index;
+            switch (Integer.signum(offset)) {
+                case -1:
+                    index = position + offset;
+                    return index < 0 ? IntStream.EOF : 
toUpperCase(input.charAt(index));
+                case 0:
+                    return 0;
+                case 1:
+                    index = position + offset - 1;
+                    return index >= input.length() ? IntStream.EOF : 
toUpperCase(input.charAt(index));
+                default:
+                    throw new UnsupportedOperationException("Not reached");
+            }
+        }
+
+        @Override
+        public int mark() {
+            return -1;
+        }
+
+        @Override
+        public void release(int marker) {
+        }
+
+        @Override
+        public int index() {
+            return position;
+        }
+
+        @Override
+        public void seek(int index) {
+            position = index;
+        }
+
+        @Override
+        public int size() {
+            return input.length();
+        }
+
+        @Override
+        public String getSourceName() {
+            return IntStream.UNKNOWN_SOURCE_NAME;
+        }
+    }
 }
diff --git 
a/fe/fe-sql-parser/src/main/java/org/apache/doris/sqlparser/DorisSqlParser.java 
b/fe/fe-sql-parser/src/main/java/org/apache/doris/sqlparser/DorisSqlParser.java
index 6d62a273448..189c60c1f36 100644
--- 
a/fe/fe-sql-parser/src/main/java/org/apache/doris/sqlparser/DorisSqlParser.java
+++ 
b/fe/fe-sql-parser/src/main/java/org/apache/doris/sqlparser/DorisSqlParser.java
@@ -26,7 +26,6 @@ import 
org.apache.doris.nereids.DorisParser.SingleStatementContext;
 import org.apache.doris.nereids.parser.CaseInsensitiveStream;
 import org.apache.doris.nereids.parser.ParseErrorListener;
 
-import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
 import org.antlr.v4.runtime.ParserRuleContext;
 import org.antlr.v4.runtime.atn.PredictionMode;
@@ -77,7 +76,7 @@ public final class DorisSqlParser {
 
     /** Build a freshly configured lexer for advanced callers that want to 
walk tokens directly. */
     public DorisLexer newLexer(String sql) {
-        DorisLexer lexer = new DorisLexer(new 
CaseInsensitiveStream(CharStreams.fromString(sql)));
+        DorisLexer lexer = new 
DorisLexer(CaseInsensitiveStream.fromString(sql));
         lexer.isNoBackslashEscapes = noBackslashEscapes;
         return lexer;
     }
diff --git 
a/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/CaseInsensitiveStreamTest.java
 
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/CaseInsensitiveStreamTest.java
new file mode 100644
index 00000000000..7544fc6278a
--- /dev/null
+++ 
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/CaseInsensitiveStreamTest.java
@@ -0,0 +1,245 @@
+// 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.DorisLexer;
+import org.apache.doris.nereids.parser.CaseInsensitiveStream;
+
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.IntStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.misc.Interval;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+class CaseInsensitiveStreamTest {
+    private static final String SOURCE_NAME = "case-insensitive-stream-test";
+    private static final String DESERET_SMALL_LONG_I = new 
String(Character.toChars(0x10428));
+    private static final String UNPAIRED_HIGH_SURROGATE = 
Character.toString((char) 0xD801);
+
+    @Test
+    void matchesReferenceLookaheadTextAndNavigation() {
+        List<String> inputs = Arrays.asList(
+                "",
+                
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$",
+                "éÉıſß中",
+                "a" + DESERET_SMALL_LONG_I + "Z",
+                "unpaired" + UNPAIRED_HIGH_SURROGATE,
+                "select 'MiXeD 中文😀' -- comment\nfrom `Table`");
+
+        for (String input : inputs) {
+            CharStream actual = new 
CaseInsensitiveStream(CharStreams.fromString(input, SOURCE_NAME));
+            CharStream reference = new 
ReferenceCaseInsensitiveStream(CharStreams.fromString(input, SOURCE_NAME));
+            assertEquivalentStream(input, actual, reference);
+
+            CharStream stringBacked = CaseInsensitiveStream.fromString(input);
+            CharStream defaultReference = new 
ReferenceCaseInsensitiveStream(CharStreams.fromString(input));
+            assertEquivalentStream(input, stringBacked, defaultReference);
+        }
+    }
+
+    private static void assertEquivalentStream(String input, CharStream 
actual, CharStream reference) {
+        Assertions.assertEquals(reference.size(), actual.size());
+        Assertions.assertEquals(reference.getSourceName(), 
actual.getSourceName());
+        for (int start = 0; start <= reference.size(); start++) {
+            for (int end : new int[] {start - 1, start, reference.size() - 1, 
reference.size() + 2}) {
+                Assertions.assertEquals(reference.getText(Interval.of(start, 
end)),
+                        actual.getText(Interval.of(start, end)),
+                        "input=" + input + ", interval=" + start + ".." + end);
+            }
+        }
+
+        for (int position = 0; position <= reference.size(); position++) {
+            reference.seek(position);
+            actual.seek(position);
+            Assertions.assertEquals(reference.index(), actual.index());
+            for (int offset = -reference.size() - 2; offset <= 
reference.size() + 2; offset++) {
+                Assertions.assertEquals(reference.LA(offset), 
actual.LA(offset),
+                        "input=" + input + ", position=" + actual.index() + ", 
offset=" + offset);
+            }
+            Assertions.assertEquals(reference.mark(), actual.mark());
+            reference.release(-1);
+            actual.release(-1);
+        }
+
+        reference.seek(0);
+        actual.seek(0);
+        while (reference.LA(1) != IntStream.EOF) {
+            Assertions.assertEquals(reference.LA(1), actual.LA(1));
+            reference.consume();
+            actual.consume();
+        }
+        Assertions.assertEquals(reference.index(), actual.index());
+        Assertions.assertEquals(IntStream.EOF, actual.LA(1));
+        Assertions.assertThrows(IllegalStateException.class, 
reference::consume);
+        Assertions.assertThrows(IllegalStateException.class, actual::consume);
+    }
+
+    @Test
+    void matchesReferenceTokensAcrossSqlAndStringModes() {
+        List<String> inputs = new ArrayList<>(Arrays.asList(
+                "",
+                "select lower_name, Mixed_Name, UPPER_NAME from db.tbl where 
id = 1",
+                "ſelect 1, ıd from café",
+                "select " + DESERET_SMALL_LONG_I + "_name, 中文列 from 数据表",
+                "select 1.2e3x, 1.2z, .5d, 2.3W",
+                "select 'lowerCase 中文😀', \"MiXeD\", `QuotedName` -- lower 
comment\n"
+                        + "from tbl /* Mixed 中文😀 */ where c = 'a\\'b'",
+                "/*+ SET_VAR(query_timeout=1) */ select value from table_name",
+                "'unterminated\\",
+                "-- comment without newline"));
+        inputs.addAll(randomInputs());
+
+        for (boolean noBackslashEscapes : new boolean[] {false, true}) {
+            for (String input : inputs) {
+                Assertions.assertEquals(referenceSnapshot(input, 
noBackslashEscapes),
+                        actualSnapshot(input, noBackslashEscapes),
+                        () -> "input=" + input + ", noBackslashEscapes=" + 
noBackslashEscapes);
+            }
+        }
+    }
+
+    private static List<String> randomInputs() {
+        String[] alphabet = {
+                "a", "z", "A", "Z", "0", "9", "_", "$", " ", "\t", "\n",
+                "'", "\"", "`", "\\", "-", "/", "*", ".", ",", "(", ")", "+", 
"=",
+                "é", "ı", "ſ", "中", DESERET_SMALL_LONG_I
+        };
+        Random random = new Random(20260902L);
+        List<String> inputs = new ArrayList<>();
+        for (int caseIndex = 0; caseIndex < 200; caseIndex++) {
+            int length = random.nextInt(65);
+            StringBuilder input = new StringBuilder();
+            for (int index = 0; index < length; index++) {
+                input.append(alphabet[random.nextInt(alphabet.length)]);
+            }
+            inputs.add(input.toString());
+        }
+        return inputs;
+    }
+
+    private static List<String> actualSnapshot(String sql, boolean 
noBackslashEscapes) {
+        DorisLexer lexer = new DorisSqlParser(noBackslashEscapes, 
false).newLexer(sql);
+        return snapshot(lexer);
+    }
+
+    private static List<String> referenceSnapshot(String sql, boolean 
noBackslashEscapes) {
+        DorisLexer lexer = new DorisLexer(
+                new 
ReferenceCaseInsensitiveStream(CharStreams.fromString(sql)));
+        lexer.isNoBackslashEscapes = noBackslashEscapes;
+        return snapshot(lexer);
+    }
+
+    private static List<String> snapshot(DorisLexer lexer) {
+        ErrorCollector errors = new ErrorCollector();
+        lexer.removeErrorListeners();
+        lexer.addErrorListener(errors);
+        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
+        tokenStream.fill();
+
+        List<String> snapshot = new ArrayList<>();
+        for (Token token : tokenStream.getTokens()) {
+            snapshot.add(token.getType() + "|" + token.getChannel() + "|" + 
token.getText()
+                    + "|" + token.getStartIndex() + "|" + token.getStopIndex()
+                    + "|" + token.getLine() + "|" + 
token.getCharPositionInLine()
+                    + "|" + token.getTokenIndex());
+        }
+        snapshot.addAll(errors.errors);
+        return snapshot;
+    }
+
+    private static class ErrorCollector extends BaseErrorListener {
+        private final List<String> errors = new ArrayList<>();
+
+        @Override
+        public void syntaxError(Recognizer<?, ?> recognizer, Object 
offendingSymbol, int line,
+                int charPositionInLine, String message, RecognitionException 
exception) {
+            errors.add("ERROR|" + line + "|" + charPositionInLine + "|" + 
message);
+        }
+    }
+
+    private static class ReferenceCaseInsensitiveStream implements CharStream {
+        private final CharStream stream;
+
+        ReferenceCaseInsensitiveStream(CharStream stream) {
+            this.stream = stream;
+        }
+
+        @Override
+        public String getText(Interval interval) {
+            return stream.getText(interval);
+        }
+
+        @Override
+        public void consume() {
+            stream.consume();
+        }
+
+        @Override
+        public int LA(int offset) {
+            int result = stream.LA(offset);
+            switch (result) {
+                case 0:
+                case IntStream.EOF:
+                    return result;
+                default:
+                    return Character.toUpperCase(result);
+            }
+        }
+
+        @Override
+        public int mark() {
+            return stream.mark();
+        }
+
+        @Override
+        public void release(int marker) {
+            stream.release(marker);
+        }
+
+        @Override
+        public int index() {
+            return stream.index();
+        }
+
+        @Override
+        public void seek(int index) {
+            stream.seek(index);
+        }
+
+        @Override
+        public int size() {
+            return stream.size();
+        }
+
+        @Override
+        public String getSourceName() {
+            return stream.getSourceName();
+        }
+    }
+}


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

Reply via email to