diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d..b21a15f54 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1265,6 +1265,17 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Both named-parameter forms match only a single-token name (or K_OUTER) directly followed + * by their assignment token; the 2-token lookahead fails for anything else, so this first + * check is equivalent to running that scan. + */ + private boolean isNamedParameterAssignmentAhead() { + int kind = getToken(2).kind; + return kind == K_ORACLE_NAMED_PARAMETER_ASSIGNMENT + || kind == K_POSTGRES_NAMED_PARAMETER_ASSIGNMENT; + } + /** Distinguish EXISTS identifiers from predicates before suffixes and clause boundaries. */ private boolean isExistsIdentifierAhead() { if (getToken(1).kind != K_EXISTS) { @@ -11048,7 +11059,11 @@ Expression Condition(): LOOKAHEAD(2, { !isExistsIdentifierAhead() }) result=ExistsExpression() | - [ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] + // last alternative of the choice: entering it and letting the productions + // themselves reject is equivalent to scanning first, so skip the deep + // lookahead simulation (this path runs for every plain condition) + LOOKAHEAD({ !interrupted }) + [ LOOKAHEAD({ getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] left=SimpleExpression() { result = left; } // Consume Oracle (+) once, before dispatching @@ -11198,10 +11213,13 @@ Expression SQLCondition(): | LOOKAHEAD(2, { !isExistsIdentifierAhead() }) result=ExistsExpression() - | left = SimpleExpression() { result = left; } + | + // last alternative of the choice: skip the redundant lookahead simulation + LOOKAHEAD({ !interrupted }) + left = SimpleExpression() { result = left; } [ LOOKAHEAD(2) ( - LOOKAHEAD(2, ) result=OverlapsCondition(left) + LOOKAHEAD({ getToken(1).kind == K_OVERLAPS }) result=OverlapsCondition(left) | LOOKAHEAD(3, {!interrupted}) result=InExpression(left) | @@ -11663,9 +11681,9 @@ ExpressionList ComplexExpressionList(): } { ( - LOOKAHEAD(2) expr=OracleNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter() | - LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter() | expr=Expression() ) @@ -11676,9 +11694,9 @@ ExpressionList ComplexExpressionList(): ( LOOKAHEAD(2, {!interrupted}) "," ( - LOOKAHEAD(2) expr=OracleNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=OracleNamedFunctionParameter() | - LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + LOOKAHEAD(2, { isNamedParameterAssignmentAhead() }) expr=PostgresNamedFunctionParameter() | LOOKAHEAD( RelObjectName() "->", { !isJsonArrowDialect() } ) expr=LambdaExpression() | @@ -12094,7 +12112,8 @@ Expression PrimaryExpression() #PrimaryExpression: //| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns() // support timestamp expressions - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new TimeKeyExpression(token.image); } + | LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TIME_KEY_EXPR || getToken(1).kind == K_CURRENT) }) + (token= | token=) { retval = new TimeKeyExpression(token.image); } | LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression() @@ -12114,7 +12133,7 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, { !interrupted && getToken(1).kind == K_KEY && getToken(2).kind != OPENING_BRACKET }) retval=KeyExpression() - | LOOKAHEAD(2, {!interrupted}) { retval = new AllValue(); } + | LOOKAHEAD({ !interrupted && getToken(1).kind == K_ALL }) { retval = new AllValue(); } | LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() }) retval=CharacterSetIntroducedString() @@ -12128,7 +12147,8 @@ Expression PrimaryExpression() #PrimaryExpression: "(" "+" ")" { ((Column) retval).setOldOracleJoinSyntax(EqualsTo.ORACLE_JOIN_RIGHT); } ] - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new BooleanValue(token.image); } + | LOOKAHEAD({ !interrupted && (getToken(1).kind == K_TRUE || getToken(1).kind == K_FALSE) }) + (token= | token=) { retval = new BooleanValue(token.image); } | retval=CharacterLiteral() { linkAST(retval,jjtThis); } diff --git a/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java b/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java new file mode 100644 index 000000000..0aa5ee61d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LookaheadDispatchTest.java @@ -0,0 +1,223 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AllValue; +import net.sf.jsqlparser.expression.BooleanValue; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.OracleNamedFunctionParameter; +import net.sf.jsqlparser.expression.OverlapsCondition; +import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; +import net.sf.jsqlparser.expression.TimeKeyExpression; +import net.sf.jsqlparser.expression.operators.relational.Between; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; +import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class LookaheadDispatchTest { + @ParameterizedTest + @MethodSource("singleTokenExpressions") + void preservesSingleTokenExpressionKinds(String sql, Class type, + String deparsed) throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + Expression expression = parser.PrimaryExpression(); + assertInstanceOf(type, expression); + assertEquals(deparsed, expression.toString()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + Expression parsed = CCJSqlParserUtil.parseExpression(sql, false); + assertInstanceOf(type, parsed); + assertEquals(deparsed, parsed.toString()); + } + + static Stream singleTokenExpressions() { + return Stream.of( + Arguments.of("TRUE", BooleanValue.class, "true"), + Arguments.of("FALSE", BooleanValue.class, "false"), + Arguments.of("ALL", AllValue.class, "ALL"), + Arguments.of("CURRENT", TimeKeyExpression.class, "CURRENT"), + Arguments.of("CURRENT_DATE", TimeKeyExpression.class, "CURRENT_DATE"), + Arguments.of("CURRENT_TIME", TimeKeyExpression.class, "CURRENT_TIME"), + Arguments.of("CURRENT_TIMESTAMP", TimeKeyExpression.class, "CURRENT_TIMESTAMP"), + Arguments.of("CURRENT_TIMEZONE", TimeKeyExpression.class, "CURRENT_TIMEZONE"), + Arguments.of("CURRENT_DATE()", TimeKeyExpression.class, "CURRENT_DATE()")); + } + + @ParameterizedTest + @ValueSource(strings = {"\"TRUE\"", "\"CURRENT_DATE\"", "\"ALL\""}) + void preservesQuotedNames(String name) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT " + name); + assertEquals(name, assertInstanceOf(Column.class, select.getSelectItem(0).getExpression()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"TRUE", "ALL", "CURRENT_DATE"}) + void honorsInterruptionBeforeSingleTokenExpressions(String sql) { + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + parser.interrupted = true; + assertThrows(ParseException.class, parser::PrimaryExpression); + assertEquals(sql, parser.getNextToken().image); + } + + @ParameterizedTest + @MethodSource("conditions") + void preservesConditionAlternatives(String sql, Class type) + throws Exception { + CCJSqlParser conditionParser = CCJSqlParserUtil.newParser(sql); + Expression condition = conditionParser.Condition(); + assertInstanceOf(type, condition); + assertEquals(sql, condition.toString()); + assertEquals(CCJSqlParserConstants.EOF, conditionParser.getNextToken().kind); + + CCJSqlParser sqlConditionParser = CCJSqlParserUtil.newParser(sql); + Expression sqlCondition = sqlConditionParser.SQLCondition(); + assertInstanceOf(type, sqlCondition); + assertEquals(sql, sqlCondition.toString()); + assertEquals(CCJSqlParserConstants.EOF, sqlConditionParser.getNextToken().kind); + + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE " + sql); + assertInstanceOf(type, select.getWhere()); + } + + static Stream conditions() { + return Stream.of( + Arguments.of("a", Column.class), + Arguments.of("exists", Column.class), + Arguments.of("exists.col", Column.class), + Arguments.of("EXISTS (SELECT 1)", ExistsExpression.class), + Arguments.of("EXISTS (1)", ExistsExpression.class), + Arguments.of("a IN (1, 2)", InExpression.class), + Arguments.of("a BETWEEN 1 AND 2", Between.class), + Arguments.of("a IS NULL", IsNullExpression.class), + Arguments.of("(a, b) OVERLAPS (c, d)", OverlapsCondition.class)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "exists", "exists.col"}) + void preservesComparisons(String column) throws Exception { + String sql = column + " = 1"; + CCJSqlParser parser = CCJSqlParserUtil.newParser(sql); + EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition()); + assertEquals(column, assertInstanceOf(Column.class, condition.getLeftExpression()) + .getFullyQualifiedName()); + assertEquals(sql, condition.toString()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + sql); + } + + @Test + void preservesOraclePriorPosition() throws Exception { + CCJSqlParser parser = CCJSqlParserUtil.newParser("PRIOR a = b"); + EqualsTo condition = assertInstanceOf(EqualsTo.class, parser.Condition()); + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + condition.getOraclePriorPosition()); + assertEquals("a", assertInstanceOf(Column.class, condition.getLeftExpression()) + .getColumnName()); + assertEquals("b", assertInstanceOf(Column.class, condition.getRightExpression()) + .getColumnName()); + assertEquals(CCJSqlParserConstants.EOF, parser.getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t CONNECT BY PRIOR a = b"); + } + + @ParameterizedTest + @ValueSource(strings = {"a IN (", "a BETWEEN 1 AND", "EXISTS (SELECT", + "(a, b) OVERLAPS ("}) + void rejectsMalformedConditions(String sql) { + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition()); + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).SQLCondition()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"a =", "exists ="}) + void rejectsMalformedComparisons(String sql) { + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Condition()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE " + sql)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void honorsInterruptionBeforeConditionDispatch(boolean sqlCondition) { + CCJSqlParser parser = CCJSqlParserUtil.newParser("1"); + parser.interrupted = true; + assertThrows(ParseException.class, () -> { + if (sqlCondition) { + parser.SQLCondition(); + } else { + parser.Condition(); + } + }); + assertEquals("1", parser.getNextToken().image); + } + + @ParameterizedTest + @CsvSource(value = {"arg|=>", "arg|:=", "OUTER|=>", "OUTER|:=", "\"arg\"|=>", + "\"arg\"|:=", "overlaps|=>", "overlaps|:="}, delimiter = '|') + void preservesNamedParameterKindsInEveryArgumentPosition(String name, String assignment) + throws JSQLParserException { + for (String prefix : new String[] {"", "a.b, "}) { + String sql = "SELECT f(" + prefix + name + " " + assignment + " value_col)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql); + Function function = assertInstanceOf(Function.class, + select.getSelectItem(0).getExpression()); + int index = prefix.isEmpty() ? 0 : 1; + assertEquals(index + 1, function.getParameters().size()); + if (index == 1) { + assertEquals("a.b", assertInstanceOf(Column.class, + function.getParameters().get(0)).getFullyQualifiedName()); + } + Expression argument = function.getParameters().get(index); + Expression value; + if ("=>".equals(assignment)) { + OracleNamedFunctionParameter parameter = + assertInstanceOf(OracleNamedFunctionParameter.class, argument); + assertEquals(name, parameter.getName()); + value = parameter.getExpression(); + } else { + PostgresNamedFunctionParameter parameter = + assertInstanceOf(PostgresNamedFunctionParameter.class, argument); + assertEquals(name, parameter.getName()); + value = parameter.getExpression(); + } + assertEquals("value_col", assertInstanceOf(Column.class, value).getColumnName()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"a.b => 1", "a.b := 1", "1 => 1", "1 := 1", "'a' => 1", + "'a' := 1", "arg =>", "arg :="}) + void rejectsInvalidNamedParametersInEveryArgumentPosition(String argument) { + for (String prefix : new String[] {"", "a.b, "}) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT f(" + prefix + argument + ")")); + } + } +}