Skip to content

Commit 68c035b

Browse files
committed
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>
1 parent 1db8572 commit 68c035b

2 files changed

Lines changed: 230 additions & 5 deletions

File tree

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11063,7 +11063,7 @@ Expression Condition():
1106311063
// themselves reject is equivalent to scanning first, so skip the deep
1106411064
// lookahead simulation (this path runs for every plain condition)
1106511065
LOOKAHEAD({ !interrupted })
11066-
[ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
11066+
[ LOOKAHEAD({ getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
1106711067
left=SimpleExpression() { result = left; }
1106811068

1106911069
// Consume Oracle (+) once, before dispatching
@@ -11219,7 +11219,7 @@ Expression SQLCondition():
1121911219
left = SimpleExpression() { result = left; }
1122011220
[
1122111221
LOOKAHEAD(2) (
11222-
LOOKAHEAD(2, <K_OVERLAPS>) result=OverlapsCondition(left)
11222+
LOOKAHEAD({ getToken(1).kind == K_OVERLAPS }) result=OverlapsCondition(left)
1122311223
|
1122411224
LOOKAHEAD(3, {!interrupted}) result=InExpression(left)
1122511225
|
@@ -12112,7 +12112,8 @@ Expression PrimaryExpression() #PrimaryExpression:
1211212112
//| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns()
1211312113

1211412114
// support timestamp expressions
12115-
| 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); }
1211612117

1211712118
| LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression()
1211812119

@@ -12132,7 +12133,7 @@ Expression PrimaryExpression() #PrimaryExpression:
1213212133

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

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

1213712138
| LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() })
1213812139
retval=CharacterSetIntroducedString()
@@ -12146,7 +12147,8 @@ Expression PrimaryExpression() #PrimaryExpression:
1214612147
"(" "+" ")" { ((Column) retval).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); }
1214712148
]
1214812149

12149-
| 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); }
1215012152

1215112153
| retval=CharacterLiteral() { linkAST(retval,jjtThis); }
1215212154

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)