Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,31 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}
}

/** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */
private boolean isExistsIdentifierAhead() {
if (getToken(1).kind != K_EXISTS) {
return false;
}
if ((ternaryThenBranchDepth > 0 || mapKeyDepth > 0)
&& ":".equals(getToken(2).image)) {
return true;
}
int kind = getToken(2).kind;
if (kind == OPENING_BRACKET) {
return "+".equals(getToken(3).image) && getToken(4).kind == CLOSING_BRACKET;
}
if (kind == K_LIMIT || kind == K_OFFSET) {
Token value = getToken(3);
return value.kind == S_LONG || value.kind == S_DOUBLE || value.kind == S_PARAMETER
|| value.kind == K_ALL || value.kind == K_NULL
|| "?".equals(value.image) || ":".equals(value.image)
|| "&".equals(value.image);
}
return isConditionKeywordAhead(2)
|| kind == K_ORDER && (getToken(3).kind == K_BY || getToken(3).kind == K_SIBLINGS)
|| kind == K_GROUP && getToken(3).kind == K_BY;
}

/**
* Follower-based disambiguation for reserved keywords in ambiguous
* positions (implicit alias, clause boundary, after parenthesised
Expand Down Expand Up @@ -2597,7 +2622,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
// PostgreSQL JSON operator, it is consumed by prattExpressionRest instead
if ("?".equals(t.image) && isTernaryAhead()) return false;
if (isComparisonOperator(t)) return true;
switch (t.kind) {
return isConditionKeywordAhead(1);
}

private boolean isConditionKeywordAhead(int offset) {
switch (getToken(offset).kind) {
// Each suffix's start token:
case K_OVERLAPS: // OVERLAPS
case K_IN: // IN
Expand Down Expand Up @@ -2625,10 +2654,10 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
return true;
case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR
// A column's NOT NULL constraint starts after its DEFAULT expression.
return getToken(2).kind != K_NULL;
return getToken(offset + 1).kind != K_NULL;
// Oracle (+) before IN: col(+) IN (...)
case OPENING_BRACKET:
return getToken(2).image.equals("+");
return getToken(offset + 1).image.equals("+");
default:
return false;
}
Expand Down Expand Up @@ -3009,6 +3038,7 @@ String NonReservedWord() :
| tk=<K_EXECUTE: "EXECUTE">
| tk=<K_EXPLAIN:"EXPLAIN">
| tk=<K_EXPLICIT:"EXPLICIT">
| tk=<K_EXISTS:"EXISTS">
| tk=<K_EXTENDED:"EXTENDED">
| tk=<K_EXTRACT:"EXTRACT">
| tk=<K_EXPORT:"EXPORT">
Expand Down Expand Up @@ -3336,7 +3366,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */
| <K_ELSE:"ELSE">
| <K_EXCEPT: "EXCEPT">
| <K_EXCLUDES: "EXCLUDES"> /* Salesforce SOQL */
| <K_EXISTS:"EXISTS">
| <K_EXTEND:"EXTEND">
| <K_FETCH:"FETCH">
| <K_FINAL: "FINAL">
Expand Down Expand Up @@ -11004,6 +11033,12 @@ Expression Condition():
( <K_NOT> { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } )
)*
(
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
result=ExistsExpression()
|
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
result=ExistsExpression()
|
[ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) <K_PRIOR> { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ]
Expand Down Expand Up @@ -11149,8 +11184,14 @@ Expression SQLCondition():
}
{
(
result=ExistsExpression()
| left = SimpleExpression() { result = left; }
LOOKAHEAD(<K_EXISTS> JdbcNamedParameter() ":", {
getToken(1).kind == K_EXISTS && ":".equals(getToken(2).image)
&& (ternaryThenBranchDepth > 0 || mapKeyDepth > 0) })
result=ExistsExpression()
|
LOOKAHEAD(2, { !isExistsIdentifierAhead() })
result=ExistsExpression()
| left = SimpleExpression() { result = left; }
[
LOOKAHEAD(2) (
LOOKAHEAD(2, <K_OVERLAPS>) result=OverlapsCondition(left)
Expand Down
194 changes: 192 additions & 2 deletions src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,29 @@
*/
package net.sf.jsqlparser.statement;

import java.util.stream.Stream;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.NotExpression;
import net.sf.jsqlparser.expression.MapExpression;
import net.sf.jsqlparser.expression.JdbcNamedParameter;
import net.sf.jsqlparser.expression.TernaryExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.ParserKeywordsUtils;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.WithItem;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;
import org.junit.jupiter.params.provider.ValueSource;

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

/**
* Verifies that all non-reserved keywords can be used as unquoted identifiers (schema, table,
Expand Down Expand Up @@ -63,10 +77,186 @@
assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (groups INT)", true);
}

// exists is a non-reserved keyword in the common dialects and reaches the
// parser as an ordinary column name in downstream rewriting tools.
@Test
public void testExistsAsIdentifier() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed("SELECT exists FROM mytable", true);
assertSqlCanBeParsedAndDeparsed("SELECT mytable.exists FROM mytable", true);
assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE exists = 1", true);
assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable ORDER BY exists", true);
assertSqlCanBeParsedAndDeparsed("UPDATE mytable SET exists = 1 WHERE id = 1", true);
assertSqlCanBeParsedAndDeparsed("INSERT INTO mytable (exists) VALUES (1)", true);
assertSqlCanBeParsedAndDeparsed("SELECT a AS exists FROM mytable", true);
}

// the guarded keyword form keeps its operator meaning
@Test
public void testExistsPredicateStaysPredicate() throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT EXISTS (SELECT 1 FROM x) FROM mytable", true);
assertInstanceOf(ExistsExpression.class, select.getSelectItem(0).getExpression());
PlainSelect negated = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM mytable WHERE NOT EXISTS (SELECT 1 FROM x)", true);
assertInstanceOf(ExistsExpression.class,
assertInstanceOf(NotExpression.class, negated.getWhere()).getExpression());
assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE EXISTS (SELECT 1 FROM x)",
true);
}

@ParameterizedTest
@ValueSource(strings = {"exists", "ExIsTs"})
void testExistsImplicitAliases(String keyword) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT a " + keyword + " FROM mytable " + keyword, true);
assertEquals(keyword, select.getSelectItem(0).getAlias().getName());
assertEquals(keyword, select.getFromItem().getAlias().getName());
assertEquals("a", assertInstanceOf(Column.class,
select.getSelectItem(0).getExpression()).getColumnName());
}

@ParameterizedTest
@ValueSource(strings = {"exists", "ExIsTs"})
void testExistsCte(String keyword) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"WITH " + keyword + "(x) AS (SELECT 1) SELECT x FROM " + keyword, true);
WithItem<?> withItem = select.getWithItemsList().get(0);
assertEquals(keyword, withItem.getAliasName());
assertNotNull(withItem.getParenthesedStatement());
assertNull(withItem.getExpression());
assertEquals(keyword, assertInstanceOf(Table.class, select.getFromItem()).getName());
assertSqlCanBeParsedAndDeparsed(
"WITH RECURSIVE " + keyword + "(x) AS (SELECT 1) SELECT * FROM " + keyword, true);
}

@ParameterizedTest
@ValueSource(strings = {"exists", "ExIsTs"})
void testExistsExpressionContexts(String keyword) throws JSQLParserException {
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
"SELECT ALL " + keyword + " FROM mytable");
assertEquals(keyword, assertInstanceOf(Column.class,
select.getSelectItem(0).getExpression()).getColumnName());
assertSqlCanBeParsedAndDeparsed("SELECT CASE " + keyword
+ " WHEN 1 THEN " + keyword + " ELSE 0 END FROM mytable", true);
assertSqlCanBeParsedAndDeparsed("UPDATE " + keyword + " SET a = 1", true);
}

@ParameterizedTest
@ValueSource(strings = {"exists", "ExIsTs"})
void testExistsTernaryOperand(String keyword) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT flag ? " + keyword + " : 0 FROM mytable", true);
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
select.getSelectItem(0).getExpression());
assertEquals(keyword, assertInstanceOf(Column.class, ternary.getThenExpression())
.getColumnName());
}

@Test
void testExistsColumnWithOracleJoin() throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM mytable WHERE exists(+) = 1", true);
EqualsTo equals = assertInstanceOf(EqualsTo.class, select.getWhere());
assertEquals("exists", assertInstanceOf(Column.class, equals.getLeftExpression())
.getColumnName());
assertEquals(SupportsOldOracleJoinSyntax.ORACLE_JOIN_RIGHT,
((Column) equals.getLeftExpression()).getOldOracleJoinSyntax());
}

@ParameterizedTest
@ValueSource(strings = {"SELECT 1", "VALUES (1)"})
void testExistsUnparenthesizedSubSelect(String query) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT EXISTS " + query, true, p -> p.withUnparenthesizedSubSelects(true));
assertInstanceOf(ExistsExpression.class, select.getSelectItem(0).getExpression());
PlainSelect where = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM mytable WHERE EXISTS " + query, true,
p -> p.withUnparenthesizedSubSelects(true));
assertInstanceOf(ExistsExpression.class, where.getWhere());
}

@ParameterizedTest
@ValueSource(strings = {"a", "t.a", "[1]", "+1", "(1)", "(+1)", ":arr", "limit", "offset"})
void testExistsBareOperandStaysPredicate(String operand) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT EXISTS " + operand + " FROM t", true);
ExistsExpression exists = assertInstanceOf(ExistsExpression.class,
select.getSelectItem(0).getExpression());
assertEquals(operand, exists.getRightExpression().toString());
PlainSelect where = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM t WHERE EXISTS " + operand, true);
assertInstanceOf(ExistsExpression.class, where.getWhere());
}

@Test
void testExistsNamedParameterInTernary() throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT flag ? EXISTS :arr : 0 FROM t", true);
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
select.getSelectItem(0).getExpression());
ExistsExpression exists = assertInstanceOf(ExistsExpression.class,
ternary.getThenExpression());
assertEquals("arr", assertInstanceOf(JdbcNamedParameter.class,
exists.getRightExpression()).getName());
}

@Test
void testExistsMapKey() throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT MAP {exists: 1}", true);
MapExpression map = assertInstanceOf(MapExpression.class,
select.getSelectItem(0).getExpression());
assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey())
.getColumnName());
}

@ParameterizedTest
@ValueSource(strings = {"IN (1, 2)", "NOT IN (1, 2)"})
void testExistsColumnInCondition(String suffix) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM t WHERE exists " + suffix, true);
InExpression in = assertInstanceOf(InExpression.class, select.getWhere());
assertEquals("exists", assertInstanceOf(Column.class, in.getLeftExpression())
.getColumnName());
}

@ParameterizedTest
@ValueSource(strings = {"ORDER BY id", "ORDER SIBLINGS BY id", "GROUP BY id", "LIMIT 1",
"LIMIT 1.0", "LIMIT ALL", "LIMIT NULL", "LIMIT ?", "LIMIT $1", "LIMIT :n", "LIMIT &n",
"OFFSET 1", "OFFSET 1.0", "OFFSET ?", "OFFSET $1", "OFFSET :n", "OFFSET &n"})
void testExistsColumnBeforeClause(String suffix) throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM t WHERE exists " + suffix, true);
assertEquals("exists", assertInstanceOf(Column.class, select.getWhere()).getColumnName());
if (suffix.startsWith("LIMIT")) {
assertNotNull(select.getLimit());
assertEquals(suffix, select.getLimit().toString().trim());
} else if (suffix.startsWith("OFFSET")) {
assertNotNull(select.getOffset());
assertEquals(suffix, select.getOffset().toString().trim());
}
}

@Test
void testExistsBeforeNestedTernary() throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT flag ? exists : b ? c : d FROM t", true);
TernaryExpression ternary = assertInstanceOf(TernaryExpression.class,
select.getSelectItem(0).getExpression());
assertEquals("exists", assertInstanceOf(Column.class, ternary.getThenExpression())
.getColumnName());
assertInstanceOf(TernaryExpression.class, ternary.getElseExpression());
PlainSelect mapSelect = (PlainSelect) assertSqlCanBeParsedAndDeparsed(
"SELECT MAP {exists: b ? c : d}", true);
MapExpression map = assertInstanceOf(MapExpression.class,
mapSelect.getSelectItem(0).getExpression());
assertEquals("exists", assertInstanceOf(Column.class, map.getEntries().get(0).getKey())
.getColumnName());
assertInstanceOf(TernaryExpression.class, map.getEntries().get(0).getValue());
// ERRORS introduces Exasol's IMPORT/EXPORT error clauses but is an ordinary name elsewhere,
// e.g. a DuckDB column holding the errors of an upstream step.
@Test
public void testErrorsAsIdentifier() throws JSQLParserException {

Check failure on line 259 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

';' expected

Check failure on line 259 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

not a statement

Check failure on line 259 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

';' expected

Check failure on line 259 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

';' expected

Check failure on line 259 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

not a statement

Check failure on line 259 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

';' expected
assertSqlCanBeParsedAndDeparsed("SELECT errors FROM mytable", true);
assertSqlCanBeParsedAndDeparsed("SELECT t.errors AS errors FROM mytable t", true);
assertSqlCanBeParsedAndDeparsed("WITH errors AS (SELECT 1) SELECT * FROM errors", true);
Expand All @@ -76,7 +266,7 @@
// Dialect-specific clause words (Exasol IMPORT/EXPORT, ClickHouse PREWHERE/SETTINGS, CREATE
// FUNCTION ... RETURNS) are ordinary names in other dialects, e.g. PostgreSQL or DuckDB.
@Test
public void testDialectClauseWordsAsIdentifiers() throws JSQLParserException {

Check failure on line 269 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

';' expected

Check failure on line 269 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

not a statement

Check failure on line 269 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

';' expected

Check failure on line 269 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

';' expected

Check failure on line 269 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

not a statement

Check failure on line 269 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

';' expected
assertSqlCanBeParsedAndDeparsed(
"SELECT csv, fbv, file, prewhere, returns, script, settings, verify FROM mytable",
true);
Expand All @@ -87,8 +277,8 @@
}

@Test
public void testClickHouseClausesAfterTable() throws JSQLParserException {

Check failure on line 280 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

';' expected

Check failure on line 280 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

not a statement

Check failure on line 280 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

';' expected

Check failure on line 280 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

';' expected

Check failure on line 280 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

not a statement

Check failure on line 280 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

';' expected
assertSqlCanBeParsedAndDeparsed("SELECT a FROM mytable PREWHERE b = 1 WHERE c = 2", true);
assertSqlCanBeParsedAndDeparsed("SELECT a FROM mytable SETTINGS max_threads = 8", true);
}
}

Check failure on line 284 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (ubuntu-latest)

reached end of file while parsing

Check failure on line 284 in src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java

View workflow job for this annotation

GitHub Actions / Gradle Check (macos-latest)

reached end of file while parsing
Loading