diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java new file mode 100644 index 000000000000..f78f4ae144d8 --- /dev/null +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import java.util.List; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.sql.ast.H2SqlAstTranslator; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.common.FetchClauseType; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.exec.spi.JdbcOperation; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@RequiresDialect(H2Dialect.class) +@DomainModel(annotatedClasses = FetchPlusOffsetParameterTest.Book.class) +@SessionFactory +@ServiceRegistry( + settingProviders = @SettingProvider(settingName = AvailableSettings.DIALECT, provider = FetchPlusOffsetParameterTest.TestSettingProvider.class) +) +@Jira("https://hibernate.atlassian.net/browse/HHH-19888") +public class FetchPlusOffsetParameterTest { + + @BeforeEach + protected void prepareTest(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + for ( int i = 1; i <= 3; i++ ) { + session.persist( new Book( i, "Book " + i ) ); + } + } + ); + } + + @Test + public void testStaticOffset(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + final List books = session.createSelectionQuery( + "from Book b order by b.id", + Book.class + ) + .setFirstResult( 2 ) + .setMaxResults( 1 ).getResultList(); + // The custom dialect will fetch offset + limit + staticOffset rows + // Since staticOffset is -1, it must yield 2 rows + assertEquals( 2, books.size() ); + } + ); + } + + @Entity(name = "Book") + public static class Book { + @Id + private Integer id; + private String title; + + public Book() { + } + + public Book(Integer id, String title) { + this.id = id; + this.title = title; + } + } + + + public static class TestSettingProvider implements SettingProvider.Provider { + + @Override + public String getSetting() { + return TestDialect.class.getName(); + } + } + + public static class TestDialect extends H2Dialect { + + public TestDialect(DialectResolutionInfo info) { + super( info ); + } + + public TestDialect() { + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new H2SqlAstTranslator<>( sessionFactory, statement ) { + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + final Expression offsetClauseExpression; + final Expression fetchClauseExpression; + if ( queryPart.isRoot() && hasLimit() ) { + prepareLimitOffsetParameters(); + offsetClauseExpression = getOffsetParameter(); + fetchClauseExpression = getLimitParameter(); + } + else { + assert queryPart.getFetchClauseType() == FetchClauseType.ROWS_ONLY; + offsetClauseExpression = queryPart.getOffsetClauseExpression(); + fetchClauseExpression = queryPart.getFetchClauseExpression(); + } + if ( offsetClauseExpression != null && fetchClauseExpression != null ) { + appendSql( " fetch first " ); + getClauseStack().push( Clause.FETCH ); + try { + renderFetchPlusOffsetExpressionAsSingleParameter( + fetchClauseExpression, + offsetClauseExpression, + -1 + ); + } + finally { + getClauseStack().pop(); + } + appendSql( " rows only" ); + } + } + }; + } + }; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 020938e419dd..73ab28372b9b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -4766,12 +4766,12 @@ public void bindParameterValue( if ( binding == null ) { throw new ExecutionException( "JDBC parameter value not bound - " + offsetParameter ); } - offsetValue = ((Number) binding.getBindValue()).intValue() + staticOffset; + offsetValue = ((Number) binding.getBindValue()).intValue(); } //noinspection unchecked fetchParameter.getExpressionType().getSingleJdbcMapping().getJdbcValueBinder().bind( statement, - bindValue.intValue() + offsetValue, + bindValue.intValue() + offsetValue + staticOffset, startPosition, executionContext.getSession() );