Skip to content

Commit 25fc1d5

Browse files
committed
fix: support EXISTS identifiers without breaking predicates
Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 8d12a5d commit 25fc1d5

2 files changed

Lines changed: 241 additions & 8 deletions

File tree

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

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,31 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12181218
}
12191219
}
12201220

1221+
/** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */
1222+
private boolean isExistsIdentifierAhead() {
1223+
if (getToken(1).kind != K_EXISTS) {
1224+
return false;
1225+
}
1226+
if ((ternaryThenBranchDepth > 0 || mapKeyDepth > 0)
1227+
&& ":".equals(getToken(2).image)) {
1228+
return true;
1229+
}
1230+
int kind = getToken(2).kind;
1231+
if (kind == OPENING_BRACKET) {
1232+
return "+".equals(getToken(3).image) && getToken(4).kind == CLOSING_BRACKET;
1233+
}
1234+
if (kind == K_LIMIT || kind == K_OFFSET) {
1235+
Token value = getToken(3);
1236+
return value.kind == S_LONG || value.kind == S_DOUBLE || value.kind == S_PARAMETER
1237+
|| value.kind == K_ALL || value.kind == K_NULL
1238+
|| "?".equals(value.image) || ":".equals(value.image)
1239+
|| "&".equals(value.image);
1240+
}
1241+
return isConditionKeywordAhead(2)
1242+
|| kind == K_ORDER && (getToken(3).kind == K_BY || getToken(3).kind == K_SIBLINGS)
1243+
|| kind == K_GROUP && getToken(3).kind == K_BY;
1244+
}
1245+
12211246
/**
12221247
* Follower-based disambiguation for reserved keywords in ambiguous
12231248
* positions (implicit alias, clause boundary, after parenthesised
@@ -2200,7 +2225,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
22002225
// PostgreSQL JSON operator, it is consumed by prattExpressionRest instead
22012226
if ("?".equals(t.image) && isTernaryAhead()) return false;
22022227
if (isComparisonOperator(t)) return true;
2203-
switch (t.kind) {
2228+
return isConditionKeywordAhead(1);
2229+
}
2230+
2231+
private boolean isConditionKeywordAhead(int offset) {
2232+
switch (getToken(offset).kind) {
22042233
// Each suffix's start token:
22052234
case K_OVERLAPS: // OVERLAPS
22062235
case K_IN: // IN
@@ -2228,10 +2257,10 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
22282257
return true;
22292258
case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR
22302259
// A column's NOT NULL constraint starts after its DEFAULT expression.
2231-
return getToken(2).kind != K_NULL;
2260+
return getToken(offset + 1).kind != K_NULL;
22322261
// Oracle (+) before IN: col(+) IN (...)
22332262
case OPENING_BRACKET:
2234-
return getToken(2).image.equals("+");
2263+
return getToken(offset + 1).image.equals("+");
22352264
default:
22362265
return false;
22372266
}
@@ -2571,6 +2600,7 @@ String NonReservedWord() :
25712600
| tk=<K_EXECUTE: "EXECUTE">
25722601
| tk=<K_EXPLAIN:"EXPLAIN">
25732602
| tk=<K_EXPLICIT:"EXPLICIT">
2603+
| tk=<K_EXISTS:"EXISTS">
25742604
| tk=<K_EXTENDED:"EXTENDED">
25752605
| tk=<K_EXTRACT:"EXTRACT">
25762606
| tk=<K_EXPORT:"EXPORT">
@@ -2893,7 +2923,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */
28932923
| <K_ERRORS: "ERRORS">
28942924
| <K_EXCEPT: "EXCEPT">
28952925
| <K_EXCLUDES: "EXCLUDES"> /* Salesforce SOQL */
2896-
| <K_EXISTS:"EXISTS">
28972926
| <K_EXTEND:"EXTEND">
28982927
| <K_FBV:"FBV">
28992928
| <K_FETCH:"FETCH">
@@ -10458,6 +10487,12 @@ Expression Condition():
1045810487
( <K_NOT> { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } )
1045910488
)*
1046010489
(
10490+
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
10491+
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
10492+
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
10493+
result=ExistsExpression()
10494+
|
10495+
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
1046110496
result=ExistsExpression()
1046210497
|
1046310498
[ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
@@ -10602,8 +10637,14 @@ Expression SQLCondition():
1060210637
}
1060310638
{
1060410639
(
10605-
result=ExistsExpression()
10606-
| left = SimpleExpression() { result = left; }
10640+
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
10641+
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
10642+
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
10643+
result=ExistsExpression()
10644+
|
10645+
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
10646+
result=ExistsExpression()
10647+
| left = SimpleExpression() { result = left; }
1060710648
[
1060810649
LOOKAHEAD(2) (
1060910650
LOOKAHEAD(2, <K_OVERLAPS>) result=OverlapsCondition(left)

‎src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java‎

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,29 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12+
import java.util.stream.Stream;
1213
import net.sf.jsqlparser.JSQLParserException;
14+
import net.sf.jsqlparser.expression.NotExpression;
15+
import net.sf.jsqlparser.expression.MapExpression;
16+
import net.sf.jsqlparser.expression.JdbcNamedParameter;
17+
import net.sf.jsqlparser.expression.TernaryExpression;
18+
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
19+
import net.sf.jsqlparser.expression.operators.relational.InExpression;
20+
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
21+
import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax;
22+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1323
import net.sf.jsqlparser.parser.ParserKeywordsUtils;
24+
import net.sf.jsqlparser.schema.Column;
25+
import net.sf.jsqlparser.schema.Table;
26+
import net.sf.jsqlparser.statement.select.PlainSelect;
27+
import net.sf.jsqlparser.statement.select.WithItem;
1428
import org.junit.jupiter.api.Test;
1529
import org.junit.jupiter.params.ParameterizedTest;
1630
import org.junit.jupiter.params.provider.MethodSource;
17-
18-
import java.util.stream.Stream;
31+
import org.junit.jupiter.params.provider.ValueSource;
1932

2033
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
34+
import static org.junit.jupiter.api.Assertions.*;
2135

2236
/**
2337
* Verifies that all non-reserved keywords can be used as unquoted identifiers (schema, table,
@@ -62,4 +76,182 @@ public void testGroupsAsIdentifier() throws JSQLParserException {
6276
assertSqlCanBeParsedAndDeparsed("SELECT a groups FROM mytable", true);
6377
assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (groups INT)", true);
6478
}
79+
80+
// exists is a non-reserved keyword in the common dialects and reaches the
81+
// parser as an ordinary column name in downstream rewriting tools.
82+
@Test
83+
public void testExistsAsIdentifier() throws JSQLParserException {
84+
assertSqlCanBeParsedAndDeparsed("SELECT exists FROM mytable", true);
85+
assertSqlCanBeParsedAndDeparsed("SELECT mytable.exists FROM mytable", true);
86+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE exists = 1", true);
87+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable ORDER BY exists", true);
88+
assertSqlCanBeParsedAndDeparsed("UPDATE mytable SET exists = 1 WHERE id = 1", true);
89+
assertSqlCanBeParsedAndDeparsed("INSERT INTO mytable (exists) VALUES (1)", true);
90+
assertSqlCanBeParsedAndDeparsed("SELECT a AS exists FROM mytable", true);
91+
}
92+
93+
// the guarded keyword form keeps its operator meaning
94+
@Test
95+
public void testExistsPredicateStaysPredicate() throws JSQLParserException {
96+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
97+
"SELECT EXISTS (SELECT 1 FROM x) FROM mytable", true);
98+
assertInstanceOf(ExistsExpression.class, select.getSelectItem(0).getExpression());
99+
PlainSelect negated = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
100+
"SELECT * FROM mytable WHERE NOT EXISTS (SELECT 1 FROM x)", true);
101+
assertInstanceOf(ExistsExpression.class,
102+
assertInstanceOf(NotExpression.class, negated.getWhere()).getExpression());
103+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE EXISTS (SELECT 1 FROM x)",
104+
true);
105+
}
106+
107+
@ParameterizedTest
108+
@ValueSource(strings = {"exists", "ExIsTs"})
109+
void testExistsImplicitAliases(String keyword) throws JSQLParserException {
110+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
111+
"SELECT a " + keyword + " FROM mytable " + keyword, true);
112+
assertEquals(keyword, select.getSelectItem(0).getAlias().getName());
113+
assertEquals(keyword, select.getFromItem().getAlias().getName());
114+
assertEquals("a", assertInstanceOf(Column.class,
115+
select.getSelectItem(0).getExpression()).getColumnName());
116+
}
117+
118+
@ParameterizedTest
119+
@ValueSource(strings = {"exists", "ExIsTs"})
120+
void testExistsCte(String keyword) throws JSQLParserException {
121+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
122+
"WITH " + keyword + "(x) AS (SELECT 1) SELECT x FROM " + keyword, true);
123+
WithItem<?> withItem = select.getWithItemsList().get(0);
124+
assertEquals(keyword, withItem.getAliasName());
125+
assertNotNull(withItem.getParenthesedStatement());
126+
assertNull(withItem.getExpression());
127+
assertEquals(keyword, assertInstanceOf(Table.class, select.getFromItem()).getName());
128+
assertSqlCanBeParsedAndDeparsed(
129+
"WITH RECURSIVE " + keyword + "(x) AS (SELECT 1) SELECT * FROM " + keyword, true);
130+
}
131+
132+
@ParameterizedTest
133+
@ValueSource(strings = {"exists", "ExIsTs"})
134+
void testExistsExpressionContexts(String keyword) throws JSQLParserException {
135+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
136+
"SELECT ALL " + keyword + " FROM mytable");
137+
assertEquals(keyword, assertInstanceOf(Column.class,
138+
select.getSelectItem(0).getExpression()).getColumnName());
139+
assertSqlCanBeParsedAndDeparsed("SELECT CASE " + keyword
140+
+ " WHEN 1 THEN " + keyword + " ELSE 0 END FROM mytable", true);
141+
assertSqlCanBeParsedAndDeparsed("UPDATE " + keyword + " SET a = 1", true);
142+
}
143+
144+
@ParameterizedTest
145+
@ValueSource(strings = {"exists", "ExIsTs"})
146+
void testExistsTernaryOperand(String keyword) throws JSQLParserException {
147+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
148+
"SELECT flag ? " + keyword + " : 0 FROM mytable", true);
149+
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
150+
select.getSelectItem(0).getExpression());
151+
assertEquals(keyword, assertInstanceOf(Column.class, ternary.getThenExpression())
152+
.getColumnName());
153+
}
154+
155+
@Test
156+
void testExistsColumnWithOracleJoin() throws JSQLParserException {
157+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
158+
"SELECT * FROM mytable WHERE exists(+) = 1", true);
159+
EqualsTo equals = assertInstanceOf(EqualsTo.class, select.getWhere());
160+
assertEquals("exists", assertInstanceOf(Column.class, equals.getLeftExpression())
161+
.getColumnName());
162+
assertEquals(SupportsOldOracleJoinSyntax.ORACLE_JOIN_RIGHT,
163+
((Column) equals.getLeftExpression()).getOldOracleJoinSyntax());
164+
}
165+
166+
@ParameterizedTest
167+
@ValueSource(strings = {"SELECT 1", "VALUES (1)"})
168+
void testExistsUnparenthesizedSubSelect(String query) throws JSQLParserException {
169+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
170+
"SELECT EXISTS " + query, true, p -> p.withUnparenthesizedSubSelects(true));
171+
assertInstanceOf(ExistsExpression.class, select.getSelectItem(0).getExpression());
172+
PlainSelect where = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
173+
"SELECT * FROM mytable WHERE EXISTS " + query, true,
174+
p -> p.withUnparenthesizedSubSelects(true));
175+
assertInstanceOf(ExistsExpression.class, where.getWhere());
176+
}
177+
178+
@ParameterizedTest
179+
@ValueSource(strings = {"a", "t.a", "[1]", "+1", "(1)", "(+1)", ":arr", "limit", "offset"})
180+
void testExistsBareOperandStaysPredicate(String operand) throws JSQLParserException {
181+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
182+
"SELECT EXISTS " + operand + " FROM t", true);
183+
ExistsExpression exists = assertInstanceOf(ExistsExpression.class,
184+
select.getSelectItem(0).getExpression());
185+
assertEquals(operand, exists.getRightExpression().toString());
186+
PlainSelect where = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
187+
"SELECT * FROM t WHERE EXISTS " + operand, true);
188+
assertInstanceOf(ExistsExpression.class, where.getWhere());
189+
}
190+
191+
@Test
192+
void testExistsNamedParameterInTernary() throws JSQLParserException {
193+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
194+
"SELECT flag ? EXISTS :arr : 0 FROM t", true);
195+
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
196+
select.getSelectItem(0).getExpression());
197+
ExistsExpression exists = assertInstanceOf(ExistsExpression.class,
198+
ternary.getThenExpression());
199+
assertEquals("arr", assertInstanceOf(JdbcNamedParameter.class,
200+
exists.getRightExpression()).getName());
201+
}
202+
203+
@Test
204+
void testExistsMapKey() throws JSQLParserException {
205+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
206+
"SELECT MAP {exists: 1}", true);
207+
MapExpression map = assertInstanceOf(MapExpression.class,
208+
select.getSelectItem(0).getExpression());
209+
assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey())
210+
.getColumnName());
211+
}
212+
213+
@ParameterizedTest
214+
@ValueSource(strings = {"IN (1, 2)", "NOT IN (1, 2)"})
215+
void testExistsColumnInCondition(String suffix) throws JSQLParserException {
216+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
217+
"SELECT * FROM t WHERE exists " + suffix, true);
218+
InExpression in = assertInstanceOf(InExpression.class, select.getWhere());
219+
assertEquals("exists", assertInstanceOf(Column.class, in.getLeftExpression())
220+
.getColumnName());
221+
}
222+
223+
@ParameterizedTest
224+
@ValueSource(strings = {"ORDER BY id", "ORDER SIBLINGS BY id", "GROUP BY id", "LIMIT 1",
225+
"LIMIT 1.0", "LIMIT ALL", "LIMIT NULL", "LIMIT ?", "LIMIT $1", "LIMIT :n", "LIMIT &n",
226+
"OFFSET 1", "OFFSET 1.0", "OFFSET ?", "OFFSET $1", "OFFSET :n", "OFFSET &n"})
227+
void testExistsColumnBeforeClause(String suffix) throws JSQLParserException {
228+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
229+
"SELECT * FROM t WHERE exists " + suffix, true);
230+
assertEquals("exists", assertInstanceOf(Column.class, select.getWhere()).getColumnName());
231+
if (suffix.startsWith("LIMIT")) {
232+
assertNotNull(select.getLimit());
233+
assertEquals(suffix, select.getLimit().toString().trim());
234+
} else if (suffix.startsWith("OFFSET")) {
235+
assertNotNull(select.getOffset());
236+
assertEquals(suffix, select.getOffset().toString().trim());
237+
}
238+
}
239+
240+
@Test
241+
void testExistsBeforeNestedTernary() throws JSQLParserException {
242+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
243+
"SELECT flag ? exists : b ? c : d FROM t", true);
244+
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
245+
select.getSelectItem(0).getExpression());
246+
assertEquals("exists", assertInstanceOf(Column.class, ternary.getThenExpression())
247+
.getColumnName());
248+
assertInstanceOf(TernaryExpression.class, ternary.getElseExpression());
249+
PlainSelect mapSelect = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
250+
"SELECT MAP {exists: b ? c : d}", true);
251+
MapExpression map = assertInstanceOf(MapExpression.class,
252+
mapSelect.getSelectItem(0).getExpression());
253+
assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey())
254+
.getColumnName());
255+
assertInstanceOf(TernaryExpression.class, map.getEntries().get(0).getValue());
256+
}
65257
}

0 commit comments

Comments
 (0)