Skip to content

Commit e1be294

Browse files
kssuminmp911de
authored andcommitted
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. Signed-off-by: kssumin <ksoomin25@gmail.com> Original pull request: #4052 Closes #2856
1 parent 998399c commit e1be294

File tree

10 files changed

+321
-23
lines changed

10 files changed

+321
-23
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class DefaultQueryEnhancer implements QueryEnhancer {
2727

2828
private final QueryProvider query;
2929
private final boolean hasConstructorExpression;
30-
private final @Nullable String alias;
30+
private final @Nullable String alias;
3131
private final String projection;
3232

3333
public DefaultQueryEnhancer(QueryProvider query) {

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

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* Value object capturing introspection details of a parsed query.
2424
*
2525
* @author Mark Paluch
26+
* @author kssumin
2627
* @since 3.5
2728
*/
2829
class QueryInformation {
@@ -33,10 +34,18 @@ class QueryInformation {
3334

3435
private final boolean hasConstructorExpression;
3536

37+
private final StatementType statementType;
38+
3639
QueryInformation(@Nullable String alias, List<QueryToken> projection, boolean hasConstructorExpression) {
40+
this(alias, projection, hasConstructorExpression, StatementType.SELECT);
41+
}
42+
43+
QueryInformation(@Nullable String alias, List<QueryToken> projection, boolean hasConstructorExpression,
44+
StatementType statementType) {
3745
this.alias = alias;
3846
this.projection = projection;
3947
this.hasConstructorExpression = hasConstructorExpression;
48+
this.statementType = statementType;
4049
}
4150

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

0 commit comments

Comments
 (0)