Skip to content
Open
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
40 changes: 37 additions & 3 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -2177,6 +2177,18 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
|| 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.)
Expand All @@ -2195,6 +2207,9 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
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
Expand All @@ -2203,7 +2218,6 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -10862,6 +10879,23 @@ Expression IsNullExpression(Expression leftExpression):
}
}

Expression TruthValueTest(Expression leftExpression):
{
Token truthValue;
boolean not = false;
}
{
<K_IS> [<K_NOT> { not = true; }]
(truthValue=<K_TRUE> | truthValue=<K_FALSE> | truthValue=<K_UNKNOWN>)
{
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading