Skip to content

Commit 3817e38

Browse files
committed
Support Common Table Expressions in HQL parser.
See #2981.
1 parent 8e3c52c commit 3817e38

File tree

4 files changed

+203
-8
lines changed

4 files changed

+203
-8
lines changed

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4

+29-1
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,37 @@ selectStatement
5151
;
5252

5353
queryExpression
54-
: orderedQuery (setOperator orderedQuery)*
54+
: withClause? orderedQuery (setOperator orderedQuery)*
5555
;
5656

57+
withClause
58+
: WITH cte (',' cte)*
59+
;
60+
61+
cte
62+
: identifier AS (NOT? MATERIALIZED)? '(' queryExpression ')' searchClause? cycleClause?
63+
;
64+
65+
searchClause
66+
: SEARCH (BREADTH | DEPTH) FIRST BY searchSpecifications SET identifier
67+
;
68+
69+
searchSpecifications
70+
: searchSpecification (',' searchSpecification)*
71+
;
72+
73+
searchSpecification
74+
: identifier sortDirection? nullsPrecedence?
75+
;
76+
77+
cycleClause
78+
: CYCLE cteAttributes SET identifier (TO literal DEFAULT literal)? (USING identifier)?
79+
;
80+
81+
cteAttributes
82+
: identifier (',' identifier)*
83+
;
84+
5785
orderedQuery
5886
: (query | '(' queryExpression ')') queryOrder?
5987
;

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java

+155-7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ public List<JpaQueryParsingToken> visitQueryExpression(HqlParser.QueryExpression
5959

6060
List<JpaQueryParsingToken> tokens = new ArrayList<>();
6161

62+
if (ctx.withClause() != null) {
63+
tokens.addAll(visit(ctx.withClause()));
64+
}
65+
6266
tokens.addAll(visit(ctx.orderedQuery(0)));
6367

6468
for (int i = 1; i < ctx.orderedQuery().size(); i++) {
@@ -70,6 +74,150 @@ public List<JpaQueryParsingToken> visitQueryExpression(HqlParser.QueryExpression
7074
return tokens;
7175
}
7276

77+
@Override
78+
public List<JpaQueryParsingToken> visitWithClause(HqlParser.WithClauseContext ctx) {
79+
80+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
81+
82+
tokens.add(TOKEN_WITH);
83+
84+
ctx.cte().forEach(cteContext -> {
85+
86+
tokens.addAll(visit(cteContext));
87+
tokens.add(TOKEN_COMMA);
88+
});
89+
CLIP(tokens);
90+
91+
return tokens;
92+
}
93+
94+
@Override
95+
public List<JpaQueryParsingToken> visitCte(HqlParser.CteContext ctx) {
96+
97+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
98+
99+
tokens.addAll(visit(ctx.identifier()));
100+
tokens.add(TOKEN_AS);
101+
NOSPACE(tokens);
102+
103+
if (ctx.NOT() != null) {
104+
tokens.add(TOKEN_NOT);
105+
}
106+
if (ctx.MATERIALIZED() != null) {
107+
tokens.add(TOKEN_MATERIALIZED);
108+
}
109+
110+
tokens.add(TOKEN_OPEN_PAREN);
111+
tokens.addAll(visit(ctx.queryExpression()));
112+
tokens.add(TOKEN_CLOSE_PAREN);
113+
114+
if (ctx.searchClause() != null) {
115+
tokens.addAll(visit(ctx.searchClause()));
116+
}
117+
if (ctx.cycleClause() != null) {
118+
tokens.addAll(visit(ctx.cycleClause()));
119+
}
120+
121+
return tokens;
122+
}
123+
124+
@Override
125+
public List<JpaQueryParsingToken> visitSearchClause(HqlParser.SearchClauseContext ctx) {
126+
127+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
128+
129+
tokens.add(new JpaQueryParsingToken(ctx.SEARCH().getText()));
130+
131+
if (ctx.BREADTH() != null) {
132+
tokens.add(new JpaQueryParsingToken(ctx.BREADTH().getText()));
133+
} else if (ctx.DEPTH() != null) {
134+
tokens.add(new JpaQueryParsingToken(ctx.DEPTH().getText()));
135+
}
136+
137+
tokens.add(new JpaQueryParsingToken(ctx.FIRST().getText()));
138+
tokens.add(new JpaQueryParsingToken(ctx.BY().getText()));
139+
tokens.addAll(visit(ctx.searchSpecifications()));
140+
tokens.add(new JpaQueryParsingToken(ctx.SET().getText()));
141+
tokens.addAll(visit(ctx.identifier()));
142+
143+
return tokens;
144+
}
145+
146+
@Override
147+
public List<JpaQueryParsingToken> visitSearchSpecifications(HqlParser.SearchSpecificationsContext ctx) {
148+
149+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
150+
151+
ctx.searchSpecification().forEach(searchSpecificationContext -> {
152+
153+
tokens.addAll(visit(searchSpecificationContext));
154+
tokens.add(TOKEN_COMMA);
155+
});
156+
CLIP(tokens);
157+
158+
return tokens;
159+
}
160+
161+
@Override
162+
public List<JpaQueryParsingToken> visitSearchSpecification(HqlParser.SearchSpecificationContext ctx) {
163+
164+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
165+
166+
tokens.addAll(visit(ctx.identifier()));
167+
168+
if (ctx.sortDirection() != null) {
169+
tokens.addAll(visit(ctx.sortDirection()));
170+
}
171+
172+
if (ctx.nullsPrecedence() != null) {
173+
tokens.addAll(visit(ctx.nullsPrecedence()));
174+
}
175+
176+
return tokens;
177+
}
178+
179+
@Override
180+
public List<JpaQueryParsingToken> visitCycleClause(HqlParser.CycleClauseContext ctx) {
181+
182+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
183+
184+
tokens.add(new JpaQueryParsingToken(ctx.CYCLE().getText()));
185+
tokens.addAll(visit(ctx.cteAttributes()));
186+
tokens.add(new JpaQueryParsingToken(ctx.SET().getText()));
187+
tokens.addAll(visit(ctx.identifier(0)));
188+
189+
if (ctx.TO() != null) {
190+
191+
tokens.add(new JpaQueryParsingToken(ctx.TO().getText()));
192+
tokens.addAll(visit(ctx.literal(0)));
193+
tokens.add(new JpaQueryParsingToken(ctx.DEFAULT().getText()));
194+
tokens.addAll(visit(ctx.literal(1)));
195+
}
196+
197+
if (ctx.USING() != null) {
198+
199+
tokens.add(new JpaQueryParsingToken(ctx.USING().getText()));
200+
tokens.addAll(visit(ctx.identifier(1)));
201+
}
202+
203+
return tokens;
204+
}
205+
206+
@Override
207+
public List<JpaQueryParsingToken> visitCteAttributes(HqlParser.CteAttributesContext ctx) {
208+
209+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
210+
211+
ctx.identifier().forEach(identifierContext -> {
212+
213+
tokens.addAll(visit(identifierContext));
214+
tokens.add(TOKEN_COMMA);
215+
});
216+
CLIP(tokens);
217+
218+
return tokens;
219+
}
220+
73221
@Override
74222
public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
75223

@@ -1876,7 +2024,7 @@ public List<JpaQueryParsingToken> visitNotPredicate(HqlParser.NotPredicateContex
18762024

18772025
List<JpaQueryParsingToken> tokens = new ArrayList<>();
18782026

1879-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2027+
tokens.add(TOKEN_NOT);
18802028
tokens.addAll(visit(ctx.predicate()));
18812029

18822030
return tokens;
@@ -1919,7 +2067,7 @@ public List<JpaQueryParsingToken> visitBetweenExpression(HqlParser.BetweenExpres
19192067
tokens.addAll(visit(ctx.expression(0)));
19202068

19212069
if (ctx.NOT() != null) {
1922-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2070+
tokens.add(TOKEN_NOT);
19232071
}
19242072

19252073
tokens.add(new JpaQueryParsingToken(ctx.BETWEEN()));
@@ -1939,7 +2087,7 @@ public List<JpaQueryParsingToken> visitDealingWithNullExpression(HqlParser.Deali
19392087
tokens.add(new JpaQueryParsingToken(ctx.IS()));
19402088

19412089
if (ctx.NOT() != null) {
1942-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2090+
tokens.add(TOKEN_NOT);
19432091
}
19442092

19452093
if (ctx.NULL() != null) {
@@ -1962,7 +2110,7 @@ public List<JpaQueryParsingToken> visitStringPatternMatching(HqlParser.StringPat
19622110
tokens.addAll(visit(ctx.expression(0)));
19632111

19642112
if (ctx.NOT() != null) {
1965-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2113+
tokens.add(TOKEN_NOT);
19662114
}
19672115

19682116
if (ctx.LIKE() != null) {
@@ -1990,7 +2138,7 @@ public List<JpaQueryParsingToken> visitInExpression(HqlParser.InExpressionContex
19902138
tokens.addAll(visit(ctx.expression()));
19912139

19922140
if (ctx.NOT() != null) {
1993-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2141+
tokens.add(TOKEN_NOT);
19942142
}
19952143

19962144
tokens.add(new JpaQueryParsingToken(ctx.IN()));
@@ -2081,14 +2229,14 @@ public List<JpaQueryParsingToken> visitCollectionExpression(HqlParser.Collection
20812229
tokens.add(new JpaQueryParsingToken(ctx.IS()));
20822230

20832231
if (ctx.NOT() != null) {
2084-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2232+
tokens.add(TOKEN_NOT);
20852233
}
20862234

20872235
tokens.add(new JpaQueryParsingToken(ctx.EMPTY()));
20882236
} else if (ctx.MEMBER() != null) {
20892237

20902238
if (ctx.NOT() != null) {
2091-
tokens.add(new JpaQueryParsingToken(ctx.NOT()));
2239+
tokens.add(TOKEN_NOT);
20922240
}
20932241

20942242
tokens.add(new JpaQueryParsingToken(ctx.MEMBER()));

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java

+7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ class JpaQueryParsingToken {
5959
public static final JpaQueryParsingToken TOKEN_DESC = new JpaQueryParsingToken("desc", false);
6060

6161
public static final JpaQueryParsingToken TOKEN_ASC = new JpaQueryParsingToken("asc", false);
62+
63+
public static final JpaQueryParsingToken TOKEN_WITH = new JpaQueryParsingToken("WITH");
64+
65+
public static final JpaQueryParsingToken TOKEN_NOT = new JpaQueryParsingToken("NOT");
66+
67+
public static final JpaQueryParsingToken TOKEN_MATERIALIZED = new JpaQueryParsingToken("materialized");
68+
6269
/**
6370
* The text value of the token.
6471
*/

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -1487,4 +1487,16 @@ select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc
14871487
""");
14881488
});
14891489
}
1490+
1491+
@Test // GH-2981
1492+
void cteWithClauseShouldWork() {
1493+
1494+
assertQuery("""
1495+
WITH maxId AS(select max(sr.snapshot.id) snapshotId from SnapshotReference sr
1496+
where sr.id.selectionId = ?1 and sr.enabled
1497+
group by sr.userId
1498+
)
1499+
select sr from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId
1500+
""");
1501+
}
14901502
}

0 commit comments

Comments
 (0)