From 678414fb87ae4050ec52fa946af01ee7c028d7e2 Mon Sep 17 00:00:00 2001 From: Xujian66 <91264074+Xujian66@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:33:53 +0800 Subject: [PATCH] fix: parse truth tests after predicates --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 40 +++++- .../relational/TruthValuePredicateTest.java | 123 ++++++++++++++++++ 2 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/expression/operators/relational/TruthValuePredicateTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 44caab632..ca9f60426 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2177,6 +2177,18 @@ public class CCJSqlParser extends AbstractJSqlParser { || getToken(1).beginLine > getToken(0).endLine; } + /** + * Checks for IS [NOT] TRUE/FALSE/UNKNOWN using at most three tokens. + */ + protected boolean isTruthValueTestAhead() { + if (getToken(1).kind != K_IS) { + return false; + } + int offset = getToken(2).kind == K_NOT ? 3 : 2; + int kind = getToken(offset).kind; + return kind == K_TRUE || kind == K_FALSE || kind == K_UNKNOWN; + } + /** * Checks if the next token can start a condition suffix * (comparison, IN, BETWEEN, LIKE, IS NULL, etc.) @@ -2195,6 +2207,9 @@ public class CCJSqlParser extends AbstractJSqlParser { if ("?".equals(t.image) && isTernaryAhead()) return false; if (isComparisonOperator(t)) return true; switch (t.kind) { + case K_IS: + // Truth tests are consumed after the complete predicate in Condition(). + return !isTruthValueTestAhead(); // Each suffix's start token: case K_OVERLAPS: // OVERLAPS case K_IN: // IN @@ -2203,7 +2218,6 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_INCLUDES: // INCLUDES (...) case K_BETWEEN: // BETWEEN case K_MEMBER: // MEMBER OF - case K_IS: // IS [NOT] NULL / TRUE / FALSE / UNKNOWN / DISTINCT case K_ISNULL: // ISNULL case K_NOTNULL: // NOTNULL case K_LIKE: // LIKE @@ -10482,15 +10496,18 @@ Expression Condition(): | LOOKAHEAD(2) result=Between(left) | LOOKAHEAD(2) result = MemberOfExpression(left) | LOOKAHEAD(3) result=IsNullExpression(left) - | LOOKAHEAD(3) result=IsBooleanExpression(left) | LOOKAHEAD(3) result=IsJsonExpression(left) - | LOOKAHEAD(3) result=IsUnknownExpression(left) | LOOKAHEAD(2) result=LikeExpression(left) | LOOKAHEAD(3) result=IsDistinctExpression(left) | result=SimilarToExpression(left) ) ] ) + // Truth tests wrap the complete predicate and bind before leading NOT. + [ + LOOKAHEAD({ getToken(1).kind == K_IS && isTruthValueTestAhead() }) + result=TruthValueTest(result) + ] { if (oraclePrior == EqualsTo.ORACLE_PRIOR_START && result instanceof SupportsOldOracleJoinSyntax) { @@ -10862,6 +10879,23 @@ Expression IsNullExpression(Expression leftExpression): } } +Expression TruthValueTest(Expression leftExpression): +{ + Token truthValue; + boolean not = false; +} +{ + [ { not = true; }] + (truthValue= | truthValue= | truthValue=) + { + if (truthValue.kind == K_UNKNOWN) { + return new IsUnknownExpression().withLeftExpression(leftExpression).withNot(not); + } + return new IsBooleanExpression().withLeftExpression(leftExpression).withNot(not) + .withIsTrue(truthValue.kind == K_TRUE); + } +} + Expression IsBooleanExpression(Expression leftExpression): { IsBooleanExpression result = new IsBooleanExpression(); diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/TruthValuePredicateTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/TruthValuePredicateTest.java new file mode 100644 index 000000000..c484fd707 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/TruthValuePredicateTest.java @@ -0,0 +1,123 @@ +/*- + * #%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.expression.operators.relational; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.NotExpression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TruthValuePredicateTest { + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "a > 0 IS TRUE | false | true", + "a IN (1, 2) IS FALSE | false | false", + "a IS NULL IS NOT TRUE | true | true", + "a > 0 IS NOT FALSE | true | false", + "a BETWEEN 1 AND 2 IS TRUE | false | true", + "a LIKE 'x%' IS FALSE | false | false", + "EXISTS (SELECT 1 FROM t) IS TRUE | false | true", + "a IS TRUE | false | true", + "a IS FALSE | false | false", + "a IS NOT TRUE | true | true", + "a IS NOT FALSE | true | false", + "(a > 0) IS TRUE | false | true" + }) + void booleanTestWrapsPredicate(String condition, boolean not, boolean isTrue) + throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + condition); + IsBooleanExpression expression = + assertInstanceOf(IsBooleanExpression.class, select.getWhere()); + assertEquals(not, expression.isNot()); + assertEquals(isTrue, expression.isTrue()); + assertEquals(condition.substring(0, condition.lastIndexOf(" IS ")), + expression.getLeftExpression().toString()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "a > 0 IS UNKNOWN | false", + "a IN (1, 2) IS NOT UNKNOWN | true", + "a IS NULL IS UNKNOWN | false", + "a IS UNKNOWN | false", + "a IS NOT UNKNOWN | true" + }) + void unknownTestWrapsPredicate(String condition, boolean not) throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + condition); + IsUnknownExpression expression = + assertInstanceOf(IsUnknownExpression.class, select.getWhere()); + assertEquals(not, expression.isNot()); + assertEquals(condition.substring(0, condition.lastIndexOf(" IS ")), + expression.getLeftExpression().toString()); + } + + @Test + void truthValueTestBindsBeforeLogicalOperators() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE a > 0 IS TRUE AND b IN (1, 2) IS FALSE OR c IS NULL"); + OrExpression or = assertInstanceOf(OrExpression.class, select.getWhere()); + AndExpression and = assertInstanceOf(AndExpression.class, or.getLeftExpression()); + IsBooleanExpression left = + assertInstanceOf(IsBooleanExpression.class, and.getLeftExpression()); + assertInstanceOf(GreaterThan.class, left.getLeftExpression()); + IsBooleanExpression right = + assertInstanceOf(IsBooleanExpression.class, and.getRightExpression()); + assertInstanceOf(InExpression.class, right.getLeftExpression()); + assertInstanceOf(IsNullExpression.class, or.getRightExpression()); + } + + @Test + void leadingNotWrapsTruthValueTest() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE NOT a > 0 IS TRUE"); + NotExpression not = assertInstanceOf(NotExpression.class, select.getWhere()); + IsBooleanExpression expression = + assertInstanceOf(IsBooleanExpression.class, not.getExpression()); + assertInstanceOf(GreaterThan.class, expression.getLeftExpression()); + } + + @Test + void selectItemAcceptsTruthValueTest() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT a > 0 IS TRUE FROM t"); + Expression expression = select.getSelectItem(0).getExpression(); + IsBooleanExpression predicate = assertInstanceOf(IsBooleanExpression.class, expression); + assertInstanceOf(GreaterThan.class, predicate.getLeftExpression()); + } + + @ParameterizedTest + @ValueSource(strings = {"a > 0 IS", "a > 0 IS NOT", "a > 0 IS MAYBE"}) + void invalidTruthValueTestIsRejected(String condition) { + assertThrows(JSQLParserException.class, () -> TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + condition)); + } + + @ParameterizedTest + @ValueSource(strings = {"a > 0 IS MAYBE", "a > 0 IS NULL"}) + void partialParsingLeavesUnrecognizedSuffix(String condition) throws JSQLParserException { + assertEquals("a > 0", CCJSqlParserUtil.parseExpression(condition).toString()); + assertEquals("a > 0", CCJSqlParserUtil.parseCondExpression(condition).toString()); + } +}