Skip to content

Commit a6c8e89

Browse files
fix: support EXISTS identifiers without breaking predicates (#2773)
Signed-off-by: 付典 <fudianchn@gmail.com> Co-authored-by: manticore-projects <andreas@manticore-projects.com>
1 parent 7dcdeec commit a6c8e89

2 files changed

Lines changed: 239 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
@@ -1265,6 +1265,31 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12651265
}
12661266
}
12671267

1268+
/** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */
1269+
private boolean isExistsIdentifierAhead() {
1270+
if (getToken(1).kind != K_EXISTS) {
1271+
return false;
1272+
}
1273+
if ((ternaryThenBranchDepth > 0 || mapKeyDepth > 0)
1274+
&& ":".equals(getToken(2).image)) {
1275+
return true;
1276+
}
1277+
int kind = getToken(2).kind;
1278+
if (kind == OPENING_BRACKET) {
1279+
return "+".equals(getToken(3).image) && getToken(4).kind == CLOSING_BRACKET;
1280+
}
1281+
if (kind == K_LIMIT || kind == K_OFFSET) {
1282+
Token value = getToken(3);
1283+
return value.kind == S_LONG || value.kind == S_DOUBLE || value.kind == S_PARAMETER
1284+
|| value.kind == K_ALL || value.kind == K_NULL
1285+
|| "?".equals(value.image) || ":".equals(value.image)
1286+
|| "&".equals(value.image);
1287+
}
1288+
return isConditionKeywordAhead(2)
1289+
|| kind == K_ORDER && (getToken(3).kind == K_BY || getToken(3).kind == K_SIBLINGS)
1290+
|| kind == K_GROUP && getToken(3).kind == K_BY;
1291+
}
1292+
12681293
/**
12691294
* Follower-based disambiguation for reserved keywords in ambiguous
12701295
* positions (implicit alias, clause boundary, after parenthesised
@@ -2597,7 +2622,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
25972622
// PostgreSQL JSON operator, it is consumed by prattExpressionRest instead
25982623
if ("?".equals(t.image) && isTernaryAhead()) return false;
25992624
if (isComparisonOperator(t)) return true;
2600-
switch (t.kind) {
2625+
return isConditionKeywordAhead(1);
2626+
}
2627+
2628+
private boolean isConditionKeywordAhead(int offset) {
2629+
switch (getToken(offset).kind) {
26012630
// Each suffix's start token:
26022631
case K_OVERLAPS: // OVERLAPS
26032632
case K_IN: // IN
@@ -2625,10 +2654,10 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
26252654
return true;
26262655
case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR
26272656
// A column's NOT NULL constraint starts after its DEFAULT expression.
2628-
return getToken(2).kind != K_NULL;
2657+
return getToken(offset + 1).kind != K_NULL;
26292658
// Oracle (+) before IN: col(+) IN (...)
26302659
case OPENING_BRACKET:
2631-
return getToken(2).image.equals("+");
2660+
return getToken(offset + 1).image.equals("+");
26322661
default:
26332662
return false;
26342663
}
@@ -3009,6 +3038,7 @@ String NonReservedWord() :
30093038
| tk=<K_EXECUTE: "EXECUTE">
30103039
| tk=<K_EXPLAIN:"EXPLAIN">
30113040
| tk=<K_EXPLICIT:"EXPLICIT">
3041+
| tk=<K_EXISTS:"EXISTS">
30123042
| tk=<K_EXTENDED:"EXTENDED">
30133043
| tk=<K_EXTRACT:"EXTRACT">
30143044
| tk=<K_EXPORT:"EXPORT">
@@ -3336,7 +3366,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */
33363366
| <K_ELSE:"ELSE">
33373367
| <K_EXCEPT: "EXCEPT">
33383368
| <K_EXCLUDES: "EXCLUDES"> /* Salesforce SOQL */
3339-
| <K_EXISTS:"EXISTS">
33403369
| <K_EXTEND:"EXTEND">
33413370
| <K_FETCH:"FETCH">
33423371
| <K_FINAL: "FINAL">
@@ -11004,6 +11033,12 @@ Expression Condition():
1100411033
( <K_NOT> { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } )
1100511034
)*
1100611035
(
11036+
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
11037+
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
11038+
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
11039+
result=ExistsExpression()
11040+
|
11041+
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
1100711042
result=ExistsExpression()
1100811043
|
1100911044
[ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
@@ -11149,8 +11184,14 @@ Expression SQLCondition():
1114911184
}
1115011185
{
1115111186
(
11152-
result=ExistsExpression()
11153-
| left = SimpleExpression() { result = left; }
11187+
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
11188+
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
11189+
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
11190+
result=ExistsExpression()
11191+
|
11192+
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
11193+
result=ExistsExpression()
11194+
| left = SimpleExpression() { result = left; }
1115411195
[
1115511196
LOOKAHEAD(2) (
1115611197
LOOKAHEAD(2, <K_OVERLAPS>) result=OverlapsCondition(left)

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

Lines changed: 192 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,
@@ -63,6 +77,182 @@ public void testGroupsAsIdentifier() throws JSQLParserException {
6377
assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (groups INT)", true);
6478
}
6579

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());
66256
// ERRORS introduces Exasol's IMPORT/EXPORT error clauses but is an ordinary name elsewhere,
67257
// e.g. a DuckDB column holding the errors of an upstream step.
68258
@Test

0 commit comments

Comments
 (0)