Skip to content

Commit 4896997

Browse files
committed
HHH-19397 allow LIMIT + OFFSET without ORDER BY
1 parent b0aebfa commit 4896997

File tree

3 files changed

+86
-55
lines changed

3 files changed

+86
-55
lines changed

hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ queryExpression
159159
* A query with an optional 'order by' clause
160160
*/
161161
orderedQuery
162-
: query queryOrder? # QuerySpecExpression
163-
| LEFT_PAREN queryExpression RIGHT_PAREN queryOrder? # NestedQueryExpression
164-
| queryOrder # QueryOrderExpression
162+
: query orderByClause? limitOffset # QuerySpecExpression
163+
| LEFT_PAREN queryExpression RIGHT_PAREN orderByClause? limitOffset # NestedQueryExpression
164+
| orderByClause limitOffset # QueryOrderExpression
165165
;
166166

167167
/**
@@ -174,10 +174,10 @@ setOperator
174174
;
175175

176176
/**
177-
* The 'order by' clause and optional subclauses for limiting and pagination
177+
* Optional subclauses for limiting and pagination
178178
*/
179-
queryOrder
180-
: orderByClause limitClause? offsetClause? fetchClause?
179+
limitOffset
180+
: limitClause? offsetClause? fetchClause?
181181
;
182182

183183
/**

hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -985,35 +985,32 @@ public SqmQueryPart<?> visitQueryOrderExpression(HqlParser.QueryOrderExpressionC
985985
final SqmFromClause fromClause = buildInferredFromClause(null);
986986
sqmQuerySpec.setFromClause( fromClause );
987987
sqmQuerySpec.setSelectClause( buildInferredSelectClause( fromClause ) );
988-
visitQueryOrder( sqmQuerySpec, ctx.queryOrder() );
988+
visitOrderBy( sqmQuerySpec, ctx.orderByClause() );
989+
visitLimitOffset( sqmQuerySpec, ctx.limitOffset() );
989990
return sqmQuerySpec;
990991
}
991992

992993
@Override
993994
public SqmQueryPart<?> visitQuerySpecExpression(HqlParser.QuerySpecExpressionContext ctx) {
994995
final SqmQueryPart<?> queryPart = visitQuery( ctx.query() );
995-
final HqlParser.QueryOrderContext queryOrderContext = ctx.queryOrder();
996-
if ( queryOrderContext != null ) {
997-
visitQueryOrder( queryPart, queryOrderContext );
998-
}
996+
visitOrderBy( queryPart, ctx.orderByClause() );
997+
visitLimitOffset( queryPart, ctx.limitOffset() );
999998
return queryPart;
1000999
}
10011000

10021001
@Override
10031002
public SqmQueryPart<?> visitNestedQueryExpression(HqlParser.NestedQueryExpressionContext ctx) {
10041003
final SqmQueryPart<?> queryPart = (SqmQueryPart<?>) ctx.queryExpression().accept( this );
1005-
final HqlParser.QueryOrderContext queryOrderContext = ctx.queryOrder();
1006-
if ( queryOrderContext != null ) {
1007-
final SqmCreationProcessingState firstProcessingState = processingStateStack.pop();
1008-
processingStateStack.push(
1009-
new SqmQueryPartCreationProcessingStateStandardImpl(
1010-
processingStateStack.getCurrent(),
1011-
firstProcessingState.getProcessingQuery(),
1012-
this
1013-
)
1014-
);
1015-
visitQueryOrder( queryPart, queryOrderContext);
1016-
}
1004+
final SqmCreationProcessingState firstProcessingState = processingStateStack.pop();
1005+
processingStateStack.push(
1006+
new SqmQueryPartCreationProcessingStateStandardImpl(
1007+
processingStateStack.getCurrent(),
1008+
firstProcessingState.getProcessingQuery(),
1009+
this
1010+
)
1011+
);
1012+
visitOrderBy( queryPart, ctx.orderByClause() );
1013+
visitLimitOffset( queryPart, ctx.limitOffset() );
10171014
return queryPart;
10181015
}
10191016

@@ -1120,44 +1117,38 @@ public SetOperator visitSetOperator(HqlParser.SetOperatorContext ctx) {
11201117
};
11211118
}
11221119

1123-
protected void visitQueryOrder(SqmQueryPart<?> sqmQueryPart, HqlParser.QueryOrderContext ctx) {
1124-
if ( ctx == null ) {
1125-
return;
1126-
}
1127-
final SqmOrderByClause orderByClause;
1128-
final HqlParser.OrderByClauseContext orderByClauseContext = ctx.orderByClause();
1129-
if ( orderByClauseContext != null ) {
1130-
if ( creationOptions.useStrictJpaCompliance() && processingStateStack.depth() > 1 ) {
1131-
throw new StrictJpaComplianceViolation(
1132-
StrictJpaComplianceViolation.Type.SUBQUERY_ORDER_BY
1133-
);
1134-
}
1120+
protected void visitLimitOffset(SqmQueryPart<?> sqmQueryPart, HqlParser.LimitOffsetContext ctx) {
1121+
if ( ctx != null ) {
1122+
final HqlParser.LimitClauseContext limitClauseContext = ctx.limitClause();
1123+
final HqlParser.OffsetClauseContext offsetClauseContext = ctx.offsetClause();
1124+
final HqlParser.FetchClauseContext fetchClauseContext = ctx.fetchClause();
1125+
if ( limitClauseContext != null || offsetClauseContext != null || fetchClauseContext != null ) {
1126+
if ( getCreationOptions().useStrictJpaCompliance() ) {
1127+
throw new StrictJpaComplianceViolation(
1128+
StrictJpaComplianceViolation.Type.LIMIT_OFFSET_CLAUSE
1129+
);
1130+
}
11351131

1136-
orderByClause = visitOrderByClause( orderByClauseContext );
1137-
sqmQueryPart.setOrderByClause( orderByClause );
1138-
}
1139-
else {
1140-
orderByClause = null;
1141-
}
1132+
if ( processingStateStack.depth() > 1 && sqmQueryPart.getOrderByClause() == null ) {
1133+
throw new SemanticException(
1134+
"A 'limit', 'offset', or 'fetch' clause requires an 'order by' clause when used in a subquery",
1135+
query
1136+
);
1137+
}
11421138

1143-
final HqlParser.LimitClauseContext limitClauseContext = ctx.limitClause();
1144-
final HqlParser.OffsetClauseContext offsetClauseContext = ctx.offsetClause();
1145-
final HqlParser.FetchClauseContext fetchClauseContext = ctx.fetchClause();
1146-
if ( limitClauseContext != null || offsetClauseContext != null || fetchClauseContext != null ) {
1147-
if ( getCreationOptions().useStrictJpaCompliance() ) {
1148-
throw new StrictJpaComplianceViolation(
1149-
StrictJpaComplianceViolation.Type.LIMIT_OFFSET_CLAUSE
1150-
);
1139+
setOffsetFetchLimit( sqmQueryPart, limitClauseContext, offsetClauseContext, fetchClauseContext );
11511140
}
1141+
}
1142+
}
11521143

1153-
if ( processingStateStack.depth() > 1 && orderByClause == null ) {
1154-
throw new SemanticException(
1155-
"A 'limit', 'offset', or 'fetch' clause requires an 'order by' clause when used in a subquery",
1156-
query
1144+
protected void visitOrderBy(SqmQueryPart<?> sqmQueryPart, HqlParser.OrderByClauseContext ctx) {
1145+
if ( ctx != null ) {
1146+
if ( creationOptions.useStrictJpaCompliance() && processingStateStack.depth() > 1 ) {
1147+
throw new StrictJpaComplianceViolation(
1148+
StrictJpaComplianceViolation.Type.SUBQUERY_ORDER_BY
11571149
);
11581150
}
1159-
1160-
setOffsetFetchLimit(sqmQueryPart, limitClauseContext, offsetClauseContext, fetchClauseContext);
1151+
sqmQueryPart.setOrderByClause( visitOrderByClause( ctx ) );
11611152
}
11621153
}
11631154

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.query.hql;
6+
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.GeneratedValue;
9+
import jakarta.persistence.Id;
10+
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
11+
import org.hibernate.testing.orm.junit.Jpa;
12+
import org.junit.jupiter.api.Test;
13+
14+
import java.util.UUID;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
18+
@Jpa(annotatedClasses = LimitOffsetTest.Sortable.class)
19+
class LimitOffsetTest {
20+
@Test
21+
void testLimitOffset(EntityManagerFactoryScope scope) {
22+
scope.inTransaction( session -> {
23+
session.persist( new Sortable() );
24+
session.persist( new Sortable() );
25+
session.persist( new Sortable() );
26+
session.persist( new Sortable() );
27+
} );
28+
scope.inTransaction( session -> {
29+
assertEquals( 2, session.createQuery( "from Sortable limit 2" ).getResultList().size() );
30+
assertEquals( 2, session.createQuery( "from Sortable offset 2" ).getResultList().size() );
31+
assertEquals( 1, session.createQuery( "from Sortable limit 1 offset 1" ).getResultList().size() );
32+
} );
33+
}
34+
@Entity(name = "Sortable")
35+
static class Sortable {
36+
@Id
37+
@GeneratedValue
38+
UUID uuid;
39+
}
40+
}

0 commit comments

Comments
 (0)