Skip to content

Commit 678414f

Browse files
committed
fix: parse truth tests after predicates
1 parent 25752d0 commit 678414f

2 files changed

Lines changed: 160 additions & 3 deletions

File tree

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,6 +2177,18 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
21772177
|| getToken(1).beginLine > getToken(0).endLine;
21782178
}
21792179

2180+
/**
2181+
* Checks for IS [NOT] TRUE/FALSE/UNKNOWN using at most three tokens.
2182+
*/
2183+
protected boolean isTruthValueTestAhead() {
2184+
if (getToken(1).kind != K_IS) {
2185+
return false;
2186+
}
2187+
int offset = getToken(2).kind == K_NOT ? 3 : 2;
2188+
int kind = getToken(offset).kind;
2189+
return kind == K_TRUE || kind == K_FALSE || kind == K_UNKNOWN;
2190+
}
2191+
21802192
/**
21812193
* Checks if the next token can start a condition suffix
21822194
* (comparison, IN, BETWEEN, LIKE, IS NULL, etc.)
@@ -2195,6 +2207,9 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
21952207
if ("?".equals(t.image) && isTernaryAhead()) return false;
21962208
if (isComparisonOperator(t)) return true;
21972209
switch (t.kind) {
2210+
case K_IS:
2211+
// Truth tests are consumed after the complete predicate in Condition().
2212+
return !isTruthValueTestAhead();
21982213
// Each suffix's start token:
21992214
case K_OVERLAPS: // OVERLAPS
22002215
case K_IN: // IN
@@ -2203,7 +2218,6 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
22032218
case K_INCLUDES: // INCLUDES (...)
22042219
case K_BETWEEN: // BETWEEN
22052220
case K_MEMBER: // MEMBER OF
2206-
case K_IS: // IS [NOT] NULL / TRUE / FALSE / UNKNOWN / DISTINCT
22072221
case K_ISNULL: // ISNULL
22082222
case K_NOTNULL: // NOTNULL
22092223
case K_LIKE: // LIKE
@@ -10482,15 +10496,18 @@ Expression Condition():
1048210496
| LOOKAHEAD(2) result=Between(left)
1048310497
| LOOKAHEAD(2) result = MemberOfExpression(left)
1048410498
| LOOKAHEAD(3) result=IsNullExpression(left)
10485-
| LOOKAHEAD(3) result=IsBooleanExpression(left)
1048610499
| LOOKAHEAD(3) result=IsJsonExpression(left)
10487-
| LOOKAHEAD(3) result=IsUnknownExpression(left)
1048810500
| LOOKAHEAD(2) result=LikeExpression(left)
1048910501
| LOOKAHEAD(3) result=IsDistinctExpression(left)
1049010502
| result=SimilarToExpression(left)
1049110503
)
1049210504
]
1049310505
)
10506+
// Truth tests wrap the complete predicate and bind before leading NOT.
10507+
[
10508+
LOOKAHEAD({ getToken(1).kind == K_IS && isTruthValueTestAhead() })
10509+
result=TruthValueTest(result)
10510+
]
1049410511
{
1049510512
if (oraclePrior == EqualsTo.ORACLE_PRIOR_START
1049610513
&& result instanceof SupportsOldOracleJoinSyntax) {
@@ -10862,6 +10879,23 @@ Expression IsNullExpression(Expression leftExpression):
1086210879
}
1086310880
}
1086410881

10882+
Expression TruthValueTest(Expression leftExpression):
10883+
{
10884+
Token truthValue;
10885+
boolean not = false;
10886+
}
10887+
{
10888+
<K_IS> [<K_NOT> { not = true; }]
10889+
(truthValue=<K_TRUE> | truthValue=<K_FALSE> | truthValue=<K_UNKNOWN>)
10890+
{
10891+
if (truthValue.kind == K_UNKNOWN) {
10892+
return new IsUnknownExpression().withLeftExpression(leftExpression).withNot(not);
10893+
}
10894+
return new IsBooleanExpression().withLeftExpression(leftExpression).withNot(not)
10895+
.withIsTrue(truthValue.kind == K_TRUE);
10896+
}
10897+
}
10898+
1086510899
Expression IsBooleanExpression(Expression leftExpression):
1086610900
{
1086710901
IsBooleanExpression result = new IsBooleanExpression();
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression.operators.relational;
11+
12+
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.expression.NotExpression;
15+
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
16+
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
17+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
18+
import net.sf.jsqlparser.statement.select.PlainSelect;
19+
import net.sf.jsqlparser.test.TestUtils;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.CsvSource;
23+
import org.junit.jupiter.params.provider.ValueSource;
24+
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
27+
import static org.junit.jupiter.api.Assertions.assertThrows;
28+
29+
class TruthValuePredicateTest {
30+
31+
@ParameterizedTest
32+
@CsvSource(delimiter = '|', value = {
33+
"a > 0 IS TRUE | false | true",
34+
"a IN (1, 2) IS FALSE | false | false",
35+
"a IS NULL IS NOT TRUE | true | true",
36+
"a > 0 IS NOT FALSE | true | false",
37+
"a BETWEEN 1 AND 2 IS TRUE | false | true",
38+
"a LIKE 'x%' IS FALSE | false | false",
39+
"EXISTS (SELECT 1 FROM t) IS TRUE | false | true",
40+
"a IS TRUE | false | true",
41+
"a IS FALSE | false | false",
42+
"a IS NOT TRUE | true | true",
43+
"a IS NOT FALSE | true | false",
44+
"(a > 0) IS TRUE | false | true"
45+
})
46+
void booleanTestWrapsPredicate(String condition, boolean not, boolean isTrue)
47+
throws JSQLParserException {
48+
PlainSelect select = (PlainSelect) TestUtils
49+
.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + condition);
50+
IsBooleanExpression expression =
51+
assertInstanceOf(IsBooleanExpression.class, select.getWhere());
52+
assertEquals(not, expression.isNot());
53+
assertEquals(isTrue, expression.isTrue());
54+
assertEquals(condition.substring(0, condition.lastIndexOf(" IS ")),
55+
expression.getLeftExpression().toString());
56+
}
57+
58+
@ParameterizedTest
59+
@CsvSource(delimiter = '|', value = {
60+
"a > 0 IS UNKNOWN | false",
61+
"a IN (1, 2) IS NOT UNKNOWN | true",
62+
"a IS NULL IS UNKNOWN | false",
63+
"a IS UNKNOWN | false",
64+
"a IS NOT UNKNOWN | true"
65+
})
66+
void unknownTestWrapsPredicate(String condition, boolean not) throws JSQLParserException {
67+
PlainSelect select = (PlainSelect) TestUtils
68+
.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + condition);
69+
IsUnknownExpression expression =
70+
assertInstanceOf(IsUnknownExpression.class, select.getWhere());
71+
assertEquals(not, expression.isNot());
72+
assertEquals(condition.substring(0, condition.lastIndexOf(" IS ")),
73+
expression.getLeftExpression().toString());
74+
}
75+
76+
@Test
77+
void truthValueTestBindsBeforeLogicalOperators() throws JSQLParserException {
78+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
79+
"SELECT * FROM t WHERE a > 0 IS TRUE AND b IN (1, 2) IS FALSE OR c IS NULL");
80+
OrExpression or = assertInstanceOf(OrExpression.class, select.getWhere());
81+
AndExpression and = assertInstanceOf(AndExpression.class, or.getLeftExpression());
82+
IsBooleanExpression left =
83+
assertInstanceOf(IsBooleanExpression.class, and.getLeftExpression());
84+
assertInstanceOf(GreaterThan.class, left.getLeftExpression());
85+
IsBooleanExpression right =
86+
assertInstanceOf(IsBooleanExpression.class, and.getRightExpression());
87+
assertInstanceOf(InExpression.class, right.getLeftExpression());
88+
assertInstanceOf(IsNullExpression.class, or.getRightExpression());
89+
}
90+
91+
@Test
92+
void leadingNotWrapsTruthValueTest() throws JSQLParserException {
93+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
94+
"SELECT * FROM t WHERE NOT a > 0 IS TRUE");
95+
NotExpression not = assertInstanceOf(NotExpression.class, select.getWhere());
96+
IsBooleanExpression expression =
97+
assertInstanceOf(IsBooleanExpression.class, not.getExpression());
98+
assertInstanceOf(GreaterThan.class, expression.getLeftExpression());
99+
}
100+
101+
@Test
102+
void selectItemAcceptsTruthValueTest() throws JSQLParserException {
103+
PlainSelect select = (PlainSelect) TestUtils
104+
.assertSqlCanBeParsedAndDeparsed("SELECT a > 0 IS TRUE FROM t");
105+
Expression expression = select.getSelectItem(0).getExpression();
106+
IsBooleanExpression predicate = assertInstanceOf(IsBooleanExpression.class, expression);
107+
assertInstanceOf(GreaterThan.class, predicate.getLeftExpression());
108+
}
109+
110+
@ParameterizedTest
111+
@ValueSource(strings = {"a > 0 IS", "a > 0 IS NOT", "a > 0 IS MAYBE"})
112+
void invalidTruthValueTestIsRejected(String condition) {
113+
assertThrows(JSQLParserException.class, () -> TestUtils
114+
.assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE " + condition));
115+
}
116+
117+
@ParameterizedTest
118+
@ValueSource(strings = {"a > 0 IS MAYBE", "a > 0 IS NULL"})
119+
void partialParsingLeavesUnrecognizedSuffix(String condition) throws JSQLParserException {
120+
assertEquals("a > 0", CCJSqlParserUtil.parseExpression(condition).toString());
121+
assertEquals("a > 0", CCJSqlParserUtil.parseCondExpression(condition).toString());
122+
}
123+
}

0 commit comments

Comments
 (0)