This is an automated email from the ASF dual-hosted git repository.
CalvinKirs 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 5e83a922771 [improvement](parser) Optimize string literal lexing
(#66902)
5e83a922771 is described below
commit 5e83a92277132f1c4533ec14b2752d160e282403
Author: morrySnow <[email protected]>
AuthorDate: Thu Aug 20 19:18:05 2026 +0800
[improvement](parser) Optimize string literal lexing (#66902)
### What problem does this PR solve?
`DorisLexer.STRING_LITERAL` evaluated SQL-mode semantic predicates for
every character, so predicate calls and allocation scaled with the
literal payload. This PR selects the SQL-mode-specific loop once after
the opening quote while preserving the accepted language and token
stream.
It also adds:
- an isolated, opt-in JMH module that does not affect the default parser
jar or its runtime dependencies;
- lexer-only, parser end-to-end, allocation, and no-string control
benchmarks;
- deterministic token/error differential tooling;
- string literal boundary, long-input, predicate-complexity, and
8-thread concurrency tests;
- a gated TODO for subsequent parser optimization work.
Representative plain single-quoted measurements:
- 4 KiB lexer: about 1.40–1.74 ms/op to 0.029–0.059 ms/op;
- 64 KiB lexer: about 21.6–24.4 ms/op to 0.55–0.70 ms/op;
- 4–64 KiB allocation reduction: about 99.6%–99.7%.
The benchmark host did not meet the strict 3% A/A stability gate for
microsecond control cases. The PR therefore reports raw ranges and
explicitly leaves fine-grained G2/G5 validation open instead of claiming
small control-path changes.
### Release note
None
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- [x] Manual test
- `./run-fe-ut.sh --run
org.apache.doris.sqlparser.DorisLexerStringLiteralTest,org.apache.doris.sqlparser.DorisSqlParserTest`
- `DISABLE_BUILD_UI=ON ./build.sh --fe`
- 46,908-case baseline/candidate token and error snapshot differential:
zero differences
- JMH lexer-only, parser end-to-end, allocation, and no-string control
benchmarks
- [ ] No need to test or manual test.
- Behavior changed:
- [x] No.
- [ ] Yes.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
fe/fe-sql-parser-benchmark/pom.xml | 94 +++++++++
.../benchmark/ParserControlBenchmark.java | 52 +++++
.../benchmark/StringLiteralBenchmark.java | 112 ++++++++++
.../benchmark/StringLiteralTokenCorpus.java | 122 +++++++++++
fe/fe-sql-parser/README.md | 13 ++
.../antlr4/org/apache/doris/nereids/DorisLexer.g4 | 10 +-
.../sqlparser/DorisLexerStringLiteralTest.java | 234 +++++++++++++++++++++
fe/pom.xml | 7 +
8 files changed, 642 insertions(+), 2 deletions(-)
diff --git a/fe/fe-sql-parser-benchmark/pom.xml
b/fe/fe-sql-parser-benchmark/pom.xml
new file mode 100644
index 00000000000..a4a579c7ce2
--- /dev/null
+++ b/fe/fe-sql-parser-benchmark/pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.doris</groupId>
+ <version>${revision}</version>
+ <artifactId>fe</artifactId>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>fe-sql-parser-benchmark</artifactId>
+ <packaging>jar</packaging>
+ <name>Doris FE SQL Parser Benchmarks</name>
+
+ <properties>
+ <jmh.version>1.37</jmh.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>fe-sql-parser</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.openjdk.jmh</groupId>
+ <artifactId>jmh-core</artifactId>
+ <version>${jmh.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.openjdk.jmh</groupId>
+ <artifactId>jmh-generator-annprocess</artifactId>
+ <version>${jmh.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>doris-fe-sql-parser-benchmarks</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>build-benchmark-jar</id>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+
<createDependencyReducedPom>false</createDependencyReducedPom>
+ <transformers>
+ <transformer
+
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>org.openjdk.jmh.Main</mainClass>
+ </transformer>
+ </transformers>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ <exclude>module-info.class</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/ParserControlBenchmark.java
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/ParserControlBenchmark.java
new file mode 100644
index 00000000000..0002f43147a
--- /dev/null
+++
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/ParserControlBenchmark.java
@@ -0,0 +1,52 @@
+// 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.sqlparser.DorisSqlParser;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+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.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+/** Guards against fixed-cost parser regressions on SQL without string
literals. */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Fork(3)
+@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 8, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+public class ParserControlBenchmark {
+ @Param({"SELECT 1", "SELECT a FROM t WHERE a > 1"})
+ public String sql;
+
+ private final DorisSqlParser parser = new DorisSqlParser();
+
+ @Benchmark
+ public Object parseStatementWithoutStringLiteral() {
+ return parser.parseStatement(sql);
+ }
+}
diff --git
a/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/StringLiteralBenchmark.java
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/StringLiteralBenchmark.java
new file mode 100644
index 00000000000..cfceace6441
--- /dev/null
+++
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/StringLiteralBenchmark.java
@@ -0,0 +1,112 @@
+// 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.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.concurrent.TimeUnit;
+
+/** Benchmarks the lexer hotspot and its impact on the public end-to-end
parser entry point. */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Fork(3)
+@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 8, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+public class StringLiteralBenchmark {
+ @Param({"false", "true"})
+ public boolean noBackslashEscapes;
+
+ @Param({"plain", "backslash", "doubledQuote", "mixed"})
+ public String pattern;
+
+ @Param({"single", "double"})
+ public String quote;
+
+ @Param({"16", "256", "4096", "65536"})
+ public int payloadLength;
+
+ private DorisSqlParser parser;
+ private String literal;
+ private String statement;
+
+ @Setup(Level.Trial)
+ public void setUp() {
+ parser = new DorisSqlParser(noBackslashEscapes, false);
+ String unit;
+ switch (pattern) {
+ case "plain":
+ unit = "a";
+ break;
+ case "backslash":
+ unit = "\\n";
+ break;
+ case "doubledQuote":
+ unit = quote.equals("single") ? "''" : "\"\"";
+ break;
+ case "mixed":
+ unit = quote.equals("single") ? "abc\\n''" : "abc\\n\"\"";
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown pattern: " +
pattern);
+ }
+
+ StringBuilder payload = new StringBuilder(payloadLength);
+ while (payload.length() + unit.length() <= payloadLength) {
+ payload.append(unit);
+ }
+ payload.append("a".repeat(payloadLength - payload.length()));
+ String quoteCharacter = quote.equals("single") ? "'" : "\"";
+ literal = quoteCharacter + payload + quoteCharacter;
+ statement = "SELECT " + literal;
+ }
+
+ @Benchmark
+ public int lexStringLiteral() {
+ DorisLexer lexer = parser.newLexer(literal);
+ 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 parseSelectStringLiteral() {
+ return parser.parseStatement(statement);
+ }
+}
diff --git
a/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/StringLiteralTokenCorpus.java
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/StringLiteralTokenCorpus.java
new file mode 100644
index 00000000000..832f2739066
--- /dev/null
+++
b/fe/fe-sql-parser-benchmark/src/main/java/org/apache/doris/sqlparser/benchmark/StringLiteralTokenCorpus.java
@@ -0,0 +1,122 @@
+// 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.BaseErrorListener;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.Token;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+/** Emits a deterministic token snapshot for baseline/candidate differential
testing. */
+public final class StringLiteralTokenCorpus {
+ private static final String[] ALPHABET = {"a", "'", "\"", "\\", "\n"};
+
+ private StringLiteralTokenCorpus() {
+ }
+
+ public static void main(String[] args) throws IOException {
+ List<String> payloads = new ArrayList<>();
+ for (int length = 0; length <= 5; length++) {
+ enumeratePayloads(new StringBuilder(), length, payloads);
+ }
+ payloads.add("中文😀");
+ payloads.add("\\\r\n");
+ payloads.add("'\"\\\n中😀");
+
+ try (PrintWriter output = new PrintWriter(Files.newBufferedWriter(
+ Path.of(args[0]), StandardCharsets.UTF_8))) {
+ int caseId = 0;
+ for (boolean noBackslashEscapes : new boolean[] {false, true}) {
+ for (String quote : new String[] {"'", "\""}) {
+ for (String payload : payloads) {
+ emit(output, caseId++, noBackslashEscapes, quote +
payload + quote);
+ emit(output, caseId++, noBackslashEscapes, quote +
payload);
+ emit(output, caseId++, noBackslashEscapes,
+ quote + payload + quote + " next /* comment
*/");
+ }
+ }
+ }
+ }
+ }
+
+ private static void enumeratePayloads(StringBuilder payload, int
remaining, List<String> output) {
+ if (remaining == 0) {
+ output.add(payload.toString());
+ return;
+ }
+ for (String character : ALPHABET) {
+ int start = payload.length();
+ payload.append(character);
+ enumeratePayloads(payload, remaining - 1, output);
+ payload.setLength(start);
+ }
+ }
+
+ private static void emit(PrintWriter output, int caseId, boolean
noBackslashEscapes, String sql) {
+ DorisLexer lexer = new DorisSqlParser(noBackslashEscapes,
false).newLexer(sql);
+ ErrorCollector errors = new ErrorCollector();
+ lexer.removeErrorListeners();
+ lexer.addErrorListener(errors);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ tokens.fill();
+
+ StringBuilder snapshot = new StringBuilder();
+
snapshot.append(caseId).append('|').append(noBackslashEscapes).append('|').append(encode(sql));
+ for (Token token : tokens.getTokens()) {
+
snapshot.append('|').append(DorisLexer.VOCABULARY.getSymbolicName(token.getType()))
+ .append(',').append(token.getChannel())
+ .append(',').append(token.getStartIndex())
+ .append(',').append(token.getStopIndex())
+ .append(',').append(token.getLine())
+ .append(',').append(token.getCharPositionInLine())
+ .append(',').append(token.getTokenIndex())
+ .append(',').append(encode(token.getText()));
+ }
+ for (String error : errors.errors) {
+ snapshot.append("|ERROR,").append(error);
+ }
+ output.println(snapshot);
+ }
+
+ private static String encode(String value) {
+ return
Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
+ }
+
+ 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(line + "," + charPositionInLine + "," +
encode(message));
+ }
+ }
+}
diff --git a/fe/fe-sql-parser/README.md b/fe/fe-sql-parser/README.md
index 3c39aed3db3..73d2d509d46 100644
--- a/fe/fe-sql-parser/README.md
+++ b/fe/fe-sql-parser/README.md
@@ -58,6 +58,19 @@ mvn -pl fe-sql-parser -am package
Output: `fe/fe-sql-parser/target/doris-fe-sql-parser.jar` (~1.3 MB). This jar
contains only the parser classes; it expects `org.antlr:antlr4-runtime:4.13.1`
to be provided by the consuming project's classpath.
+### Parser microbenchmarks
+
+The optional `benchmark` profile builds a self-contained JMH jar without
adding JMH to the default parser jar or its runtime dependencies:
+
+```bash
+# From the fe/ directory
+mvn -Pbenchmark -pl fe-sql-parser-benchmark -am package -DskipTests
+java -jar fe-sql-parser-benchmark/target/doris-fe-sql-parser-benchmarks.jar \
+ '.*StringLiteralBenchmark.*' -prof gc -rf json -rff
/tmp/string-literal-benchmark.json
+```
+
+Use the same JDK, corpus parameters, JMH arguments, and machine state for
baseline and candidate runs. Run the same baseline artifact twice before
comparing a change; the raw JSON and artifact hash should be retained with the
result summary.
+
To install it to your local Maven repository so other projects can resolve it:
```bash
diff --git
a/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
b/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
index 44668589229..49b6767297d 100644
--- a/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
+++ b/fe/fe-sql-parser/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
@@ -665,8 +665,14 @@ ATSIGN: '@';
DOUBLEATSIGN: '@@';
STRING_LITERAL
- : '\'' ( {!isNoBackslashEscapes}? '\\'. | '\'\'' |
{!isNoBackslashEscapes}? ~('\'' | '\\') | {isNoBackslashEscapes}? ~('\''))* '\''
- | '"' ( {!isNoBackslashEscapes}? '\\'. | '""' | {!isNoBackslashEscapes}?
~('"'| '\\') | {isNoBackslashEscapes}? ~('"'))* '"'
+ : '\'' (
+ {!isNoBackslashEscapes}? ('\\' . | '\'\'' | ~('\'' | '\\'))*
+ | {isNoBackslashEscapes}? ('\'\'' | ~('\''))*
+ ) '\''
+ | '"' (
+ {!isNoBackslashEscapes}? ('\\' . | '""' | ~('"' | '\\'))*
+ | {isNoBackslashEscapes}? ('""' | ~('"'))*
+ ) '"'
;
VARBINARY_LITERAL
diff --git
a/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/DorisLexerStringLiteralTest.java
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/DorisLexerStringLiteralTest.java
new file mode 100644
index 00000000000..b244240fc4a
--- /dev/null
+++
b/fe/fe-sql-parser/src/test/java/org/apache/doris/sqlparser/DorisLexerStringLiteralTest.java
@@ -0,0 +1,234 @@
+// 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.CharStreams;
+import org.antlr.v4.runtime.RuleContext;
+import org.antlr.v4.runtime.Token;
+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.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+class DorisLexerStringLiteralTest {
+ @Test
+ void lexesValidStringsAsOneTokenInBothSqlModes() {
+ List<String> literals = Arrays.asList(
+ singleQuoted(""),
+ doubleQuoted(""),
+ singleQuoted("plain ASCII"),
+ doubleQuoted("plain ASCII"),
+ singleQuoted("中文😀"),
+ doubleQuoted("中文😀"),
+ singleQuoted("it''s"),
+ doubleQuoted("a\"\"b"),
+ singleQuoted("line\nbreak"),
+ singleQuoted("a\\b"),
+ singleQuoted("a\\\\"));
+
+ for (boolean noBackslashEscapes : new boolean[] {false, true}) {
+ for (String literal : literals) {
+ assertSingleStringToken(literal, noBackslashEscapes);
+ }
+ }
+
+ assertSingleStringToken(singleQuoted("a\\'b"), false);
+ assertSingleStringToken(doubleQuoted("a\\\"b"), false);
+ assertSingleStringToken(singleQuoted("a\\"), true);
+ assertSingleStringToken(doubleQuoted("a\\"), true);
+ }
+
+ @Test
+ void preservesModeSensitiveTokenBoundariesAndPositions() {
+ String escapedQuote = singleQuoted("a\\'b");
+ Assertions.assertEquals(Arrays.asList(
+ "STRING_LITERAL|'a\\'b'|0|0|5|1|0",
+ "EOF|<EOF>|0|6|5|1|6"),
+ snapshot(escapedQuote, false));
+ Assertions.assertEquals(Arrays.asList(
+ "STRING_LITERAL|'a\\'|0|0|3|1|0",
+ "IDENTIFIER|b|0|4|4|1|4",
+ "UNRECOGNIZED|'|0|5|5|1|5",
+ "EOF|<EOF>|0|6|5|1|6"),
+ snapshot(escapedQuote, true));
+
+ String trailingBackslash = singleQuoted("a\\");
+ Assertions.assertEquals(Arrays.asList(
+ "UNRECOGNIZED|'|0|0|0|1|0",
+ "IDENTIFIER|a|0|1|1|1|1",
+ "UNRECOGNIZED|\\|0|2|2|1|2",
+ "UNRECOGNIZED|'|0|3|3|1|3",
+ "EOF|<EOF>|0|4|3|1|4"),
+ snapshot(trailingBackslash, false));
+ Assertions.assertEquals(Arrays.asList(
+ "STRING_LITERAL|'a\\'|0|0|3|1|0",
+ "EOF|<EOF>|0|4|3|1|4"),
+ snapshot(trailingBackslash, true));
+ }
+
+ @Test
+ void lexesLongStringsWithoutChangingTheirSourceInterval() {
+ for (int length : new int[] {0, 1, 16, 256, 4096, 65536}) {
+ for (boolean noBackslashEscapes : new boolean[] {false, true}) {
+ assertSingleStringToken(singleQuoted("a".repeat(length)),
noBackslashEscapes);
+ assertSingleStringToken(singleQuoted("''".repeat(length)),
noBackslashEscapes);
+ assertSingleStringToken(singleQuoted("\\n".repeat(length)),
noBackslashEscapes);
+ }
+ }
+ }
+
+ @Test
+ void semanticPredicateCallsDoNotScaleWithStringLength() {
+ int shortCount = predicateCalls(singleQuoted("a".repeat(16)), false);
+ int longCount = predicateCalls(singleQuoted("a".repeat(4096)), false);
+ int noBackslashShortCount =
predicateCalls(singleQuoted("a".repeat(16)), true);
+ int noBackslashLongCount =
predicateCalls(singleQuoted("a".repeat(4096)), true);
+
+ Assertions.assertTrue(longCount <= shortCount + 2,
+ () -> "default mode predicate calls scaled from " + shortCount
+ " to " + longCount);
+ Assertions.assertTrue(noBackslashLongCount <= noBackslashShortCount +
2,
+ () -> "NO_BACKSLASH_ESCAPES predicate calls scaled from "
+ + noBackslashShortCount + " to " +
noBackslashLongCount);
+ }
+
+ @Test
+ void lexesDeterministicallyWithSharedStaticDfa() throws Exception {
+ List<String> inputs = Arrays.asList(
+ singleQuoted("plain"),
+ doubleQuoted("中文😀"),
+ singleQuoted("a\\'b"),
+ doubleQuoted("a\\\"b"),
+ singleQuoted("''\\n"),
+ doubleQuoted("\"\"\\n"),
+ "'unterminated",
+ "\"unterminated");
+ List<List<String>> expected = new ArrayList<>();
+ for (boolean noBackslashEscapes : new boolean[] {false, true}) {
+ for (String input : inputs) {
+ expected.add(snapshot(input, noBackslashEscapes));
+ }
+ }
+
+ ExecutorService executor = Executors.newFixedThreadPool(8);
+ try {
+ List<Callable<Void>> tasks = new ArrayList<>();
+ for (int thread = 0; thread < 8; thread++) {
+ tasks.add(() -> {
+ for (int repetition = 0; repetition < 100; repetition++) {
+ int caseIndex = 0;
+ for (boolean noBackslashEscapes : new boolean[]
{false, true}) {
+ for (String input : inputs) {
+
Assertions.assertEquals(expected.get(caseIndex++),
+ snapshot(input, noBackslashEscapes));
+ }
+ }
+ }
+ return null;
+ });
+ }
+ for (Future<Void> result : executor.invokeAll(tasks)) {
+ result.get();
+ }
+ } finally {
+ executor.shutdownNow();
+ Assertions.assertTrue(executor.awaitTermination(10,
TimeUnit.SECONDS));
+ }
+ }
+
+ private static int predicateCalls(String sql, boolean noBackslashEscapes) {
+ CountingLexer lexer = new CountingLexer(sql);
+ lexer.isNoBackslashEscapes = noBackslashEscapes;
+ while (lexer.nextToken().getType() != Token.EOF) {
+ // Consume all tokens.
+ }
+ return lexer.predicateCalls;
+ }
+
+ private static void assertSingleStringToken(String literal, boolean
noBackslashEscapes) {
+ List<Token> tokens = lex(literal, noBackslashEscapes);
+ Assertions.assertEquals(2, tokens.size(), () -> snapshot(literal,
noBackslashEscapes).toString());
+ int codePointLength = literal.codePointCount(0, literal.length());
+ Token string = tokens.get(0);
+ Assertions.assertEquals(DorisLexer.STRING_LITERAL, string.getType());
+ Assertions.assertEquals(literal, string.getText());
+ Assertions.assertEquals(Token.DEFAULT_CHANNEL, string.getChannel());
+ Assertions.assertEquals(0, string.getStartIndex());
+ Assertions.assertEquals(codePointLength - 1, string.getStopIndex());
+ Assertions.assertEquals(1, string.getLine());
+ Assertions.assertEquals(0, string.getCharPositionInLine());
+
+ Token eof = tokens.get(1);
+ Assertions.assertEquals(Token.EOF, eof.getType());
+ Assertions.assertEquals(codePointLength, eof.getStartIndex());
+ Assertions.assertEquals(codePointLength - 1, eof.getStopIndex());
+ }
+
+ private static List<String> snapshot(String sql, boolean
noBackslashEscapes) {
+ List<String> snapshot = new ArrayList<>();
+ for (Token token : lex(sql, noBackslashEscapes)) {
+ String type =
DorisLexer.VOCABULARY.getSymbolicName(token.getType());
+ snapshot.add(type + "|" + token.getText().replace("\n", "\\n") +
"|"
+ + token.getChannel() + "|" + token.getStartIndex() + "|" +
token.getStopIndex()
+ + "|" + token.getLine() + "|" +
token.getCharPositionInLine());
+ }
+ return snapshot;
+ }
+
+ private static List<Token> lex(String sql, boolean noBackslashEscapes) {
+ DorisLexer lexer = new DorisSqlParser(noBackslashEscapes,
false).newLexer(sql);
+ List<Token> tokens = new ArrayList<>();
+ Token token;
+ do {
+ token = lexer.nextToken();
+ tokens.add(token);
+ } while (token.getType() != Token.EOF);
+ return tokens;
+ }
+
+ private static String singleQuoted(String payload) {
+ return "'" + payload + "'";
+ }
+
+ private static String doubleQuoted(String payload) {
+ return "\"" + payload + "\"";
+ }
+
+ private static class CountingLexer extends DorisLexer {
+ private int predicateCalls;
+
+ CountingLexer(String sql) {
+ super(new CaseInsensitiveStream(CharStreams.fromString(sql)));
+ }
+
+ @Override
+ public boolean sempred(RuleContext localctx, int ruleIndex, int
predIndex) {
+ predicateCalls++;
+ return super.sempred(localctx, ruleIndex, predIndex);
+ }
+ }
+}
diff --git a/fe/pom.xml b/fe/pom.xml
index fa1ef0a650a..ba8c67b4b3c 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -440,6 +440,13 @@ under the License.
<fastutil.version>8.5.18</fastutil.version>
</properties>
<profiles>
+ <profile>
+ <!-- Keep benchmarks out of the default FE reactor and all
published runtime jars. -->
+ <id>benchmark</id>
+ <modules>
+ <module>fe-sql-parser-benchmark</module>
+ </modules>
+ </profile>
<profile>
<id>general-env</id>
<activation>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]