Add support for IS TRUE|FALSE|EMPTY and CONTAINS|INCLUDES|INTERSECTS HQL predicates.

Closes #3628
This commit is contained in:
Mark Paluch
2024-09-30 13:46:09 +02:00
parent 41a7f9b443
commit 3eb3353091
3 changed files with 187 additions and 78 deletions

View File

@@ -571,13 +571,15 @@ pathContinutation
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-conditional-expressions
predicate
: '(' predicate ')' # GroupedPredicate
| dealingWithNullExpression # NullExpressionPredicate
| expression IS NOT? (NULL|EMPTY|TRUE|FALSE) # IsBooleanPredicate
| expression IS NOT? DISTINCT FROM expression # IsDistinctFromPredicate
| expression NOT? MEMBER OF? path # MemberOfPredicate
| inExpression # InPredicate
| betweenExpression # BetweenPredicate
| expression NOT? (CONTAINS|INCLUDES|INTERSECTS) expression # ContainsPredicate
| relationalExpression # RelationalPredicate
| stringPatternMatching # LikePredicate
| existsExpression # ExistsPredicate
| collectionExpression # CollectionPredicate
| NOT predicate # NotPredicate
| predicate AND predicate # AndPredicate
| predicate OR predicate # OrPredicate
@@ -600,12 +602,6 @@ betweenExpression
: expression NOT? BETWEEN expression AND expression
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-null-predicate
dealingWithNullExpression
: expression IS NOT? NULL
| expression IS NOT? DISTINCT FROM expression
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-like-predicate
stringPatternMatching
: expression NOT? (LIKE | ILIKE) expression (ESCAPE (stringLiteral|parameter))?
@@ -632,12 +628,6 @@ existsExpression
| EXISTS expression
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-collection-operators
collectionExpression
: expression IS NOT? EMPTY
| expression NOT? MEMBER OF path
;
// Projection
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-select-new
instantiationTarget
@@ -707,6 +697,7 @@ reservedWord
| CASE
| CAST
| COLLATE
| CONTAINS
| COUNT
| CROSS
| CUBE
@@ -739,6 +730,7 @@ reservedWord
| EXISTS
| EXP
| EXTRACT
| FALSE
| FETCH
| FILTER
| FIRST
@@ -757,12 +749,14 @@ reservedWord
| IGNORE
| ILIKE
| IN
| INCLUDES
| INDEX
| INDICES
| INNER
| INSERT
| INSTANT
| INTERSECT
| INTERSECTS
| INTO
| IS
| JOIN
@@ -834,6 +828,7 @@ reservedWord
| SOME
| SUBSTRING
| SUM
| TRUE
| THEN
| TIES
| TIME
@@ -917,6 +912,7 @@ CASE : C A S E;
CAST : C A S T;
CEILING : C E I L I N G;
COLLATE : C O L L A T E;
CONTAINS : C O N T A I N S;
COUNT : C O U N T;
CROSS : C R O S S;
CUBE : C U B E;
@@ -969,12 +965,14 @@ ID : I D;
IGNORE : I G N O R E;
ILIKE : I L I K E;
IN : I N;
INCLUDES : I N C L U D E S;
INDEX : I N D E X;
INDICES : I N D I C E S;
INNER : I N N E R;
INSERT : I N S E R T;
INSTANT : I N S T A N T;
INTERSECT : I N T E R S E C T;
INTERSECTS : I N T E R S E C T S;
INTO : I N T O;
IS : I S;
JOIN : J O I N;

View File

@@ -2094,8 +2094,77 @@ class HqlQueryRenderer extends HqlBaseVisitor<List<JpaQueryParsingToken>> {
}
@Override
public List<JpaQueryParsingToken> visitNullExpressionPredicate(HqlParser.NullExpressionPredicateContext ctx) {
return visit(ctx.dealingWithNullExpression());
public List<JpaQueryParsingToken> visitIsBooleanPredicate(HqlParser.IsBooleanPredicateContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
tokens.addAll(visit(ctx.expression()));
tokens.add(new JpaQueryParsingToken(ctx.IS()));
if (ctx.NOT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
}
if (ctx.NULL() != null) {
tokens.add(new JpaQueryParsingToken(ctx.NULL()));
}
if (ctx.TRUE() != null) {
tokens.add(new JpaQueryParsingToken(ctx.TRUE()));
}
if (ctx.FALSE() != null) {
tokens.add(new JpaQueryParsingToken(ctx.FALSE()));
}
if (ctx.EMPTY() != null) {
tokens.add(new JpaQueryParsingToken(ctx.EMPTY()));
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitMemberOfPredicate(HqlParser.MemberOfPredicateContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
tokens.addAll(visit(ctx.expression()));
if (ctx.NOT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
}
if (ctx.MEMBER() != null) {
tokens.add(new JpaQueryParsingToken(ctx.MEMBER()));
}
if (ctx.OF() != null) {
tokens.add(new JpaQueryParsingToken(ctx.OF()));
}
tokens.addAll(visit(ctx.path()));
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitIsDistinctFromPredicate(HqlParser.IsDistinctFromPredicateContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>(16);
tokens.addAll(visit(ctx.expression(0)));
tokens.add(new JpaQueryParsingToken(ctx.IS()));
if (ctx.NOT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
}
if (ctx.DISTINCT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.DISTINCT()));
tokens.add(new JpaQueryParsingToken(ctx.FROM()));
tokens.addAll(visit(ctx.expression(1)));
}
return tokens;
}
@Override
@@ -2103,6 +2172,33 @@ class HqlQueryRenderer extends HqlBaseVisitor<List<JpaQueryParsingToken>> {
return visit(ctx.betweenExpression());
}
@Override
public List<JpaQueryParsingToken> visitContainsPredicate(HqlParser.ContainsPredicateContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
tokens.addAll(visit(ctx.expression(0)));
if (ctx.NOT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
}
if (ctx.CONTAINS() != null) {
tokens.add(new JpaQueryParsingToken(ctx.CONTAINS()));
}
if (ctx.INCLUDES() != null) {
tokens.add(new JpaQueryParsingToken(ctx.INCLUDES()));
}
if (ctx.INTERSECTS() != null) {
tokens.add(new JpaQueryParsingToken(ctx.INTERSECTS()));
}
tokens.addAll(visit(ctx.expression(1)));
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitOrPredicate(HqlParser.OrPredicateContext ctx) {
@@ -2125,11 +2221,6 @@ class HqlQueryRenderer extends HqlBaseVisitor<List<JpaQueryParsingToken>> {
return visit(ctx.existsExpression());
}
@Override
public List<JpaQueryParsingToken> visitCollectionPredicate(HqlParser.CollectionPredicateContext ctx) {
return visit(ctx.collectionExpression());
}
@Override
public List<JpaQueryParsingToken> visitAndPredicate(HqlParser.AndPredicateContext ctx) {
@@ -2224,30 +2315,6 @@ class HqlQueryRenderer extends HqlBaseVisitor<List<JpaQueryParsingToken>> {
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitDealingWithNullExpression(HqlParser.DealingWithNullExpressionContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
tokens.addAll(visit(ctx.expression(0)));
tokens.add(new JpaQueryParsingToken(ctx.IS()));
if (ctx.NOT() != null) {
tokens.add(TOKEN_NOT);
}
if (ctx.NULL() != null) {
tokens.add(new JpaQueryParsingToken(ctx.NULL()));
} else if (ctx.DISTINCT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.DISTINCT()));
tokens.add(new JpaQueryParsingToken(ctx.FROM()));
tokens.addAll(visit(ctx.expression(1)));
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitStringPatternMatching(HqlParser.StringPatternMatchingContext ctx) {
@@ -2368,36 +2435,6 @@ class HqlQueryRenderer extends HqlBaseVisitor<List<JpaQueryParsingToken>> {
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitCollectionExpression(HqlParser.CollectionExpressionContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
tokens.addAll(visit(ctx.expression()));
if (ctx.IS() != null) {
tokens.add(new JpaQueryParsingToken(ctx.IS()));
if (ctx.NOT() != null) {
tokens.add(TOKEN_NOT);
}
tokens.add(new JpaQueryParsingToken(ctx.EMPTY()));
} else if (ctx.MEMBER() != null) {
if (ctx.NOT() != null) {
tokens.add(TOKEN_NOT);
}
tokens.add(new JpaQueryParsingToken(ctx.MEMBER()));
tokens.add(new JpaQueryParsingToken(ctx.OF()));
tokens.addAll(visit(ctx.path()));
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitInstantiationTarget(HqlParser.InstantiationTargetContext ctx) {

View File

@@ -416,8 +416,7 @@ class HqlSpecificationTests {
* @see #functionInvocationExampleWithCorrection()
*/
@Test
@Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator")
void functionInvocationExample_SPEC_BUG() {
void functionInvocationExampleAsBooleanExpression() {
HqlQueryParser.parseQuery("""
SELECT c
@@ -436,6 +435,18 @@ class HqlSpecificationTests {
""");
}
@Test // GH-3628
void functionInvocationWithIsBoolean() {
assertQuery("""
from RoleTmpl where find_in_set(:appId, appIds) is true
""");
assertQuery("""
from RoleTmpl where find_in_set(:appId, appIds) is false
""");
}
@Test
void updateCaseExample1() {
@@ -751,7 +762,7 @@ class HqlSpecificationTests {
}
@Test
void theRest26() {
void collectionIsEmpty() {
HqlQueryParser.parseQuery("""
DELETE
@@ -759,6 +770,69 @@ class HqlSpecificationTests {
WHERE c.status = 'inactive'
AND c.orders IS EMPTY
""");
assertQuery("""
DELETE
FROM Customer c
WHERE c.status = 'inactive'
AND c.orders IS NOT EMPTY
""");
}
@Test // GH-3628
void booleanPredicate() {
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS TRUE
""");
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS NOT TRUE
""");
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS FALSE
""");
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS NOT FALSE
""");
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS NULL
""");
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS NOT NULL
""");
}
@Test // GH-3628
void distinctFromPredicate() {
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS DISTINCT FROM c.payments
""");
assertQuery("""
SELECT c
FROM Customer c
WHERE c.orders IS NOT DISTINCT FROM c.payments
""");
}
@Test