Skip to content

Commit 6275802

Browse files
committed
Prevent sorting and count queries for non-SELECT statements.
Add statement type detection to QueryEnhancer implementations to validate operations. QueryEnhancer now throws IllegalStateException when attempting to create count queries or apply sorting to INSERT, UPDATE, DELETE, or MERGE statements, as these operations are only valid for SELECT queries.
1 parent 3ac19b3 commit 6275802

File tree

10 files changed

+356
-22
lines changed

10 files changed

+356
-22
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18+
import java.util.Locale;
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
1822
import org.jspecify.annotations.Nullable;
1923

2024
/**
@@ -25,30 +29,69 @@
2529
*/
2630
class DefaultQueryEnhancer implements QueryEnhancer {
2731

32+
private static final Pattern STATEMENT_TYPE_PATTERN = Pattern.compile(
33+
"^\\s*(SELECT|FROM|INSERT|UPDATE|DELETE|MERGE)", Pattern.CASE_INSENSITIVE);
34+
2835
private final QueryProvider query;
2936
private final boolean hasConstructorExpression;
3037
private final @Nullable String alias;
3138
private final String projection;
39+
private final StatementType statementType;
3240

3341
public DefaultQueryEnhancer(QueryProvider query) {
3442
this.query = query;
3543
this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString());
3644
this.alias = QueryUtils.detectAlias(query.getQueryString());
3745
this.projection = QueryUtils.getProjection(this.query.getQueryString());
46+
this.statementType = detectStatementType(query.getQueryString());
3847
}
3948

4049
@Override
4150
public String rewrite(QueryRewriteInformation rewriteInformation) {
51+
52+
if (statementType != StatementType.SELECT && !rewriteInformation.getSort().isUnsorted()) {
53+
throw new IllegalStateException(String.format(
54+
"Cannot apply sorting to %s statement. Sorting is only supported for SELECT statements.",
55+
statementType));
56+
}
57+
4258
return QueryUtils.applySorting(this.query.getQueryString(), rewriteInformation.getSort(), alias);
4359
}
4460

4561
@Override
4662
public String createCountQueryFor(@Nullable String countProjection) {
4763

64+
if (statementType != StatementType.SELECT) {
65+
throw new IllegalStateException(String.format(
66+
"Cannot derive count query for %s statement. Count queries are only supported for SELECT statements.",
67+
statementType));
68+
}
69+
4870
boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNative() : true;
4971
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, nativeQuery);
5072
}
5173

74+
private static StatementType detectStatementType(String query) {
75+
76+
Matcher matcher = STATEMENT_TYPE_PATTERN.matcher(query);
77+
if (matcher.find()) {
78+
String type = matcher.group(1).toUpperCase(Locale.ENGLISH);
79+
return switch (type) {
80+
case "SELECT", "FROM" -> StatementType.SELECT; // FROM is also a SELECT in JPQL
81+
case "INSERT" -> StatementType.INSERT;
82+
case "UPDATE" -> StatementType.UPDATE;
83+
case "DELETE" -> StatementType.DELETE;
84+
case "MERGE" -> StatementType.MERGE;
85+
default -> StatementType.OTHER;
86+
};
87+
}
88+
return StatementType.OTHER;
89+
}
90+
91+
enum StatementType {
92+
SELECT, INSERT, UPDATE, DELETE, MERGE, OTHER
93+
}
94+
5295
@Override
5396
public boolean hasConstructorExpression() {
5497
return this.hasConstructorExpression;

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,36 @@ class EqlQueryIntrospector extends EqlBaseVisitor<Void> implements ParsedQueryIn
3838
private @Nullable List<QueryToken> projection;
3939
private boolean projectionProcessed;
4040
private boolean hasConstructorExpression = false;
41+
private QueryInformation.StatementType statementType = QueryInformation.StatementType.SELECT;
4142

4243
@Override
4344
public QueryInformation getParsedQueryInformation() {
4445
return new QueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection,
45-
hasConstructorExpression);
46+
hasConstructorExpression, statementType);
47+
}
48+
49+
@Override
50+
public Void visitSelectQuery(EqlParser.SelectQueryContext ctx) {
51+
statementType = QueryInformation.StatementType.SELECT;
52+
return super.visitSelectQuery(ctx);
53+
}
54+
55+
@Override
56+
public Void visitFromQuery(EqlParser.FromQueryContext ctx) {
57+
statementType = QueryInformation.StatementType.SELECT;
58+
return super.visitFromQuery(ctx);
59+
}
60+
61+
@Override
62+
public Void visitUpdate_statement(EqlParser.Update_statementContext ctx) {
63+
statementType = QueryInformation.StatementType.UPDATE;
64+
return super.visitUpdate_statement(ctx);
65+
}
66+
67+
@Override
68+
public Void visitDelete_statement(EqlParser.Delete_statementContext ctx) {
69+
statementType = QueryInformation.StatementType.DELETE;
70+
return super.visitDelete_statement(ctx);
4671
}
4772

4873
@Override

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,29 @@
2929
class HibernateQueryInformation extends QueryInformation {
3030

3131
private final boolean hasCte;
32-
32+
3333
private final boolean hasFromFunction;
34-
3534

3635
public HibernateQueryInformation(@Nullable String alias, List<QueryToken> projection,
37-
boolean hasConstructorExpression, boolean hasCte,boolean hasFromFunction) {
36+
boolean hasConstructorExpression, boolean hasCte, boolean hasFromFunction) {
3837
super(alias, projection, hasConstructorExpression);
3938
this.hasCte = hasCte;
4039
this.hasFromFunction = hasFromFunction;
4140
}
4241

42+
public HibernateQueryInformation(@Nullable String alias, List<QueryToken> projection,
43+
boolean hasConstructorExpression, StatementType statementType, boolean hasCte, boolean hasFromFunction) {
44+
super(alias, projection, hasConstructorExpression, statementType);
45+
this.hasCte = hasCte;
46+
this.hasFromFunction = hasFromFunction;
47+
}
48+
4349
public boolean hasCte() {
4450
return hasCte;
4551
}
46-
52+
4753
public boolean hasFromFunction() {
4854
return hasFromFunction;
4955
}
50-
56+
5157
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,36 @@ class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIn
4242
private boolean hasConstructorExpression = false;
4343
private boolean hasCte = false;
4444
private boolean hasFromFunction = false;
45+
private QueryInformation.StatementType statementType = QueryInformation.StatementType.SELECT;
4546

4647
@Override
4748
public HibernateQueryInformation getParsedQueryInformation() {
4849
return new HibernateQueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection,
49-
hasConstructorExpression, hasCte, hasFromFunction);
50+
hasConstructorExpression, statementType, hasCte, hasFromFunction);
51+
}
52+
53+
@Override
54+
public Void visitSelectStatement(HqlParser.SelectStatementContext ctx) {
55+
statementType = QueryInformation.StatementType.SELECT;
56+
return super.visitSelectStatement(ctx);
57+
}
58+
59+
@Override
60+
public Void visitInsertStatement(HqlParser.InsertStatementContext ctx) {
61+
statementType = QueryInformation.StatementType.INSERT;
62+
return super.visitInsertStatement(ctx);
63+
}
64+
65+
@Override
66+
public Void visitUpdateStatement(HqlParser.UpdateStatementContext ctx) {
67+
statementType = QueryInformation.StatementType.UPDATE;
68+
return super.visitUpdateStatement(ctx);
69+
}
70+
71+
@Override
72+
public Void visitDeleteStatement(HqlParser.DeleteStatementContext ctx) {
73+
statementType = QueryInformation.StatementType.DELETE;
74+
return super.visitDeleteStatement(ctx);
5075
}
5176

5277
@Override

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,16 @@ private String doApplySorting(Sort sort, @Nullable String alias) {
343343
String queryString = query.getQueryString();
344344
Assert.hasText(queryString, "Query must not be null or empty");
345345

346-
if (this.parsedType != ParsedType.SELECT || sort.isUnsorted()) {
346+
if (this.parsedType != ParsedType.SELECT) {
347+
if (!sort.isUnsorted()) {
348+
throw new IllegalStateException(String.format(
349+
"Cannot apply sorting to %s statement. Sorting is only supported for SELECT statements.",
350+
this.parsedType));
351+
}
352+
return queryString;
353+
}
354+
355+
if (sort.isUnsorted()) {
347356
return queryString;
348357
}
349358

@@ -383,7 +392,9 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab
383392
public String createCountQueryFor(@Nullable String countProjection) {
384393

385394
if (this.parsedType != ParsedType.SELECT) {
386-
return this.query.getQueryString();
395+
throw new IllegalStateException(String.format(
396+
"Cannot derive count query for %s statement. Count queries are only supported for SELECT statements.",
397+
this.parsedType));
387398
}
388399

389400
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty");

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ public DeclaredQuery getQuery() {
220220

221221
@Override
222222
public String rewrite(QueryRewriteInformation rewriteInformation) {
223+
224+
if (!queryInformation.isSelectStatement() && !rewriteInformation.getSort().isUnsorted()) {
225+
throw new IllegalStateException(String.format(
226+
"Cannot apply sorting to %s statement. Sorting is only supported for SELECT statements.",
227+
queryInformation.getStatementType()));
228+
}
229+
223230
return QueryRenderer.TokenRenderer.render(
224231
sortFunction.apply(rewriteInformation.getSort(), this.queryInformation, rewriteInformation.getReturnedType())
225232
.visit(context));
@@ -232,6 +239,13 @@ public String rewrite(QueryRewriteInformation rewriteInformation) {
232239
*/
233240
@Override
234241
public String createCountQueryFor(@Nullable String countProjection) {
242+
243+
if (!queryInformation.isSelectStatement()) {
244+
throw new IllegalStateException(String.format(
245+
"Cannot derive count query for %s statement. Count queries are only supported for SELECT statements.",
246+
queryInformation.getStatementType()));
247+
}
248+
235249
return QueryRenderer.TokenRenderer
236250
.render(countQueryFunction.apply(countProjection, this.queryInformation).visit(context));
237251
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,36 @@ class JpqlQueryIntrospector extends JpqlBaseVisitor<Void> implements ParsedQuery
3838
private @Nullable List<QueryToken> projection;
3939
private boolean projectionProcessed;
4040
private boolean hasConstructorExpression = false;
41+
private QueryInformation.StatementType statementType = QueryInformation.StatementType.SELECT;
4142

4243
@Override
4344
public QueryInformation getParsedQueryInformation() {
4445
return new QueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection,
45-
hasConstructorExpression);
46+
hasConstructorExpression, statementType);
47+
}
48+
49+
@Override
50+
public Void visitSelectQuery(JpqlParser.SelectQueryContext ctx) {
51+
statementType = QueryInformation.StatementType.SELECT;
52+
return super.visitSelectQuery(ctx);
53+
}
54+
55+
@Override
56+
public Void visitFromQuery(JpqlParser.FromQueryContext ctx) {
57+
statementType = QueryInformation.StatementType.SELECT;
58+
return super.visitFromQuery(ctx);
59+
}
60+
61+
@Override
62+
public Void visitUpdate_statement(JpqlParser.Update_statementContext ctx) {
63+
statementType = QueryInformation.StatementType.UPDATE;
64+
return super.visitUpdate_statement(ctx);
65+
}
66+
67+
@Override
68+
public Void visitDelete_statement(JpqlParser.Delete_statementContext ctx) {
69+
statementType = QueryInformation.StatementType.DELETE;
70+
return super.visitDelete_statement(ctx);
4671
}
4772

4873
@Override

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,18 @@ class QueryInformation {
3333

3434
private final boolean hasConstructorExpression;
3535

36+
private final StatementType statementType;
37+
3638
QueryInformation(@Nullable String alias, List<QueryToken> projection, boolean hasConstructorExpression) {
39+
this(alias, projection, hasConstructorExpression, StatementType.SELECT);
40+
}
41+
42+
QueryInformation(@Nullable String alias, List<QueryToken> projection, boolean hasConstructorExpression,
43+
StatementType statementType) {
3744
this.alias = alias;
3845
this.projection = projection;
3946
this.hasConstructorExpression = hasConstructorExpression;
47+
this.statementType = statementType;
4048
}
4149

4250
/**
@@ -61,4 +69,59 @@ public List<QueryToken> getProjection() {
6169
public boolean hasConstructorExpression() {
6270
return hasConstructorExpression;
6371
}
72+
73+
/**
74+
* @return the statement type of the query.
75+
* @since 4.0
76+
*/
77+
public StatementType getStatementType() {
78+
return statementType;
79+
}
80+
81+
/**
82+
* @return {@code true} if the query is a SELECT statement.
83+
* @since 4.0
84+
*/
85+
public boolean isSelectStatement() {
86+
return statementType == StatementType.SELECT;
87+
}
88+
89+
/**
90+
* Enum representing the type of SQL/JPQL statement.
91+
*
92+
* @since 4.0
93+
*/
94+
enum StatementType {
95+
96+
/**
97+
* SELECT statement.
98+
*/
99+
SELECT,
100+
101+
/**
102+
* INSERT statement.
103+
*/
104+
INSERT,
105+
106+
/**
107+
* UPDATE statement.
108+
*/
109+
UPDATE,
110+
111+
/**
112+
* DELETE statement.
113+
*/
114+
DELETE,
115+
116+
/**
117+
* MERGE statement.
118+
*/
119+
MERGE,
120+
121+
/**
122+
* Other statement types.
123+
*/
124+
OTHER
125+
}
126+
64127
}

0 commit comments

Comments
 (0)