From 1db85728f03ce9770a8038cb3a5b7b7f69364764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Thu, 15 Oct 2026 23:13:01 +0800 Subject: [PATCH 1/2] perf: skip redundant lookahead simulations in Condition dispatch and expression lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main branch of Condition/SQLCondition is the last alternative of its choice: entering it and letting the productions reject is equivalent to the deep jj_2 phase simulation that ran for every plain condition first, so gate it with a cheap !interrupted check instead. The named-parameter alternatives of ComplexExpressionList only ever match a single-token name directly followed by their assignment token, so a first check on the second token is equivalent to their 2-token lookahead scan. parseSQLStatements: 21.3-21.4 vs 24.6-24.8 ms/op against master in 3 interleaved JMH pairs (-13% to -14%, error intervals not overlapping); behavior verified identical by 26 EXISTS/named-parameter probes, the full 10,344-test suite (same 35 pre-existing environment failures as master) and a byte-identical AST dump of performance.sql. Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d..20cb82a5a 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1265,6 +1265,17 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Both named-parameter forms match only a single-token name (or K_OUTER) directly followed + * by their assignment token; the 2-token lookahead fails for anything else, so this first + * check is equivalent to running that scan. + */ + private boolean isNamedParameterAssignmentAhead() { + int kind = getToken(2).kind; + return kind == K_ORACLE_NAMED_PARAMETER_ASSIGNMENT + || kind == K_POSTGRES_NAMED_PARAMETER_ASSIGNMENT; + } + /** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */ private boolean isExistsIdentifierAhead() { if (getToken(1).kind != K_EXISTS) { @@ -11048,6 +11059,10 @@ Expression Condition(): LOOKAHEAD(2, { !isExistsIdentifierAhead() }) result=ExistsExpression() | + // last alternative of the choice: entering it and letting the productions + // themselves reject is equivalent to scanning first, so skip the deep + // lookahead simulation (this path runs for every plain condition) + LOOKAHEAD({ !interrupted }) [ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] left=SimpleExpression() { result = left; } @@ -11198,7 +11213,10 @@ Expression SQLCondition(): | LOOKAHEAD(2, { !isExistsIdentifierAhead() }) result=ExistsExpression() - | left = SimpleExpression() { result = left; } + | + // last alternative of the choice: skip the redundant lookahead simulation + LOOKAHEAD({ !interrupted }) + left = SimpleExpression() { result = left; } [ LOOKAHEAD(2) ( LOOKAHEAD(2, ) result=OverlapsCondition(left) @@ -11663,9 +11681,9 @@ ExpressionList ComplexExpressionList(): } { ( - LOOKAHEAD(2) expr=OracleNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter() | - LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter() | expr=Expression() ) @@ -11676,9 +11694,9 @@ ExpressionList ComplexExpressionList(): ( LOOKAHEAD(2, {!interrupted}) "," ( - LOOKAHEAD(2) expr=OracleNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter() | - LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter() | LOOKAHEAD( RelObjectName() "->", { !isJsonArrowDialect() } ) expr=LambdaExpression() | From 68c035bc058d5236ec8d30ca7909816a0a56e091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Fri, 16 Oct 2026 08:47:20 +0800 Subject: [PATCH 2/2] perf: remove redundant single-token lookahead simulations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use exact token predicates for PRIOR, OVERLAPS, time keywords, ALL and boolean literals while retaining interruption checks. Add 52 boundary cases for expression dispatch, named arguments, malformed input and cancellation. Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 12 +- .../parser/LookaheadDispatchTest.java | 223 ++++++++++++++++++ 2 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 20cb82a5a..b21a15f54 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11063,7 +11063,7 @@ Expression Condition(): // themselves reject is equivalent to scanning first, so skip the deep // lookahead simulation (this path runs for every plain condition) LOOKAHEAD({ !interrupted }) - [ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] + [ LOOKAHEAD({ getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] left=SimpleExpression() { result = left; } // Consume Oracle (+) once, before dispatching @@ -11219,7 +11219,7 @@ Expression SQLCondition(): left = SimpleExpression() { result = left; } [ LOOKAHEAD(2) ( - LOOKAHEAD(2, ) result=OverlapsCondition(left) + LOOKAHEAD({ getToken(1).kind == K_OVERLAPS }) result=OverlapsCondition(left) | LOOKAHEAD(3, {!interrupted}) result=InExpression(left) | @@ -12112,7 +12112,8 @@ Expression PrimaryExpression() #PrimaryExpression: //| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns() // support timestamp expressions - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new TimeKeyExpression(token.image); } + | LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TIME_KEY_EXPR || getToken(1).kind == K_CURRENT) }) + (token= | token=) { retval = new TimeKeyExpression(token.image); } | LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression() @@ -12132,7 +12133,7 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, { !interrupted && getToken(1).kind == K_KEY && getToken(2).kind != OPENING_BRACKET }) retval=KeyExpression() - | LOOKAHEAD(2, {!interrupted}) { retval = new AllValue(); } + | LOOKAHEAD({ !interrupted && getToken(1).kind == K_ALL }) { retval = new AllValue(); } | LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() }) retval=CharacterSetIntroducedString() @@ -12146,7 +12147,8 @@ Expression PrimaryExpression() #PrimaryExpression: "(" "+" ")" { ((Column) retval).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); } ] - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new BooleanValue(token.image); } + | LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TRUE || getToken(1).kind == K_FALSE) }) + (token= | token=) { retval = new BooleanValue(token.image); } | retval=CharacterLiteral() { linkAST(retval,jjtThis); } diff --git a/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java b/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java new file mode 100644 index 000000000..0aa5ee61d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java @@ -0,0 +1,223 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AllValue; +import net.sf.jsqlparser.expression.BooleanValue; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.OracleNamedFunctionParameter; +import net.sf.jsqlparser.expression.OverlapsCondition; +import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; +import net.sf.jsqlparser.expression.TimeKeyExpression; +import net.sf.jsqlparser.expression.operators.relational.Between; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; +import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +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.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class LookaheadDispatchTest { + @ParameterizedTest + @MethodSource("singleTokenExpressions") + void preservesSingleTokenExpressionKinds(String sql, Class type, + String deparsed) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + Expression expression = parser.PrimaryExpression(); + assertInstanceOf(type, expression); + assertEquals(deparsed, expression.toString()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + Expression parsed = CCJSqlParserUtil.parseExpression(sql, false); + assertInstanceOf(type, parsed); + assertEquals(deparsed, parsed.toString()); + } + + static Stream singleTokenExpressions() { + return Stream.of( + Arguments.of("TRUE", BooleanValue.class, "true"), + Arguments.of("FALSE", BooleanValue.class, "false"), + Arguments.of("ALL", AllValue.class, "ALL"), + Arguments.of("CURRENT", TimeKeyExpression.class, "CURRENT"), + Arguments.of("CURRENT_DATE", TimeKeyExpression.class, "CURRENT_DATE"), + Arguments.of("CURRENT_TIME", TimeKeyExpression.class, "CURRENT_TIME"), + Arguments.of("CURRENT_TIMESTAMP", TimeKeyExpression.class, "CURRENT_TIMESTAMP"), + Arguments.of("CURRENT_TIMEZONE", TimeKeyExpression.class, "CURRENT_TIMEZONE"), + Arguments.of("CURRENT_DATE()", TimeKeyExpression.class, "CURRENT_DATE()")); + } + + @ParameterizedTest + @ValueSource(strings = {"\"TRUE\"", "\"CURRENT_DATE\"", "\"ALL\""}) + void preservesQuotedNames(String name) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT " + name); + assertEquals(name, assertInstanceOf(Column.class, select.getSelectItem(0).getExpression()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"TRUE", "ALL", "CURRENT_DATE"}) + void honorsInterruptionBeforeSingleTokenExpressions(String sql) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + parser.interrupted = true; + assertThrows(ParseException.class, parser::PrimaryExpression); + assertEquals(sql, parser.getNextToken().image); + } + + @ParameterizedTest + @MethodSource("conditions") + void preservesConditionAlternatives(String sql, Class type) + throws Exception { + CCJSqlParser conditionParser = CCJSqlParserUtil.newParser(sql); + Expression condition = conditionParser.Condition(); + assertInstanceOf(type, condition); + assertEquals(sql, condition.toString()); + assertEquals(CCJSqlParserConstants.EOF, conditionParser.getNextToken().kind); + + CCJSqlParser sqlConditionParser = CCJSqlParserUtil.newParser(sql); + Expression sqlCondition = sqlConditionParser.SQLCondition(); + assertInstanceOf(type, sqlCondition); + assertEquals(sql, sqlCondition.toString()); + assertEquals(CCJSqlParserConstants.EOF, sqlConditionParser.getNextToken().kind); + + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE " + sql); + assertInstanceOf(type, select.getWhere()); + } + + static Stream conditions() { + return Stream.of( + Arguments.of("a", Column.class), + Arguments.of("exists", Column.class), + Arguments.of("exists.col", Column.class), + Arguments.of("EXISTS (SELECT 1)", ExistsExpression.class), + Arguments.of("EXISTS (1)", ExistsExpression.class), + Arguments.of("a IN (1, 2)", InExpression.class), + Arguments.of("a BETWEEN 1 AND 2", Between.class), + Arguments.of("a IS NULL", IsNullExpression.class), + Arguments.of("(a, b) OVERLAPS (c, d)", OverlapsCondition.class)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "exists", "exists.col"}) + void preservesComparisons(String column) throws Exception { + String sql = column + " = 1"; + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition()); + assertEquals(column, assertInstanceOf(Column.class, condition.getLeftExpression()) + .getFullyQualifiedName()); + assertEquals(sql, condition.toString()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + sql); + } + + @Test + void preservesOraclePriorPosition() throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser("PRIOR a = b"); + EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition()); + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + condition.getOraclePriorPosition()); + assertEquals("a", assertInstanceOf(Column.class, condition.getLeftExpression()) + .getColumnName()); + assertEquals("b", assertInstanceOf(Column.class, condition.getRightExpression()) + .getColumnName()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t CONNECT BY PRIOR a = b"); + } + + @ParameterizedTest + @ValueSource(strings = {"a IN (", "a BETWEEN 1 AND", "EXISTS (SELECT", + "(a, b) OVERLAPS ("}) + void rejectsMalformedConditions(String sql) { + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition()); + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).SQLCondition()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"a =", "exists ="}) + void rejectsMalformedComparisons(String sql) { + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void honorsInterruptionBeforeConditionDispatch(boolean sqlCondition) { + CCJSqlParser parser = CCJSqlParserUtil.newParser("1"); + parser.interrupted = true; + assertThrows(ParseException.class, () -> { + if (sqlCondition) { + parser.SQLCondition(); + } else { + parser.Condition(); + } + }); + assertEquals("1", parser.getNextToken().image); + } + + @ParameterizedTest + @CsvSource(value = {"arg|=>", "arg|:=", "OUTER|=>", "OUTER|:=", "\"arg\"|=>", + "\"arg\"|:=", "overlaps|=>", "overlaps|:="}, delimiter = '|') + void preservesNamedParameterKindsInEveryArgumentPosition(String name, String assignment) + throws JSQLParserException { + for (String prefix : new String[] {"", "a.b, "}) { + String sql = "SELECT f(" + prefix + name + " " + assignment + " value_col)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql); + Function function = assertInstanceOf(Function.class, + select.getSelectItem(0).getExpression()); + int index = prefix.isEmpty() ? 0 : 1; + assertEquals(index + 1, function.getParameters().size()); + if (index == 1) { + assertEquals("a.b", assertInstanceOf(Column.class, + function.getParameters().get(0)).getFullyQualifiedName()); + } + Expression argument = function.getParameters().get(index); + Expression value; + if ("=>".equals(assignment)) { + OracleNamedFunctionParameter parameter = + assertInstanceOf(OracleNamedFunctionParameter.class, argument); + assertEquals(name, parameter.getName()); + value = parameter.getExpression(); + } else { + PostgresNamedFunctionParameter parameter = + assertInstanceOf(PostgresNamedFunctionParameter.class, argument); + assertEquals(name, parameter.getName()); + value = parameter.getExpression(); + } + assertEquals("value_col", assertInstanceOf(Column.class, value).getColumnName()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"a.b => 1", "a.b := 1", "1 => 1", "1 := 1", "'a' => 1", + "'a' := 1", "arg =>", "arg :="}) + void rejectsInvalidNamedParametersInEveryArgumentPosition(String argument) { + for (String prefix : new String[] {"", "a.b, "}) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT f(" + prefix + argument + ")")); + } + } +}