Skip to content

Commit 4770815

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

2 files changed

Lines changed: 227 additions & 8 deletions

File tree

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

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

1221+
/** Disambiguate the EXISTS column's Oracle join suffix and expression delimiters. */
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+
return isConditionKeywordAhead(2)
1235+
|| kind == K_ORDER && (getToken(3).kind == K_BY || getToken(3).kind == K_SIBLINGS)
1236+
|| kind == K_GROUP && getToken(3).kind == K_BY
1237+
|| (kind == K_LIMIT || kind == K_OFFSET)
1238+
&& (getToken(3).kind == S_LONG || getToken(3).kind == S_PARAMETER);
1239+
}
1240+
12211241
/**
12221242
* Follower-based disambiguation for reserved keywords in ambiguous
12231243
* positions (implicit alias, clause boundary, after parenthesised
@@ -2200,7 +2220,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
22002220
// PostgreSQL JSON operator, it is consumed by prattExpressionRest instead
22012221
if ("?".equals(t.image) && isTernaryAhead()) return false;
22022222
if (isComparisonOperator(t)) return true;
2203-
switch (t.kind) {
2223+
return isConditionKeywordAhead(1);
2224+
}
2225+
2226+
private boolean isConditionKeywordAhead(int offset) {
2227+
switch (getToken(offset).kind) {
22042228
// Each suffix's start token:
22052229
case K_OVERLAPS: // OVERLAPS
22062230
case K_IN: // IN
@@ -2228,10 +2252,10 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
22282252
return true;
22292253
case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR
22302254
// A column's NOT NULL constraint starts after its DEFAULT expression.
2231-
return getToken(2).kind != K_NULL;
2255+
return getToken(offset + 1).kind != K_NULL;
22322256
// Oracle (+) before IN: col(+) IN (...)
22332257
case OPENING_BRACKET:
2234-
return getToken(2).image.equals("+");
2258+
return getToken(offset + 1).image.equals("+");
22352259
default:
22362260
return false;
22372261
}
@@ -2571,6 +2595,7 @@ String NonReservedWord() :
25712595
| tk=<K_EXECUTE: "EXECUTE">
25722596
| tk=<K_EXPLAIN:"EXPLAIN">
25732597
| tk=<K_EXPLICIT:"EXPLICIT">
2598+
| tk=<K_EXISTS:"EXISTS">
25742599
| tk=<K_EXTENDED:"EXTENDED">
25752600
| tk=<K_EXTRACT:"EXTRACT">
25762601
| tk=<K_EXPORT:"EXPORT">
@@ -2893,7 +2918,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */
28932918
| <K_ERRORS: "ERRORS">
28942919
| <K_EXCEPT: "EXCEPT">
28952920
| <K_EXCLUDES: "EXCLUDES"> /* Salesforce SOQL */
2896-
| <K_EXISTS:"EXISTS">
28972921
| <K_EXTEND:"EXTEND">
28982922
| <K_FBV:"FBV">
28992923
| <K_FETCH:"FETCH">
@@ -10458,6 +10482,12 @@ Expression Condition():
1045810482
( <K_NOT> { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } )
1045910483
)*
1046010484
(
10485+
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
10486+
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
10487+
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
10488+
result=ExistsExpression()
10489+
|
10490+
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
1046110491
result=ExistsExpression()
1046210492
|
1046310493
[ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
@@ -10602,8 +10632,14 @@ Expression SQLCondition():
1060210632
}
1060310633
{
1060410634
(
10605-
result=ExistsExpression()
10606-
| left = SimpleExpression() { result = left; }
10635+
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
10636+
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
10637+
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
10638+
result=ExistsExpression()
10639+
|
10640+
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
10641+
result=ExistsExpression()
10642+
| left = SimpleExpression() { result = left; }
1060710643
[
1060810644
LOOKAHEAD(2) (
1060910645
LOOKAHEAD(2, <K_OVERLAPS>) result=OverlapsCondition(left)

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

Lines changed: 185 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,173 @@ 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"})
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", "GROUP BY id", "LIMIT 1"})
225+
void testExistsColumnBeforeClause(String suffix) throws JSQLParserException {
226+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
227+
"SELECT * FROM t WHERE exists " + suffix, true);
228+
assertEquals("exists", assertInstanceOf(Column.class, select.getWhere()).getColumnName());
229+
}
230+
231+
@Test
232+
void testExistsBeforeNestedTernary() throws JSQLParserException {
233+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
234+
"SELECT flag ? exists : b ? c : d FROM t", true);
235+
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
236+
select.getSelectItem(0).getExpression());
237+
assertEquals("exists", assertInstanceOf(Column.class, ternary.getThenExpression())
238+
.getColumnName());
239+
assertInstanceOf(TernaryExpression.class, ternary.getElseExpression());
240+
PlainSelect mapSelect = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
241+
"SELECT MAP {exists: b ? c : d}", true);
242+
MapExpression map = assertInstanceOf(MapExpression.class,
243+
mapSelect.getSelectItem(0).getExpression());
244+
assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey())
245+
.getColumnName());
246+
assertInstanceOf(TernaryExpression.class, map.getEntries().get(0).getValue());
247+
}
65248
}

0 commit comments

Comments
 (0)