Skip to content

Commit dabc8b1

Browse files
authored
perf: skip redundant lookahead simulations in expression dispatch (#2827)
* perf: skip redundant lookahead simulations in Condition dispatch and expression lists 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: 付典 <fudianchn@gmail.com> * perf: remove redundant single-token lookahead simulations 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: 付典 <fudianchn@gmail.com> --------- Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 9db0ee1 commit dabc8b1

2 files changed

Lines changed: 253 additions & 10 deletions

File tree

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12651265
}
12661266
}
12671267

1268+
/**
1269+
* Both named-parameter forms match only a single-token name (or K_OUTER) directly followed
1270+
* by their assignment token; the 2-token lookahead fails for anything else, so this first
1271+
* check is equivalent to running that scan.
1272+
*/
1273+
private boolean isNamedParameterAssignmentAhead() {
1274+
int kind = getToken(2).kind;
1275+
return kind == K_ORACLE_NAMED_PARAMETER_ASSIGNMENT
1276+
|| kind == K_POSTGRES_NAMED_PARAMETER_ASSIGNMENT;
1277+
}
1278+
12681279
/** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */
12691280
private boolean isExistsIdentifierAhead() {
12701281
if (getToken(1).kind != K_EXISTS) {
@@ -11048,7 +11059,11 @@ Expression Condition():
1104811059
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
1104911060
result=ExistsExpression()
1105011061
|
11051-
[ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
11062+
// last alternative of the choice: entering it and letting the productions
11063+
// themselves reject is equivalent to scanning first, so skip the deep
11064+
// lookahead simulation (this path runs for every plain condition)
11065+
LOOKAHEAD({ !interrupted })
11066+
[ LOOKAHEAD({ getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
1105211067
left=SimpleExpression() { result = left; }
1105311068

1105411069
// Consume Oracle (+) once, before dispatching
@@ -11198,10 +11213,13 @@ Expression SQLCondition():
1119811213
|
1119911214
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
1120011215
result=ExistsExpression()
11201-
| left = SimpleExpression() { result = left; }
11216+
|
11217+
// last alternative of the choice: skip the redundant lookahead simulation
11218+
LOOKAHEAD({ !interrupted })
11219+
left = SimpleExpression() { result = left; }
1120211220
[
1120311221
LOOKAHEAD(2) (
11204-
LOOKAHEAD(2, <K_OVERLAPS>) result=OverlapsCondition(left)
11222+
LOOKAHEAD({ getToken(1).kind == K_OVERLAPS }) result=OverlapsCondition(left)
1120511223
|
1120611224
LOOKAHEAD(3, {!interrupted}) result=InExpression(left)
1120711225
|
@@ -11663,9 +11681,9 @@ ExpressionList ComplexExpressionList():
1166311681
}
1166411682
{
1166511683
(
11666-
LOOKAHEAD(2) expr=OracleNamedFunctionParameter()
11684+
LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter()
1166711685
|
11668-
LOOKAHEAD(2) expr=PostgresNamedFunctionParameter()
11686+
LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter()
1166911687
|
1167011688
expr=Expression()
1167111689
)
@@ -11676,9 +11694,9 @@ ExpressionList ComplexExpressionList():
1167611694
(
1167711695
LOOKAHEAD(2, {!interrupted}) ","
1167811696
(
11679-
LOOKAHEAD(2) expr=OracleNamedFunctionParameter()
11697+
LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter()
1168011698
|
11681-
LOOKAHEAD(2) expr=PostgresNamedFunctionParameter()
11699+
LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter()
1168211700
|
1168311701
LOOKAHEAD( RelObjectName() "->", { !isJsonArrowDialect() } ) expr=LambdaExpression()
1168411702
|
@@ -12094,7 +12112,8 @@ Expression PrimaryExpression() #PrimaryExpression:
1209412112
//| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns()
1209512113

1209612114
// support timestamp expressions
12097-
| LOOKAHEAD(2, {!interrupted}) (token=<K_TIME_KEY_EXPR> | token=<K_CURRENT>) { retval = new TimeKeyExpression(token.image); }
12115+
| LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TIME_KEY_EXPR || getToken(1).kind == K_CURRENT) })
12116+
(token=<K_TIME_KEY_EXPR> | token=<K_CURRENT>) { retval = new TimeKeyExpression(token.image); }
1209812117

1209912118
| LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression()
1210012119

@@ -12114,7 +12133,7 @@ Expression PrimaryExpression() #PrimaryExpression:
1211412133

1211512134
| LOOKAHEAD(2, { !interrupted && getToken(1).kind == K_KEY && getToken(2).kind != OPENING_BRACKET }) retval=KeyExpression()
1211612135

12117-
| LOOKAHEAD(2, {!interrupted}) <K_ALL> { retval = new AllValue(); }
12136+
| LOOKAHEAD({ !interrupted && getToken(1).kind == K_ALL }) <K_ALL> { retval = new AllValue(); }
1211812137

1211912138
| LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() })
1212012139
retval=CharacterSetIntroducedString()
@@ -12128,7 +12147,8 @@ Expression PrimaryExpression() #PrimaryExpression:
1212812147
"(" "+" ")" { ((Column) retval).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); }
1212912148
]
1213012149

12131-
| LOOKAHEAD(2, {!interrupted}) (token=<K_TRUE> | token=<K_FALSE>) { retval = new BooleanValue(token.image); }
12150+
| LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TRUE || getToken(1).kind == K_FALSE) })
12151+
(token=<K_TRUE> | token=<K_FALSE>) { retval = new BooleanValue(token.image); }
1213212152

1213312153
| retval=CharacterLiteral() { linkAST(retval,jjtThis); }
1213412154

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
17+
import java.util.stream.Stream;
18+
import net.sf.jsqlparser.JSQLParserException;
19+
import net.sf.jsqlparser.expression.AllValue;
20+
import net.sf.jsqlparser.expression.BooleanValue;
21+
import net.sf.jsqlparser.expression.Expression;
22+
import net.sf.jsqlparser.expression.Function;
23+
import net.sf.jsqlparser.expression.OracleNamedFunctionParameter;
24+
import net.sf.jsqlparser.expression.OverlapsCondition;
25+
import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter;
26+
import net.sf.jsqlparser.expression.TimeKeyExpression;
27+
import net.sf.jsqlparser.expression.operators.relational.Between;
28+
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
29+
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
30+
import net.sf.jsqlparser.expression.operators.relational.InExpression;
31+
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
32+
import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax;
33+
import net.sf.jsqlparser.schema.Column;
34+
import net.sf.jsqlparser.statement.select.PlainSelect;
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.params.ParameterizedTest;
37+
import org.junit.jupiter.params.provider.Arguments;
38+
import org.junit.jupiter.params.provider.CsvSource;
39+
import org.junit.jupiter.params.provider.MethodSource;
40+
import org.junit.jupiter.params.provider.ValueSource;
41+
42+
class LookaheadDispatchTest {
43+
@ParameterizedTest
44+
@MethodSource("singleTokenExpressions")
45+
void preservesSingleTokenExpressionKinds(String sql, Class<? extends Expression> type,
46+
String deparsed) throws Exception {
47+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
48+
Expression expression = parser.PrimaryExpression();
49+
assertInstanceOf(type, expression);
50+
assertEquals(deparsed, expression.toString());
51+
assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind);
52+
Expression parsed = CCJSqlParserUtil.parseExpression(sql, false);
53+
assertInstanceOf(type, parsed);
54+
assertEquals(deparsed, parsed.toString());
55+
}
56+
57+
static Stream<Arguments> singleTokenExpressions() {
58+
return Stream.of(
59+
Arguments.of("TRUE", BooleanValue.class, "true"),
60+
Arguments.of("FALSE", BooleanValue.class, "false"),
61+
Arguments.of("ALL", AllValue.class, "ALL"),
62+
Arguments.of("CURRENT", TimeKeyExpression.class, "CURRENT"),
63+
Arguments.of("CURRENT_DATE", TimeKeyExpression.class, "CURRENT_DATE"),
64+
Arguments.of("CURRENT_TIME", TimeKeyExpression.class, "CURRENT_TIME"),
65+
Arguments.of("CURRENT_TIMESTAMP", TimeKeyExpression.class, "CURRENT_TIMESTAMP"),
66+
Arguments.of("CURRENT_TIMEZONE", TimeKeyExpression.class, "CURRENT_TIMEZONE"),
67+
Arguments.of("CURRENT_DATE()", TimeKeyExpression.class, "CURRENT_DATE()"));
68+
}
69+
70+
@ParameterizedTest
71+
@ValueSource(strings = {"\"TRUE\"", "\"CURRENT_DATE\"", "\"ALL\""})
72+
void preservesQuotedNames(String name) throws JSQLParserException {
73+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT " + name);
74+
assertEquals(name, assertInstanceOf(Column.class, select.getSelectItem(0).getExpression())
75+
.getColumnName());
76+
}
77+
78+
@ParameterizedTest
79+
@ValueSource(strings = {"TRUE", "ALL", "CURRENT_DATE"})
80+
void honorsInterruptionBeforeSingleTokenExpressions(String sql) {
81+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
82+
parser.interrupted = true;
83+
assertThrows(ParseException.class, parser::PrimaryExpression);
84+
assertEquals(sql, parser.getNextToken().image);
85+
}
86+
87+
@ParameterizedTest
88+
@MethodSource("conditions")
89+
void preservesConditionAlternatives(String sql, Class<? extends Expression> type)
90+
throws Exception {
91+
CCJSqlParser conditionParser = CCJSqlParserUtil.newParser(sql);
92+
Expression condition = conditionParser.Condition();
93+
assertInstanceOf(type, condition);
94+
assertEquals(sql, condition.toString());
95+
assertEquals(CCJSqlParserConstants.EOF, conditionParser.getNextToken().kind);
96+
97+
CCJSqlParser sqlConditionParser = CCJSqlParserUtil.newParser(sql);
98+
Expression sqlCondition = sqlConditionParser.SQLCondition();
99+
assertInstanceOf(type, sqlCondition);
100+
assertEquals(sql, sqlCondition.toString());
101+
assertEquals(CCJSqlParserConstants.EOF, sqlConditionParser.getNextToken().kind);
102+
103+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
104+
"SELECT * FROM t WHERE " + sql);
105+
assertInstanceOf(type, select.getWhere());
106+
}
107+
108+
static Stream<Arguments> conditions() {
109+
return Stream.of(
110+
Arguments.of("a", Column.class),
111+
Arguments.of("exists", Column.class),
112+
Arguments.of("exists.col", Column.class),
113+
Arguments.of("EXISTS (SELECT 1)", ExistsExpression.class),
114+
Arguments.of("EXISTS (1)", ExistsExpression.class),
115+
Arguments.of("a IN (1, 2)", InExpression.class),
116+
Arguments.of("a BETWEEN 1 AND 2", Between.class),
117+
Arguments.of("a IS NULL", IsNullExpression.class),
118+
Arguments.of("(a, b) OVERLAPS (c, d)", OverlapsCondition.class));
119+
}
120+
121+
@ParameterizedTest
122+
@ValueSource(strings = {"a", "exists", "exists.col"})
123+
void preservesComparisons(String column) throws Exception {
124+
String sql = column + " = 1";
125+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
126+
EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition());
127+
assertEquals(column, assertInstanceOf(Column.class, condition.getLeftExpression())
128+
.getFullyQualifiedName());
129+
assertEquals(sql, condition.toString());
130+
assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind);
131+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + sql);
132+
}
133+
134+
@Test
135+
void preservesOraclePriorPosition() throws Exception {
136+
CCJSqlParser parser = CCJSqlParserUtil.newParser("PRIOR a = b");
137+
EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition());
138+
assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START,
139+
condition.getOraclePriorPosition());
140+
assertEquals("a", assertInstanceOf(Column.class, condition.getLeftExpression())
141+
.getColumnName());
142+
assertEquals("b", assertInstanceOf(Column.class, condition.getRightExpression())
143+
.getColumnName());
144+
assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind);
145+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM t CONNECT BY PRIOR a = b");
146+
}
147+
148+
@ParameterizedTest
149+
@ValueSource(strings = {"a IN (", "a BETWEEN 1 AND", "EXISTS (SELECT",
150+
"(a, b) OVERLAPS ("})
151+
void rejectsMalformedConditions(String sql) {
152+
assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition());
153+
assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).SQLCondition());
154+
assertThrows(JSQLParserException.class,
155+
() -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql));
156+
}
157+
158+
@ParameterizedTest
159+
@ValueSource(strings = {"a =", "exists ="})
160+
void rejectsMalformedComparisons(String sql) {
161+
assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition());
162+
assertThrows(JSQLParserException.class,
163+
() -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql));
164+
}
165+
166+
@ParameterizedTest
167+
@ValueSource(booleans = {false, true})
168+
void honorsInterruptionBeforeConditionDispatch(boolean sqlCondition) {
169+
CCJSqlParser parser = CCJSqlParserUtil.newParser("1");
170+
parser.interrupted = true;
171+
assertThrows(ParseException.class, () -> {
172+
if (sqlCondition) {
173+
parser.SQLCondition();
174+
} else {
175+
parser.Condition();
176+
}
177+
});
178+
assertEquals("1", parser.getNextToken().image);
179+
}
180+
181+
@ParameterizedTest
182+
@CsvSource(value = {"arg|=>", "arg|:=", "OUTER|=>", "OUTER|:=", "\"arg\"|=>",
183+
"\"arg\"|:=", "overlaps|=>", "overlaps|:="}, delimiter = '|')
184+
void preservesNamedParameterKindsInEveryArgumentPosition(String name, String assignment)
185+
throws JSQLParserException {
186+
for (String prefix : new String[] {"", "a.b, "}) {
187+
String sql = "SELECT f(" + prefix + name + " " + assignment + " value_col)";
188+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql);
189+
Function function = assertInstanceOf(Function.class,
190+
select.getSelectItem(0).getExpression());
191+
int index = prefix.isEmpty() ? 0 : 1;
192+
assertEquals(index + 1, function.getParameters().size());
193+
if (index == 1) {
194+
assertEquals("a.b", assertInstanceOf(Column.class,
195+
function.getParameters().get(0)).getFullyQualifiedName());
196+
}
197+
Expression argument = function.getParameters().get(index);
198+
Expression value;
199+
if ("=>".equals(assignment)) {
200+
OracleNamedFunctionParameter parameter =
201+
assertInstanceOf(OracleNamedFunctionParameter.class, argument);
202+
assertEquals(name, parameter.getName());
203+
value = parameter.getExpression();
204+
} else {
205+
PostgresNamedFunctionParameter parameter =
206+
assertInstanceOf(PostgresNamedFunctionParameter.class, argument);
207+
assertEquals(name, parameter.getName());
208+
value = parameter.getExpression();
209+
}
210+
assertEquals("value_col", assertInstanceOf(Column.class, value).getColumnName());
211+
}
212+
}
213+
214+
@ParameterizedTest
215+
@ValueSource(strings = {"a.b => 1", "a.b := 1", "1 => 1", "1 := 1", "'a' => 1",
216+
"'a' := 1", "arg =>", "arg :="})
217+
void rejectsInvalidNamedParametersInEveryArgumentPosition(String argument) {
218+
for (String prefix : new String[] {"", "a.b, "}) {
219+
assertThrows(JSQLParserException.class,
220+
() -> CCJSqlParserUtil.parse("SELECT f(" + prefix + argument + ")"));
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)