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 f02e46084a..674e67721a 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 @@ -51,9 +51,37 @@ selectStatement ; queryExpression - : orderedQuery (setOperator orderedQuery)* + : withClause? orderedQuery (setOperator orderedQuery)* ; +withClause + : WITH cte (',' cte)* + ; + +cte + : identifier AS (NOT? MATERIALIZED)? '(' queryExpression ')' searchClause? cycleClause? + ; + +searchClause + : SEARCH (BREADTH | DEPTH) FIRST BY searchSpecifications SET identifier + ; + +searchSpecifications + : searchSpecification (',' searchSpecification)* + ; + +searchSpecification + : identifier sortDirection? nullsPrecedence? + ; + +cycleClause + : CYCLE cteAttributes SET identifier (TO literal DEFAULT literal)? (USING identifier)? + ; + +cteAttributes + : identifier (',' identifier)* + ; + orderedQuery : (query | '(' queryExpression ')') queryOrder? ; 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 1adfa14216..39d6271fe3 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 @@ -59,6 +59,10 @@ public List visitQueryExpression(HqlParser.QueryExpression List tokens = new ArrayList<>(); + if (ctx.withClause() != null) { + tokens.addAll(visit(ctx.withClause())); + } + tokens.addAll(visit(ctx.orderedQuery(0))); for (int i = 1; i < ctx.orderedQuery().size(); i++) { @@ -70,6 +74,150 @@ public List visitQueryExpression(HqlParser.QueryExpression return tokens; } + @Override + public List visitWithClause(HqlParser.WithClauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(TOKEN_WITH); + + ctx.cte().forEach(cteContext -> { + + tokens.addAll(visit(cteContext)); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + + return tokens; + } + + @Override + public List visitCte(HqlParser.CteContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.identifier())); + tokens.add(TOKEN_AS); + NOSPACE(tokens); + + if (ctx.NOT() != null) { + tokens.add(TOKEN_NOT); + } + if (ctx.MATERIALIZED() != null) { + tokens.add(TOKEN_MATERIALIZED); + } + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.queryExpression())); + tokens.add(TOKEN_CLOSE_PAREN); + + if (ctx.searchClause() != null) { + tokens.addAll(visit(ctx.searchClause())); + } + if (ctx.cycleClause() != null) { + tokens.addAll(visit(ctx.cycleClause())); + } + + return tokens; + } + + @Override + public List visitSearchClause(HqlParser.SearchClauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.SEARCH().getText())); + + if (ctx.BREADTH() != null) { + tokens.add(new JpaQueryParsingToken(ctx.BREADTH().getText())); + } else if (ctx.DEPTH() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DEPTH().getText())); + } + + tokens.add(new JpaQueryParsingToken(ctx.FIRST().getText())); + tokens.add(new JpaQueryParsingToken(ctx.BY().getText())); + tokens.addAll(visit(ctx.searchSpecifications())); + tokens.add(new JpaQueryParsingToken(ctx.SET().getText())); + tokens.addAll(visit(ctx.identifier())); + + return tokens; + } + + @Override + public List visitSearchSpecifications(HqlParser.SearchSpecificationsContext ctx) { + + List tokens = new ArrayList<>(); + + ctx.searchSpecification().forEach(searchSpecificationContext -> { + + tokens.addAll(visit(searchSpecificationContext)); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + + return tokens; + } + + @Override + public List visitSearchSpecification(HqlParser.SearchSpecificationContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.identifier())); + + if (ctx.sortDirection() != null) { + tokens.addAll(visit(ctx.sortDirection())); + } + + if (ctx.nullsPrecedence() != null) { + tokens.addAll(visit(ctx.nullsPrecedence())); + } + + return tokens; + } + + @Override + public List visitCycleClause(HqlParser.CycleClauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.CYCLE().getText())); + tokens.addAll(visit(ctx.cteAttributes())); + tokens.add(new JpaQueryParsingToken(ctx.SET().getText())); + tokens.addAll(visit(ctx.identifier(0))); + + if (ctx.TO() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.TO().getText())); + tokens.addAll(visit(ctx.literal(0))); + tokens.add(new JpaQueryParsingToken(ctx.DEFAULT().getText())); + tokens.addAll(visit(ctx.literal(1))); + } + + if (ctx.USING() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.USING().getText())); + tokens.addAll(visit(ctx.identifier(1))); + } + + return tokens; + } + + @Override + public List visitCteAttributes(HqlParser.CteAttributesContext ctx) { + + List tokens = new ArrayList<>(); + + ctx.identifier().forEach(identifierContext -> { + + tokens.addAll(visit(identifierContext)); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + + return tokens; + } + @Override public List visitOrderedQuery(HqlParser.OrderedQueryContext ctx) { @@ -1876,7 +2024,7 @@ public List visitNotPredicate(HqlParser.NotPredicateContex List tokens = new ArrayList<>(); - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); tokens.addAll(visit(ctx.predicate())); return tokens; @@ -1919,7 +2067,7 @@ public List visitBetweenExpression(HqlParser.BetweenExpres tokens.addAll(visit(ctx.expression(0))); if (ctx.NOT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); } tokens.add(new JpaQueryParsingToken(ctx.BETWEEN())); @@ -1939,7 +2087,7 @@ public List visitDealingWithNullExpression(HqlParser.Deali tokens.add(new JpaQueryParsingToken(ctx.IS())); if (ctx.NOT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); } if (ctx.NULL() != null) { @@ -1962,7 +2110,7 @@ public List visitStringPatternMatching(HqlParser.StringPat tokens.addAll(visit(ctx.expression(0))); if (ctx.NOT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); } if (ctx.LIKE() != null) { @@ -1990,7 +2138,7 @@ public List visitInExpression(HqlParser.InExpressionContex tokens.addAll(visit(ctx.expression())); if (ctx.NOT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); } tokens.add(new JpaQueryParsingToken(ctx.IN())); @@ -2081,14 +2229,14 @@ public List visitCollectionExpression(HqlParser.Collection tokens.add(new JpaQueryParsingToken(ctx.IS())); if (ctx.NOT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); } tokens.add(new JpaQueryParsingToken(ctx.EMPTY())); } else if (ctx.MEMBER() != null) { if (ctx.NOT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.NOT())); + tokens.add(TOKEN_NOT); } tokens.add(new JpaQueryParsingToken(ctx.MEMBER())); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java index e4ca63df2e..59c409e9e3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java @@ -59,6 +59,13 @@ class JpaQueryParsingToken { public static final JpaQueryParsingToken TOKEN_DESC = new JpaQueryParsingToken("desc", false); public static final JpaQueryParsingToken TOKEN_ASC = new JpaQueryParsingToken("asc", false); + + public static final JpaQueryParsingToken TOKEN_WITH = new JpaQueryParsingToken("WITH"); + + public static final JpaQueryParsingToken TOKEN_NOT = new JpaQueryParsingToken("NOT"); + + public static final JpaQueryParsingToken TOKEN_MATERIALIZED = new JpaQueryParsingToken("materialized"); + /** * The text value of the token. */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 6cb62adae3..d3a9f83264 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1487,4 +1487,16 @@ select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc """); }); } + + @Test // GH-2981 + void cteWithClauseShouldWork() { + + assertQuery(""" + WITH maxId AS(select max(sr.snapshot.id) snapshotId from SnapshotReference sr + where sr.id.selectionId = ?1 and sr.enabled + group by sr.userId + ) + select sr from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId + """); + } }