Skip to content

Commit b592705

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 spring-projects#2578.
1 parent e2e2734 commit b592705

File tree

4 files changed

+335
-7
lines changed

4 files changed

+335
-7
lines changed

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

+94-4
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,14 @@ public String applySorting(Sort sort, @Nullable String alias) {
107111
}
108112

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

112124
final Set<String> joinAliases = getJoinAliases(selectBody);
@@ -127,6 +139,33 @@ public String applySorting(Sort sort, @Nullable String alias) {
127139

128140
}
129141

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

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

181226
/**
@@ -259,6 +304,17 @@ private String detectAlias(String query) {
259304
}
260305

261306
Select selectStatement = parseSelectStatement(query);
307+
308+
/*
309+
For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide
310+
alias since:
311+
* ValuesStatement has no alias
312+
* SetOperation can have multiple alias for each operation item
313+
*/
314+
if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
315+
return null;
316+
}
317+
262318
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
263319
return detectAlias(selectBody);
264320
}
@@ -273,6 +329,10 @@ private String detectAlias(String query) {
273329
@Nullable
274330
private static String detectAlias(PlainSelect selectBody) {
275331

332+
if (selectBody.getFromItem() == null) {
333+
return null;
334+
}
335+
276336
Alias alias = selectBody.getFromItem().getAlias();
277337
return alias == null ? null : alias.getName();
278338
}
@@ -287,6 +347,14 @@ public String createCountQueryFor(@Nullable String countProjection) {
287347
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty!");
288348

289349
Select selectStatement = parseSelectStatement(this.query.getQueryString());
350+
351+
/*
352+
We only support count queries for {@link PlainSelect}.
353+
*/
354+
if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
355+
return this.query.getQueryString();
356+
}
357+
290358
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
291359

292360
// remove order by
@@ -322,8 +390,15 @@ public String createCountQueryFor(@Nullable String countProjection) {
322390
Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct);
323391
selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
324392

325-
return selectBody.toString();
393+
if (CollectionUtils.isEmpty(selectStatement.getWithItemsList())) {
394+
return selectBody.toString();
395+
}
326396

397+
String withStatements = selectStatement.getWithItemsList().stream() //
398+
.map(WithItem::toString) //
399+
.collect(Collectors.joining(","));
400+
401+
return "with " + withStatements + "\n" + selectBody;
327402
}
328403

329404
@Override
@@ -336,9 +411,24 @@ public String getProjection() {
336411
Assert.hasText(query.getQueryString(), "Query must not be null or empty!");
337412

338413
Select selectStatement = parseSelectStatement(query.getQueryString());
339-
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
340414

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

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

+66
Original file line numberDiff line numberDiff line change
@@ -2702,6 +2702,72 @@ public void correctlyBuildSortClauseWhenSortingByFunctionAliasAndFunctionContain
27022702
repository.findAllAndSortByFunctionResultNamedParameter("prefix", "suffix", Sort.by("idWithPrefixAndSuffix"));
27032703
}
27042704

2705+
@Test // GH-2578
2706+
void simpleNativeExceptTest() {
2707+
2708+
flushTestUsers();
2709+
2710+
List<String> foundIds = repository.findWithSimpleExceptNative();
2711+
2712+
assertThat(foundIds) //
2713+
.isNotEmpty() //
2714+
.contains("Oliver", "kevin");
2715+
}
2716+
2717+
@Test // GH-2578
2718+
void simpleNativeUnionTest() {
2719+
2720+
flushTestUsers();
2721+
2722+
List<String> foundIds = repository.findWithSimpleUnionNative();
2723+
2724+
assertThat(foundIds) //
2725+
.isNotEmpty() //
2726+
.containsExactlyInAnyOrder("Dave", "Joachim", "Oliver", "kevin");
2727+
}
2728+
2729+
@Test // GH-2578
2730+
void complexNativeExceptTest() {
2731+
2732+
flushTestUsers();
2733+
2734+
List<String> foundIds = repository.findWithComplexExceptNative();
2735+
2736+
assertThat(foundIds).containsExactly("Oliver", "kevin");
2737+
}
2738+
2739+
@Test // GH-2578
2740+
void simpleValuesStatementNative() {
2741+
2742+
flushTestUsers();
2743+
2744+
List<Integer> foundIds = repository.valuesStatementNative();
2745+
2746+
assertThat(foundIds).containsExactly(1);
2747+
}
2748+
2749+
@Test // GH-2578
2750+
void withStatementNative() {
2751+
2752+
flushTestUsers();
2753+
2754+
List<User> foundData = repository.withNativeStatement();
2755+
2756+
assertThat(foundData) //
2757+
.map(User::getFirstname) //
2758+
.containsExactly("Joachim", "Dave", "kevin");
2759+
}
2760+
2761+
@Test // GH-2578
2762+
void complexWithNativeStatement() {
2763+
2764+
flushTestUsers();
2765+
2766+
List<String> foundData = repository.complexWithNativeStatement();
2767+
2768+
assertThat(foundData).containsExactly("joachim", "dave", "kevin");
2769+
}
2770+
27052771
private Page<User> executeSpecWithSort(Sort sort) {
27062772

27072773
flushTestUsers();

0 commit comments

Comments
 (0)