Skip to content

Commit 7b8eaeb

Browse files
committed
HHH-19888 Ensure static offset is always respected in FetchPlusOffsetParameterBinder
1 parent 1620d49 commit 7b8eaeb

File tree

2 files changed

+155
-2
lines changed

2 files changed

+155
-2
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.community.dialect;
6+
7+
import java.util.List;
8+
9+
import org.hibernate.cfg.AvailableSettings;
10+
import org.hibernate.dialect.H2Dialect;
11+
import org.hibernate.dialect.sql.ast.H2SqlAstTranslator;
12+
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
13+
import org.hibernate.engine.spi.SessionFactoryImplementor;
14+
import org.hibernate.query.common.FetchClauseType;
15+
import org.hibernate.sql.ast.Clause;
16+
import org.hibernate.sql.ast.SqlAstTranslator;
17+
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
18+
import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
19+
import org.hibernate.sql.ast.tree.Statement;
20+
import org.hibernate.sql.ast.tree.expression.Expression;
21+
import org.hibernate.sql.ast.tree.select.QueryPart;
22+
import org.hibernate.sql.exec.spi.JdbcOperation;
23+
24+
import org.hibernate.testing.orm.junit.DomainModel;
25+
import org.hibernate.testing.orm.junit.Jira;
26+
import org.hibernate.testing.orm.junit.RequiresDialect;
27+
import org.hibernate.testing.orm.junit.ServiceRegistry;
28+
import org.hibernate.testing.orm.junit.SessionFactory;
29+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
30+
import org.hibernate.testing.orm.junit.SettingProvider;
31+
import org.junit.jupiter.api.BeforeEach;
32+
import org.junit.jupiter.api.Test;
33+
34+
import jakarta.persistence.Entity;
35+
import jakarta.persistence.Id;
36+
37+
import static org.junit.jupiter.api.Assertions.assertEquals;
38+
39+
@RequiresDialect(H2Dialect.class)
40+
@DomainModel(annotatedClasses = FetchPlusOffsetParameterTest.Book.class)
41+
@SessionFactory
42+
@ServiceRegistry(
43+
settingProviders = @SettingProvider(settingName = AvailableSettings.DIALECT, provider = FetchPlusOffsetParameterTest.TestSettingProvider.class)
44+
)
45+
@Jira("https://hibernate.atlassian.net/browse/HHH-19888")
46+
public class FetchPlusOffsetParameterTest {
47+
48+
@BeforeEach
49+
protected void prepareTest(SessionFactoryScope scope) {
50+
scope.inTransaction(
51+
(session) -> {
52+
for ( int i = 1; i <= 3; i++ ) {
53+
session.persist( new Book( i, "Book " + i ) );
54+
}
55+
}
56+
);
57+
}
58+
59+
@Test
60+
public void testStaticOffset(SessionFactoryScope scope) {
61+
scope.inTransaction(
62+
(session) -> {
63+
final List<Book> books = session.createSelectionQuery(
64+
"from Book b order by b.id",
65+
Book.class
66+
)
67+
.setFirstResult( 2 )
68+
.setMaxResults( 1 ).getResultList();
69+
// The custom dialect will fetch offset + limit + staticOffset rows
70+
// Since staticOffset is -1, it must yield 2 rows
71+
assertEquals( 2, books.size() );
72+
}
73+
);
74+
}
75+
76+
@Entity(name = "Book")
77+
public static class Book {
78+
@Id
79+
private Integer id;
80+
private String title;
81+
82+
public Book() {
83+
}
84+
85+
public Book(Integer id, String title) {
86+
this.id = id;
87+
this.title = title;
88+
}
89+
}
90+
91+
92+
public static class TestSettingProvider implements SettingProvider.Provider<String> {
93+
94+
@Override
95+
public String getSetting() {
96+
return TestDialect.class.getName();
97+
}
98+
}
99+
100+
public static class TestDialect extends H2Dialect {
101+
102+
public TestDialect(DialectResolutionInfo info) {
103+
super( info );
104+
}
105+
106+
public TestDialect() {
107+
}
108+
109+
@Override
110+
public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
111+
return new StandardSqlAstTranslatorFactory() {
112+
@Override
113+
protected <T extends JdbcOperation> SqlAstTranslator<T> buildTranslator(
114+
SessionFactoryImplementor sessionFactory, Statement statement) {
115+
return new H2SqlAstTranslator<>( sessionFactory, statement ) {
116+
@Override
117+
public void visitOffsetFetchClause(QueryPart queryPart) {
118+
final Expression offsetClauseExpression;
119+
final Expression fetchClauseExpression;
120+
if ( queryPart.isRoot() && hasLimit() ) {
121+
prepareLimitOffsetParameters();
122+
offsetClauseExpression = getOffsetParameter();
123+
fetchClauseExpression = getLimitParameter();
124+
}
125+
else {
126+
assert queryPart.getFetchClauseType() == FetchClauseType.ROWS_ONLY;
127+
offsetClauseExpression = queryPart.getOffsetClauseExpression();
128+
fetchClauseExpression = queryPart.getFetchClauseExpression();
129+
}
130+
if ( offsetClauseExpression != null && fetchClauseExpression != null ) {
131+
appendSql( " fetch first " );
132+
getClauseStack().push( Clause.FETCH );
133+
try {
134+
renderFetchPlusOffsetExpressionAsSingleParameter(
135+
fetchClauseExpression,
136+
offsetClauseExpression,
137+
-1
138+
);
139+
}
140+
finally {
141+
getClauseStack().pop();
142+
}
143+
appendSql( " rows only" );
144+
}
145+
}
146+
};
147+
}
148+
};
149+
}
150+
}
151+
}
152+
153+

hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4683,12 +4683,12 @@ public void bindParameterValue(
46834683
if ( binding == null ) {
46844684
throw new ExecutionException( "JDBC parameter value not bound - " + offsetParameter );
46854685
}
4686-
offsetValue = ((Number) binding.getBindValue()).intValue() + staticOffset;
4686+
offsetValue = ((Number) binding.getBindValue()).intValue();
46874687
}
46884688
//noinspection unchecked
46894689
fetchParameter.getExpressionType().getSingleJdbcMapping().getJdbcValueBinder().bind(
46904690
statement,
4691-
bindValue.intValue() + offsetValue,
4691+
bindValue.intValue() + offsetValue + staticOffset,
46924692
startPosition,
46934693
executionContext.getSession()
46944694
);

0 commit comments

Comments
 (0)