Skip to content

Commit 1ea2c9a

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 1ea2c9a

File tree

10 files changed

+364
-22
lines changed

10 files changed

+364
-22
lines changed

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,84 @@
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
/**
2125
* The implementation of the Regex-based {@link QueryEnhancer} using {@link QueryUtils}.
2226
*
2327
* @author Diego Krupitza
28+
* @author kssumin
2429
* @since 2.7.0
2530
*/
2631
class DefaultQueryEnhancer implements QueryEnhancer {
2732

33+
private static final Pattern STATEMENT_TYPE_PATTERN = Pattern.compile(
34+
"^\\s*(SELECT|FROM|INSERT|UPDATE|DELETE|MERGE)", Pattern.CASE_INSENSITIVE);
35+
2836
private final QueryProvider query;
2937
private final boolean hasConstructorExpression;
3038
private final @Nullable String alias;
3139
private final String projection;
40+
private final StatementType statementType;
3241

3342
public DefaultQueryEnhancer(QueryProvider query) {
3443
this.query = query;
3544
this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString());
3645
this.alias = QueryUtils.detectAlias(query.getQueryString());
3746
this.projection = QueryUtils.getProjection(this.query.getQueryString());
47+
this.statementType = detectStatementType(query.getQueryString());
3848
}
3949

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

4562
@Override
4663
public String createCountQueryFor(@Nullable String countProjection) {
4764

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

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

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*
2929
* @author Mark Paluch
3030
* @author Christoph Strobl
31+
* @author kssumin
3132
*/
3233
@SuppressWarnings("UnreachableCode")
3334
class EqlQueryIntrospector extends EqlBaseVisitor<Void> implements ParsedQueryIntrospector<QueryInformation> {
@@ -38,11 +39,36 @@ class EqlQueryIntrospector extends EqlBaseVisitor<Void> implements ParsedQueryIn
3839
private @Nullable List<QueryToken> projection;
3940
private boolean projectionProcessed;
4041
private boolean hasConstructorExpression = false;
42+
private QueryInformation.StatementType statementType = QueryInformation.StatementType.SELECT;
4143

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

4874
@Override

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,35 @@
2424
*
2525
* @author Mark Paluch
2626
* @author Oscar Fanchin
27+
* @author kssumin
2728
* @since 3.5
2829
*/
2930
class HibernateQueryInformation extends QueryInformation {
3031

3132
private final boolean hasCte;
32-
33+
3334
private final boolean hasFromFunction;
34-
3535

3636
public HibernateQueryInformation(@Nullable String alias, List<QueryToken> projection,
37-
boolean hasConstructorExpression, boolean hasCte,boolean hasFromFunction) {
37+
boolean hasConstructorExpression, boolean hasCte, boolean hasFromFunction) {
3838
super(alias, projection, hasConstructorExpression);
3939
this.hasCte = hasCte;
4040
this.hasFromFunction = hasFromFunction;
4141
}
4242

43+
public HibernateQueryInformation(@Nullable String alias, List<QueryToken> projection,
44+
boolean hasConstructorExpression, StatementType statementType, boolean hasCte, boolean hasFromFunction) {
45+
super(alias, projection, hasConstructorExpression, statementType);
46+
this.hasCte = hasCte;
47+
this.hasFromFunction = hasFromFunction;
48+
}
49+
4350
public boolean hasCte() {
4451
return hasCte;
4552
}
46-
53+
4754
public boolean hasFromFunction() {
4855
return hasFromFunction;
4956
}
50-
57+
5158
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*
3131
* @author Mark Paluch
3232
* @author Oscar Fanchin
33+
* @author kssumin
3334
*/
3435
@SuppressWarnings({ "UnreachableCode", "ConstantValue" })
3536
class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIntrospector<HibernateQueryInformation> {
@@ -42,11 +43,36 @@ class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIn
4243
private boolean hasConstructorExpression = false;
4344
private boolean hasCte = false;
4445
private boolean hasFromFunction = false;
46+
private QueryInformation.StatementType statementType = QueryInformation.StatementType.SELECT;
4547

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

5278
@Override

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
* @author Yanming Zhou
7070
* @author Christoph Strobl
7171
* @author Diego Pedregal
72+
* @author kssumin
7273
* @since 2.7.0
7374
*/
7475
public class JSqlParserQueryEnhancer implements QueryEnhancer {
@@ -343,7 +344,16 @@ private String doApplySorting(Sort sort, @Nullable String alias) {
343344
String queryString = query.getQueryString();
344345
Assert.hasText(queryString, "Query must not be null or empty");
345346

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

@@ -383,7 +393,9 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab
383393
public String createCountQueryFor(@Nullable String countProjection) {
384394

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

389401
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
*
4242
* @author Greg Turnquist
4343
* @author Mark Paluch
44+
* @author kssumin
4445
* @since 3.1
4546
* @see JpqlQueryParser
4647
* @see HqlQueryParser
@@ -220,6 +221,13 @@ public DeclaredQuery getQuery() {
220221

221222
@Override
222223
public String rewrite(QueryRewriteInformation rewriteInformation) {
224+
225+
if (!queryInformation.isSelectStatement() && !rewriteInformation.getSort().isUnsorted()) {
226+
throw new IllegalStateException(String.format(
227+
"Cannot apply sorting to %s statement. Sorting is only supported for SELECT statements.",
228+
queryInformation.getStatementType()));
229+
}
230+
223231
return QueryRenderer.TokenRenderer.render(
224232
sortFunction.apply(rewriteInformation.getSort(), this.queryInformation, rewriteInformation.getReturnedType())
225233
.visit(context));
@@ -232,6 +240,13 @@ public String rewrite(QueryRewriteInformation rewriteInformation) {
232240
*/
233241
@Override
234242
public String createCountQueryFor(@Nullable String countProjection) {
243+
244+
if (!queryInformation.isSelectStatement()) {
245+
throw new IllegalStateException(String.format(
246+
"Cannot derive count query for %s statement. Count queries are only supported for SELECT statements.",
247+
queryInformation.getStatementType()));
248+
}
249+
235250
return QueryRenderer.TokenRenderer
236251
.render(countQueryFunction.apply(countProjection, this.queryInformation).visit(context));
237252
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*
2929
* @author Mark Paluch
3030
* @author Christoph Strobl
31+
* @author kssumin
3132
*/
3233
@SuppressWarnings({ "UnreachableCode", "ConstantValue" })
3334
class JpqlQueryIntrospector extends JpqlBaseVisitor<Void> implements ParsedQueryIntrospector<QueryInformation> {
@@ -38,11 +39,36 @@ class JpqlQueryIntrospector extends JpqlBaseVisitor<Void> implements ParsedQuery
3839
private @Nullable List<QueryToken> projection;
3940
private boolean projectionProcessed;
4041
private boolean hasConstructorExpression = false;
42+
private QueryInformation.StatementType statementType = QueryInformation.StatementType.SELECT;
4143

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

4874
@Override

0 commit comments

Comments
 (0)