Skip to content

Commit 31e1327

Browse files
committed
Introduce support for ordering by aliased columns.
If a projection of an HQL query is aliased, be that a function call or a simply alias, apply sorting should NOT result in that order parameter having the primary FROM clause's alias prefixed to. Resolves #2863. Related: #2626, #2322.
1 parent b3f2522 commit 31e1327

File tree

2 files changed

+127
-51
lines changed

2 files changed

+127
-51
lines changed

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

+57-18
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
import java.util.ArrayList;
2121
import java.util.Collections;
22+
import java.util.HashSet;
2223
import java.util.List;
24+
import java.util.Set;
2325

2426
import org.antlr.v4.runtime.ParserRuleContext;
2527
import org.springframework.data.domain.Sort;
@@ -41,13 +43,15 @@ class HqlQueryTransformer extends HqlQueryRenderer {
4143

4244
private final @Nullable String countProjection;
4345

44-
private @Nullable String alias = null;
46+
private @Nullable String primaryFromClauseAlias = null;
4547

4648
private List<JpaQueryParsingToken> projection = Collections.emptyList();
4749
private boolean projectionProcessed;
4850

4951
private boolean hasConstructorExpression = false;
5052

53+
private Set<String> projectionAliases;
54+
5155
HqlQueryTransformer() {
5256
this(Sort.unsorted(), false, null);
5357
}
@@ -67,11 +71,12 @@ private HqlQueryTransformer(Sort sort, boolean countQuery, @Nullable String coun
6771
this.sort = sort;
6872
this.countQuery = countQuery;
6973
this.countProjection = countProjection;
74+
this.projectionAliases = new HashSet<>();
7075
}
7176

7277
@Nullable
7378
public String getAlias() {
74-
return this.alias;
79+
return this.primaryFromClauseAlias;
7580
}
7681

7782
public List<JpaQueryParsingToken> getProjection() {
@@ -137,13 +142,14 @@ public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContex
137142
if (order.isIgnoreCase()) {
138143
tokens.add(TOKEN_LOWER_FUNC);
139144
}
145+
140146
tokens.add(new JpaQueryParsingToken(() -> {
141147

142-
if (order.getProperty().contains("(")) {
148+
if (shouldAlias(order)) {
149+
return primaryFromClauseAlias + "." + order.getProperty();
150+
} else {
143151
return order.getProperty();
144152
}
145-
146-
return this.alias + "." + order.getProperty();
147153
}, true));
148154
if (order.isIgnoreCase()) {
149155
NOSPACE(tokens);
@@ -164,6 +170,33 @@ public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContex
164170
return tokens;
165171
}
166172

173+
/**
174+
* Determine when an {@link org.springframework.data.domain.Sort.Order} parameter should alias (or not).
175+
*
176+
* @param order
177+
* @return boolean whether or not to apply the primary FROM clause's alias
178+
*/
179+
private boolean shouldAlias(Sort.Order order) {
180+
181+
if (orderParameterIsAFunction(order)) {
182+
return false;
183+
}
184+
185+
if (orderParameterReferencesAProjectionAlias(order)) {
186+
return false;
187+
}
188+
189+
return true;
190+
}
191+
192+
private boolean orderParameterIsAFunction(Sort.Order order) {
193+
return order.getProperty().contains("(");
194+
}
195+
196+
private boolean orderParameterReferencesAProjectionAlias(Sort.Order order) {
197+
return projectionAliases.contains(order.getProperty());
198+
}
199+
167200
@Override
168201
public List<JpaQueryParsingToken> visitFromQuery(HqlParser.FromQueryContext ctx) {
169202

@@ -176,7 +209,7 @@ public List<JpaQueryParsingToken> visitFromQuery(HqlParser.FromQueryContext ctx)
176209
if (countProjection != null) {
177210
tokens.add(new JpaQueryParsingToken(countProjection));
178211
} else {
179-
tokens.add(new JpaQueryParsingToken(() -> this.alias, false));
212+
tokens.add(new JpaQueryParsingToken(() -> this.primaryFromClauseAlias, false));
180213
}
181214

182215
tokens.add(TOKEN_CLOSE_PAREN);
@@ -240,8 +273,8 @@ public List<JpaQueryParsingToken> visitFromRoot(HqlParser.FromRootContext ctx) {
240273
if (ctx.variable() != null) {
241274
tokens.addAll(visit(ctx.variable()));
242275

243-
if (this.alias == null && !isSubquery(ctx)) {
244-
this.alias = tokens.get(tokens.size() - 1).getToken();
276+
if (primaryFromClauseAlias == null && !isSubquery(ctx)) {
277+
primaryFromClauseAlias = tokens.get(tokens.size() - 1).getToken();
245278
}
246279
}
247280
} else if (ctx.subquery() != null) {
@@ -256,8 +289,8 @@ public List<JpaQueryParsingToken> visitFromRoot(HqlParser.FromRootContext ctx) {
256289
if (ctx.variable() != null) {
257290
tokens.addAll(visit(ctx.variable()));
258291

259-
if (this.alias == null && !isSubquery(ctx)) {
260-
this.alias = tokens.get(tokens.size() - 1).getToken();
292+
if (primaryFromClauseAlias == null && !isSubquery(ctx)) {
293+
primaryFromClauseAlias = tokens.get(tokens.size() - 1).getToken();
261294
}
262295
}
263296
}
@@ -268,16 +301,22 @@ public List<JpaQueryParsingToken> visitFromRoot(HqlParser.FromRootContext ctx) {
268301
@Override
269302
public List<JpaQueryParsingToken> visitAlias(HqlParser.AliasContext ctx) {
270303

271-
List<JpaQueryParsingToken> tokens = newArrayList();
304+
List<JpaQueryParsingToken> tokens = super.visitAlias(ctx);
272305

273-
if (ctx.AS() != null) {
274-
tokens.add(new JpaQueryParsingToken(ctx.AS()));
306+
if (primaryFromClauseAlias == null && !isSubquery(ctx)) {
307+
primaryFromClauseAlias = tokens.get(tokens.size() - 1).getToken();
275308
}
276309

277-
tokens.addAll(visit(ctx.identifier()));
310+
return tokens;
311+
}
312+
313+
@Override
314+
public List<JpaQueryParsingToken> visitVariable(HqlParser.VariableContext ctx) {
315+
316+
List<JpaQueryParsingToken> tokens = super.visitVariable(ctx);
278317

279-
if (this.alias == null && !isSubquery(ctx)) {
280-
this.alias = tokens.get(tokens.size() - 1).getToken();
318+
if (ctx.identifier() != null) {
319+
projectionAliases.add(tokens.get(tokens.size() - 1).getToken());
281320
}
282321

283322
return tokens;
@@ -312,13 +351,13 @@ public List<JpaQueryParsingToken> visitSelectClause(HqlParser.SelectClauseContex
312351

313352
if (selectionListTokens.stream().anyMatch(hqlToken -> hqlToken.getToken().contains("new"))) {
314353
// constructor
315-
tokens.add(new JpaQueryParsingToken(() -> this.alias));
354+
tokens.add(new JpaQueryParsingToken(() -> this.primaryFromClauseAlias));
316355
} else {
317356
// keep all the select items to distinct against
318357
tokens.addAll(selectionListTokens);
319358
}
320359
} else {
321-
tokens.add(new JpaQueryParsingToken(() -> this.alias));
360+
tokens.add(new JpaQueryParsingToken(() -> this.primaryFromClauseAlias));
322361
}
323362
}
324363

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

+70-33
Original file line numberDiff line numberDiff line change
@@ -379,94 +379,85 @@ void doesNotPrefixUnsafeJpaSortFunctionCalls() {
379379
assertThat(createQueryFor("select p from Person p", sort)).endsWith("order by sum(foo) asc");
380380
}
381381

382-
@Test // DATAJPA-965, DATAJPA-970
382+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
383383
void doesNotPrefixMultipleAliasedFunctionCalls() {
384384

385385
String query = "SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m";
386386
Sort sort = Sort.by("avgPrice", "sumStocks");
387387

388-
// TODO: Add support for aliased functions
389-
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc, sumStocks asc");
388+
assertThat(createQueryFor(query, sort)).endsWith("order by avgPrice asc, sumStocks asc");
390389
}
391390

392-
@Test // DATAJPA-965, DATAJPA-970
391+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
393392
void doesNotPrefixSingleAliasedFunctionCalls() {
394393

395394
String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
396395
Sort sort = Sort.by("avgPrice");
397396

398-
// TODO: Add support for aliased functions
399-
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc");
397+
assertThat(createQueryFor(query, sort)).endsWith("order by avgPrice asc");
400398
}
401399

402-
@Test // DATAJPA-965, DATAJPA-970
400+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
403401
void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() {
404402

405403
String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
406404
Sort sort = Sort.by("someOtherProperty");
407405

408-
// TODO: Add support for aliased functions
409-
// assertThat(query(query, (Sort) "m")).endsWith("order by m.someOtherProperty asc");
406+
assertThat(createQueryFor(query, sort)).endsWith("order by m.someOtherProperty asc");
410407
}
411408

412-
@Test // DATAJPA-965, DATAJPA-970
409+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
413410
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() {
414411

415412
String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m";
416413
Sort sort = Sort.by("name", "avgPrice");
417414

418-
// TODO: Add support for aliased functions
419-
// assertThat(query(query, (Sort) "m")).endsWith("order by m.name asc, avgPrice asc");
415+
assertThat(createQueryFor(query, sort)).endsWith("order by m.name asc, avgPrice asc");
420416
}
421417

422-
@Test // DATAJPA-965, DATAJPA-970
418+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
423419
void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() {
424420

425421
String query = "SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m";
426422
Sort sort = Sort.by("trimmedName");
427423

428-
// TODO: Add support for aliased functions
429-
// assertThat(query(query, (Sort) "m")).endsWith("order by trimmedName asc");
424+
assertThat(createQueryFor(query, sort)).endsWith("order by trimmedName asc");
430425
}
431426

432-
@Test // DATAJPA-965, DATAJPA-970
427+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
433428
void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() {
434429

435430
String query = "SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m";
436431
Sort sort = Sort.by("extendedName");
437432

438-
// TODO: Add support for aliased functions
439-
// assertThat(query(query, (Sort) "m")).endsWith("order by extendedName asc");
433+
assertThat(createQueryFor(query, sort)).endsWith("order by extendedName asc");
440434
}
441435

442-
@Test // DATAJPA-965, DATAJPA-970
436+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
443437
void doesNotPrefixAliasedFunctionCallNameWithUnderscores() {
444438

445439
String query = "SELECT AVG(m.price) AS avg_price FROM Magazine m";
446440
Sort sort = Sort.by("avg_price");
447441

448-
// TODO: Add support for aliased functions
449-
// assertThat(query(query, (Sort) "m")).endsWith("order by avg_price asc");
442+
assertThat(createQueryFor(query, sort)).endsWith("order by avg_price asc");
450443
}
451444

452-
@Test // DATAJPA-965, DATAJPA-970
445+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
453446
void doesNotPrefixAliasedFunctionCallNameWithDots() {
454447

455448
String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
456449
Sort sort = Sort.by("m.avg");
457450

458-
// TODO: Add support for aliased functions
459-
// assertThat(query(query, (Sort) "m")).endsWith("order by m.avg asc");
451+
assertThatIllegalArgumentException().isThrownBy(() -> createQueryFor(query, sort));
460452
}
461453

462-
@Test // DATAJPA-965, DATAJPA-970
454+
@Test // DATAJPA-965, DATAJPA-970, GH-2863
463455
void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() {
464456

465457
String query = "SELECT AVG( m.price ) AS avgPrice FROM Magazine m";
466458
Sort sort = Sort.by("avgPrice");
467459

468-
// TODO: Add support for aliased functions
469-
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc");
460+
assertThat(createQueryFor(query, sort)).endsWith("order by avgPrice asc");
470461
}
471462

472463
@Test // DATAJPA-1506
@@ -498,18 +489,18 @@ void createCountQuerySupportsLineBreaksInSelectClause() {
498489
" where user.age = 18\n ");
499490
}
500491

501-
@Test // DATAJPA-1061
492+
@Test // DATAJPA-1061, GH-2863
502493
void appliesSortCorrectlyForFieldAliases() {
503494

504495
String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
505496
Sort sort = Sort.by("authorName");
506497

507498
String fullQuery = createQueryFor(query, sort);
508499

509-
assertThat(fullQuery).endsWith("order by m.authorName asc");
500+
assertThat(fullQuery).endsWith("order by authorName asc");
510501
}
511502

512-
@Test // GH-2280
503+
@Test // GH-2280, GH-2863
513504
void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() {
514505

515506
String query = "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer";
@@ -518,18 +509,18 @@ void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() {
518509
String fullQuery = createQueryFor(query, sort);
519510

520511
assertThat(fullQuery).isEqualTo(
521-
"SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(customer.name) asc");
512+
"SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(name) asc");
522513
}
523514

524-
@Test // DATAJPA-1061
515+
@Test // DATAJPA-1061, GH-2863
525516
void appliesSortCorrectlyForFunctionAliases() {
526517

527518
String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
528519
Sort sort = Sort.by("title");
529520

530521
String fullQuery = createQueryFor(query, sort);
531522

532-
assertThat(fullQuery).endsWith("order by m.title asc");
523+
assertThat(fullQuery).endsWith("order by title asc");
533524
}
534525

535526
@Test // DATAJPA-1061
@@ -793,6 +784,52 @@ void sortProperlyAppendsToExistingOrderByWithFunction() {
793784
"select e from SampleEntity e where function('nativeFunc', ?1) > 'testVal' order by function('nativeFunc', ?1), e.age desc");
794785
}
795786

787+
@Test // GH-2626, GH-2863
788+
void orderByAliasedColumn() {
789+
790+
assertThat(createQueryFor("""
791+
select
792+
max(resource.name) as resourceName,
793+
max(resource.id) as id,
794+
max(resource.description) as description,
795+
max(resource.uuid) as uuid,
796+
max(resource.type) as type,
797+
max(resource.createdOn) as createdOn,
798+
max(users.firstName) as authorFirstName,
799+
max(users.lastName) as authorLastName,
800+
max(file.version) as version,
801+
max(file.comment) as comment,
802+
file.deployed as deployed,
803+
max(log.date) as modifiedOn
804+
from Resource resource
805+
where (
806+
cast(:startDate as date) is null
807+
or resource.latestLogRecord.date between cast(:startDate as date) and cast(:endDate as date)
808+
)
809+
group by resource.id, file.deployed, log.author.firstName, file.comment
810+
""", Sort.by(Sort.Direction.DESC, "uuid"))).endsWith("order by uuid desc");
811+
}
812+
813+
@Test // GH-2863, GH-2322
814+
void sortShouldWorkWhenAliasingFunctions() {
815+
816+
assertThat(createQueryFor("""
817+
SELECT
818+
DISTINCT(event.id) as id,
819+
event.name as name,
820+
MIN(bundle.base_price_amount) as cheapestBundlePrice,
821+
MIN(DATE(bundle.start)) as earliestBundleStart
822+
FROM event event
823+
LEFT JOIN bundle bundle ON event.id = bundle.event_id
824+
GROUP BY event.id
825+
""", Sort.by(Sort.Direction.ASC, "cheapestBundlePrice") //
826+
.and(Sort.by(
827+
Sort.Direction.ASC, "earliestBundleStart")) //
828+
.and(Sort.by(Sort.Direction.ASC,
829+
"name"))))
830+
.endsWith(" order by cheapestBundlePrice asc, earliestBundleStart asc, name asc");
831+
}
832+
796833
private void assertCountQuery(String originalQuery, String countQuery) {
797834
assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
798835
}

0 commit comments

Comments
 (0)