From 25fc1d545846d7542d9a728bc3439fc7ba3606c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Sun, 11 Oct 2026 20:50:39 +0800 Subject: [PATCH] fix: support EXISTS identifiers without breaking predicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 53 ++++- .../sf/jsqlparser/statement/KeywordsTest.java | 196 +++++++++++++++++- 2 files changed, 241 insertions(+), 8 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6a82cb3bb..5d08bb7a0 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1218,6 +1218,31 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** 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 @@ -2200,7 +2225,11 @@ public class CCJSqlParser extends AbstractJSqlParser { // 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 @@ -2228,10 +2257,10 @@ public class CCJSqlParser extends AbstractJSqlParser { 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; } @@ -2571,6 +2600,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2893,7 +2923,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | /* Salesforce SOQL */ -| | | | @@ -10458,6 +10487,12 @@ Expression Condition(): ( { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } ) )* ( + LOOKAHEAD( 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 }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] @@ -10602,8 +10637,14 @@ Expression SQLCondition(): } { ( - result=ExistsExpression() - | left = SimpleExpression() { result = left; } + LOOKAHEAD( 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, ) result=OverlapsCondition(left) diff --git a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java index 9e2c14ce7..7b5469921 100644 --- a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java @@ -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, @@ -62,4 +76,182 @@ public void testGroupsAsIdentifier() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT a groups FROM mytable", true); 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()); + } }