From 3eb3353091ed7ffcf35e6694ad96c510ca7c7946 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 30 Sep 2024 13:46:09 +0200 Subject: [PATCH] Add support for `IS TRUE|FALSE|EMPTY` and `CONTAINS|INCLUDES|INTERSECTS` HQL predicates. Closes #3628 --- .../data/jpa/repository/query/Hql.g4 | 26 ++- .../repository/query/HqlQueryRenderer.java | 159 +++++++++++------- .../query/HqlSpecificationTests.java | 80 ++++++++- 3 files changed, 187 insertions(+), 78 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 index 5edddf97d..2734420da 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 @@ -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; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 5b8c990fe..28fa868b1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -2094,8 +2094,77 @@ class HqlQueryRenderer extends HqlBaseVisitor> { } @Override - public List visitNullExpressionPredicate(HqlParser.NullExpressionPredicateContext ctx) { - return visit(ctx.dealingWithNullExpression()); + public List visitIsBooleanPredicate(HqlParser.IsBooleanPredicateContext ctx) { + + List 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 visitMemberOfPredicate(HqlParser.MemberOfPredicateContext ctx) { + + List 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 visitIsDistinctFromPredicate(HqlParser.IsDistinctFromPredicateContext ctx) { + + List 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> { return visit(ctx.betweenExpression()); } + @Override + public List visitContainsPredicate(HqlParser.ContainsPredicateContext ctx) { + + List 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 visitOrPredicate(HqlParser.OrPredicateContext ctx) { @@ -2125,11 +2221,6 @@ class HqlQueryRenderer extends HqlBaseVisitor> { return visit(ctx.existsExpression()); } - @Override - public List visitCollectionPredicate(HqlParser.CollectionPredicateContext ctx) { - return visit(ctx.collectionExpression()); - } - @Override public List visitAndPredicate(HqlParser.AndPredicateContext ctx) { @@ -2224,30 +2315,6 @@ class HqlQueryRenderer extends HqlBaseVisitor> { return tokens; } - @Override - public List visitDealingWithNullExpression(HqlParser.DealingWithNullExpressionContext ctx) { - - List 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 visitStringPatternMatching(HqlParser.StringPatternMatchingContext ctx) { @@ -2368,36 +2435,6 @@ class HqlQueryRenderer extends HqlBaseVisitor> { return tokens; } - @Override - public List visitCollectionExpression(HqlParser.CollectionExpressionContext ctx) { - - List 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 visitInstantiationTarget(HqlParser.InstantiationTargetContext ctx) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java index 3440616f4..d4946711f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java @@ -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