diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index b9a113092a4d..7c54e02d90e5 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -167,9 +167,9 @@ queryExpression * A query with an optional 'order by' clause */ orderedQuery - : query queryOrder? # QuerySpecExpression - | LEFT_PAREN queryExpression RIGHT_PAREN queryOrder? # NestedQueryExpression - | queryOrder # QueryOrderExpression + : query orderByClause? limitOffset # QuerySpecExpression + | LEFT_PAREN queryExpression RIGHT_PAREN orderByClause? limitOffset # NestedQueryExpression + | orderByClause limitOffset # QueryOrderExpression ; /** @@ -182,10 +182,10 @@ setOperator ; /** - * The 'order by' clause and optional subclauses for limiting and pagination + * Optional subclauses for limiting and pagination */ -queryOrder - : orderByClause limitClause? offsetClause? fetchClause? +limitOffset + : limitClause? offsetClause? fetchClause? ; /** diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 17b152e5642f..de2ed65269ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -1031,35 +1031,32 @@ public SqmQueryPart visitQueryOrderExpression(HqlParser.QueryOrderExpressionC final SqmFromClause fromClause = buildInferredFromClause(null); sqmQuerySpec.setFromClause( fromClause ); sqmQuerySpec.setSelectClause( buildInferredSelectClause( fromClause ) ); - visitQueryOrder( sqmQuerySpec, ctx.queryOrder() ); + visitOrderBy( sqmQuerySpec, ctx.orderByClause() ); + visitLimitOffset( sqmQuerySpec, ctx.limitOffset() ); return sqmQuerySpec; } @Override public SqmQueryPart visitQuerySpecExpression(HqlParser.QuerySpecExpressionContext ctx) { final SqmQueryPart queryPart = visitQuery( ctx.query() ); - final HqlParser.QueryOrderContext queryOrderContext = ctx.queryOrder(); - if ( queryOrderContext != null ) { - visitQueryOrder( queryPart, queryOrderContext ); - } + visitOrderBy( queryPart, ctx.orderByClause() ); + visitLimitOffset( queryPart, ctx.limitOffset() ); return queryPart; } @Override public SqmQueryPart visitNestedQueryExpression(HqlParser.NestedQueryExpressionContext ctx) { final SqmQueryPart queryPart = (SqmQueryPart) ctx.queryExpression().accept( this ); - final HqlParser.QueryOrderContext queryOrderContext = ctx.queryOrder(); - if ( queryOrderContext != null ) { - final SqmCreationProcessingState firstProcessingState = processingStateStack.pop(); - processingStateStack.push( - new SqmQueryPartCreationProcessingStateStandardImpl( - processingStateStack.getCurrent(), - firstProcessingState.getProcessingQuery(), - this - ) - ); - visitQueryOrder( queryPart, queryOrderContext); - } + final SqmCreationProcessingState firstProcessingState = processingStateStack.pop(); + processingStateStack.push( + new SqmQueryPartCreationProcessingStateStandardImpl( + processingStateStack.getCurrent(), + firstProcessingState.getProcessingQuery(), + this + ) + ); + visitOrderBy( queryPart, ctx.orderByClause() ); + visitLimitOffset( queryPart, ctx.limitOffset() ); return queryPart; } @@ -1178,44 +1175,36 @@ public SetOperator visitSetOperator(HqlParser.SetOperatorContext ctx) { } } - protected void visitQueryOrder(SqmQueryPart sqmQueryPart, HqlParser.QueryOrderContext ctx) { - if ( ctx == null ) { - return; - } - final SqmOrderByClause orderByClause; - final HqlParser.OrderByClauseContext orderByClauseContext = ctx.orderByClause(); - if ( orderByClauseContext != null ) { - if ( creationOptions.useStrictJpaCompliance() && processingStateStack.depth() > 1 ) { - throw new StrictJpaComplianceViolation( - StrictJpaComplianceViolation.Type.SUBQUERY_ORDER_BY - ); + protected void visitLimitOffset(SqmQueryPart sqmQueryPart, HqlParser.LimitOffsetContext ctx) { + if (ctx != null) { + final HqlParser.LimitClauseContext limitClauseContext = ctx.limitClause(); + final HqlParser.OffsetClauseContext offsetClauseContext = ctx.offsetClause(); + final HqlParser.FetchClauseContext fetchClauseContext = ctx.fetchClause(); + if (limitClauseContext != null || offsetClauseContext != null || fetchClauseContext != null) { + if (getCreationOptions().useStrictJpaCompliance()) { + throw new StrictJpaComplianceViolation( + StrictJpaComplianceViolation.Type.LIMIT_OFFSET_CLAUSE + ); + } + if ( processingStateStack.depth() > 1 && sqmQueryPart.getOrderByClause() == null ) { + throw new SemanticException( + "A 'limit', 'offset', or 'fetch' clause requires an 'order by' clause when used in a subquery", + query + ); + } + setOffsetFetchLimit( sqmQueryPart, limitClauseContext, offsetClauseContext, fetchClauseContext ); } - - orderByClause = visitOrderByClause( orderByClauseContext ); - sqmQueryPart.setOrderByClause( orderByClause ); - } - else { - orderByClause = null; } + } - final HqlParser.LimitClauseContext limitClauseContext = ctx.limitClause(); - final HqlParser.OffsetClauseContext offsetClauseContext = ctx.offsetClause(); - final HqlParser.FetchClauseContext fetchClauseContext = ctx.fetchClause(); - if ( limitClauseContext != null || offsetClauseContext != null || fetchClauseContext != null ) { - if ( getCreationOptions().useStrictJpaCompliance() ) { + protected void visitOrderBy(SqmQueryPart sqmQueryPart, HqlParser.OrderByClauseContext ctx) { + if ( ctx != null ) { + if ( creationOptions.useStrictJpaCompliance() && processingStateStack.depth() > 1 ) { throw new StrictJpaComplianceViolation( - StrictJpaComplianceViolation.Type.LIMIT_OFFSET_CLAUSE - ); - } - - if ( processingStateStack.depth() > 1 && orderByClause == null ) { - throw new SemanticException( - "A 'limit', 'offset', or 'fetch' clause requires an 'order by' clause when used in a subquery", - query + StrictJpaComplianceViolation.Type.SUBQUERY_ORDER_BY ); } - - setOffsetFetchLimit(sqmQueryPart, limitClauseContext, offsetClauseContext, fetchClauseContext); + sqmQueryPart.setOrderByClause( visitOrderByClause( ctx ) ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LimitOffsetTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LimitOffsetTest.java new file mode 100644 index 000000000000..1408f1d25702 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LimitOffsetTest.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Jpa(annotatedClasses = LimitOffsetTest.Sortable.class) +class LimitOffsetTest { + @Test + void testLimitOffset(EntityManagerFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Sortable() ); + session.persist( new Sortable() ); + session.persist( new Sortable() ); + session.persist( new Sortable() ); + } ); + scope.inTransaction( session -> { + assertEquals( 2, session.createQuery( "from Sortable limit 2" ).getResultList().size() ); + assertEquals( 2, session.createQuery( "from Sortable offset 2" ).getResultList().size() ); + assertEquals( 1, session.createQuery( "from Sortable limit 1 offset 1" ).getResultList().size() ); + } ); + } + @Entity(name = "Sortable") + static class Sortable { + @Id + @GeneratedValue + UUID uuid; + } +} \ No newline at end of file