Skip to content

Commit c4a6d38

Browse files
DiegoKrupitzagregturn
authored andcommitted
Adds support for more SelectBody types in JSqlParserQueryEhancer.
We now support `ValuesStatement` and `SetOperationList`. This allows native queries to use `union`, `except`, and `with` statements in native SQL queries. Closes #2578.
1 parent 63919b5 commit c4a6d38

File tree

4 files changed

+333
-8
lines changed

4 files changed

+333
-8
lines changed

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

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@
2929
import net.sf.jsqlparser.statement.select.OrderByElement;
3030
import net.sf.jsqlparser.statement.select.PlainSelect;
3131
import net.sf.jsqlparser.statement.select.Select;
32+
import net.sf.jsqlparser.statement.select.SelectBody;
3233
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
3334
import net.sf.jsqlparser.statement.select.SelectItem;
35+
import net.sf.jsqlparser.statement.select.SetOperationList;
36+
import net.sf.jsqlparser.statement.select.WithItem;
3437
import net.sf.jsqlparser.statement.update.Update;
38+
import net.sf.jsqlparser.statement.values.ValuesStatement;
3539

3640
import java.util.ArrayList;
3741
import java.util.Collections;
@@ -107,6 +111,13 @@ public String applySorting(Sort sort, @Nullable String alias) {
107111
}
108112

109113
Select selectStatement = parseSelectStatement(queryString);
114+
115+
if (selectStatement.getSelectBody()instanceof SetOperationList setOperationList) {
116+
return applySortingToSetOperationList(setOperationList, sort);
117+
} else if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
118+
return queryString;
119+
}
120+
110121
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
111122

112123
final Set<String> joinAliases = getJoinAliases(selectBody);
@@ -115,7 +126,7 @@ public String applySorting(Sort sort, @Nullable String alias) {
115126

116127
List<OrderByElement> orderByElements = sort.stream() //
117128
.map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) //
118-
.collect(Collectors.toList());
129+
.toList();
119130

120131
if (CollectionUtils.isEmpty(selectBody.getOrderByElements())) {
121132
selectBody.setOrderByElements(new ArrayList<>());
@@ -127,6 +138,33 @@ public String applySorting(Sort sort, @Nullable String alias) {
127138

128139
}
129140

141+
/**
142+
* Returns the {@link SetOperationList} as a string query with {@link Sort}s applied in the right order.
143+
*
144+
* @param setOperationListStatement
145+
* @param sort
146+
* @return
147+
*/
148+
private String applySortingToSetOperationList(SetOperationList setOperationListStatement, Sort sort) {
149+
150+
// special case: ValuesStatements are detected as nested OperationListStatements
151+
if (setOperationListStatement.getSelects().stream().anyMatch(ValuesStatement.class::isInstance)) {
152+
return setOperationListStatement.toString();
153+
}
154+
155+
// if (CollectionUtils.isEmpty(setOperationListStatement.getOrderByElements())) {
156+
if (setOperationListStatement.getOrderByElements() == null) {
157+
setOperationListStatement.setOrderByElements(new ArrayList<>());
158+
}
159+
160+
List<OrderByElement> orderByElements = sort.stream() //
161+
.map(order -> getOrderClause(Collections.emptySet(), Collections.emptySet(), null, order)) //
162+
.toList();
163+
setOperationListStatement.getOrderByElements().addAll(orderByElements);
164+
165+
return setOperationListStatement.toString();
166+
}
167+
130168
/**
131169
* Returns the aliases used inside the selection part in the query.
132170
*
@@ -175,7 +213,12 @@ private Set<String> getJoinAliases(String query) {
175213
return new HashSet<>();
176214
}
177215

178-
return getJoinAliases((PlainSelect) parseSelectStatement(query).getSelectBody());
216+
Select selectStatement = parseSelectStatement(query);
217+
if (selectStatement.getSelectBody()instanceof PlainSelect selectBody) {
218+
return getJoinAliases(selectBody);
219+
}
220+
221+
return new HashSet<>();
179222
}
180223

181224
/**
@@ -259,6 +302,17 @@ private String detectAlias(String query) {
259302
}
260303

261304
Select selectStatement = parseSelectStatement(query);
305+
306+
/*
307+
For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide
308+
alias since:
309+
* ValuesStatement has no alias
310+
* SetOperation can have multiple alias for each operation item
311+
*/
312+
if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
313+
return null;
314+
}
315+
262316
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
263317
return detectAlias(selectBody);
264318
}
@@ -273,6 +327,10 @@ private String detectAlias(String query) {
273327
@Nullable
274328
private static String detectAlias(PlainSelect selectBody) {
275329

330+
if (selectBody.getFromItem() == null) {
331+
return null;
332+
}
333+
276334
Alias alias = selectBody.getFromItem().getAlias();
277335
return alias == null ? null : alias.getName();
278336
}
@@ -287,6 +345,14 @@ public String createCountQueryFor(@Nullable String countProjection) {
287345
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty");
288346

289347
Select selectStatement = parseSelectStatement(this.query.getQueryString());
348+
349+
/*
350+
We only support count queries for {@link PlainSelect}.
351+
*/
352+
if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
353+
return this.query.getQueryString();
354+
}
355+
290356
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
291357

292358
// remove order by
@@ -322,8 +388,15 @@ public String createCountQueryFor(@Nullable String countProjection) {
322388
Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct);
323389
selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
324390

325-
return selectBody.toString();
391+
if (CollectionUtils.isEmpty(selectStatement.getWithItemsList())) {
392+
return selectBody.toString();
393+
}
326394

395+
String withStatements = selectStatement.getWithItemsList().stream() //
396+
.map(WithItem::toString) //
397+
.collect(Collectors.joining(","));
398+
399+
return "with " + withStatements + "\n" + selectBody;
327400
}
328401

329402
@Override
@@ -336,9 +409,23 @@ public String getProjection() {
336409
Assert.hasText(query.getQueryString(), "Query must not be null or empty");
337410

338411
Select selectStatement = parseSelectStatement(query.getQueryString());
339-
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
340412

341-
return selectBody.getSelectItems() //
413+
if (selectStatement.getSelectBody() instanceof ValuesStatement) {
414+
return "";
415+
}
416+
417+
SelectBody selectBody = selectStatement.getSelectBody();
418+
419+
if (selectStatement.getSelectBody()instanceof SetOperationList setOperationList) {
420+
// using the first one since for setoperations the projection has to be the same
421+
selectBody = setOperationList.getSelects().get(0);
422+
423+
if (!(selectBody instanceof PlainSelect)) {
424+
return "";
425+
}
426+
}
427+
428+
return ((PlainSelect) selectBody).getSelectItems() //
342429
.stream() //
343430
.map(Object::toString) //
344431
.collect(Collectors.joining(", ")).trim();

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,6 +2902,72 @@ public void correctlyBuildSortClauseWhenSortingByFunctionAliasAndFunctionContain
29022902
repository.findAllAndSortByFunctionResultNamedParameter("prefix", "suffix", Sort.by("idWithPrefixAndSuffix"));
29032903
}
29042904

2905+
@Test // GH-2578
2906+
void simpleNativeExceptTest() {
2907+
2908+
flushTestUsers();
2909+
2910+
List<String> foundIds = repository.findWithSimpleExceptNative();
2911+
2912+
assertThat(foundIds) //
2913+
.isNotEmpty() //
2914+
.contains("Oliver", "kevin");
2915+
}
2916+
2917+
@Test // GH-2578
2918+
void simpleNativeUnionTest() {
2919+
2920+
flushTestUsers();
2921+
2922+
List<String> foundIds = repository.findWithSimpleUnionNative();
2923+
2924+
assertThat(foundIds) //
2925+
.isNotEmpty() //
2926+
.containsExactlyInAnyOrder("Dave", "Joachim", "Oliver", "kevin");
2927+
}
2928+
2929+
@Test // GH-2578
2930+
void complexNativeExceptTest() {
2931+
2932+
flushTestUsers();
2933+
2934+
List<String> foundIds = repository.findWithComplexExceptNative();
2935+
2936+
assertThat(foundIds).containsExactly("Oliver", "kevin");
2937+
}
2938+
2939+
@Test // GH-2578
2940+
void simpleValuesStatementNative() {
2941+
2942+
flushTestUsers();
2943+
2944+
List<Integer> foundIds = repository.valuesStatementNative();
2945+
2946+
assertThat(foundIds).containsExactly(1);
2947+
}
2948+
2949+
@Test // GH-2578
2950+
void withStatementNative() {
2951+
2952+
flushTestUsers();
2953+
2954+
List<User> foundData = repository.withNativeStatement();
2955+
2956+
assertThat(foundData) //
2957+
.map(User::getFirstname) //
2958+
.containsExactly("Joachim", "Dave", "kevin");
2959+
}
2960+
2961+
@Test // GH-2578
2962+
void complexWithNativeStatement() {
2963+
2964+
flushTestUsers();
2965+
2966+
List<String> foundData = repository.complexWithNativeStatement();
2967+
2968+
assertThat(foundData).containsExactly("joachim", "dave", "kevin");
2969+
}
2970+
29052971
private Page<User> executeSpecWithSort(Sort sort) {
29062972

29072973
flushTestUsers();

0 commit comments

Comments
 (0)