From 68f300e9408a4afb304fc101074078877e9db0c5 Mon Sep 17 00:00:00 2001 From: carlblan Date: Thu, 22 May 2025 22:41:03 +0000 Subject: [PATCH 1/2] Various fix for TimesTen Dialect, SqlAstTranslator, LimitHandler and SequenceSupport --- .../community/dialect/TimesTenDialect.java | 227 +++++++++++++++++- .../dialect/TimesTenSqlAstTranslator.java | 64 +++++ .../pagination/TimesTenLimitHandler.java | 44 +++- .../sequence/TimesTenSequenceSupport.java | 34 ++- .../sql/ast/spi/AbstractSqlAstTranslator.java | 37 --- 5 files changed, 351 insertions(+), 55 deletions(-) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java index aa30500db527..f5065b51e347 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java @@ -15,8 +15,11 @@ import org.hibernate.community.dialect.sequence.SequenceInformationExtractorTimesTenDatabaseImpl; import org.hibernate.community.dialect.sequence.TimesTenSequenceSupport; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.BooleanDecoder; import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.OracleTruncFunction; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.dialect.lock.LockingStrategy; import org.hibernate.dialect.lock.OptimisticForceIncrementLockingStrategy; import org.hibernate.dialect.lock.OptimisticLockingStrategy; @@ -34,6 +37,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.Lockable; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableInsertStrategy; @@ -42,6 +46,7 @@ import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -52,6 +57,15 @@ import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.type.BasicType; +import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.dialect.function.CurrentFunction; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import jakarta.persistence.GenerationType; +import java.util.Date; + import jakarta.persistence.TemporalType; import static org.hibernate.dialect.SimpleDatabaseVersion.ZERO_VERSION; @@ -59,14 +73,13 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; /** - * A SQL dialect for TimesTen 5.1. + * A SQL dialect for Oracle TimesTen *

* Known limitations: * joined-subclass support because of no CASE support in TimesTen * No support for subqueries that includes aggregation * - size() in HQL not supported * - user queries that does subqueries with aggregation - * No CLOB/BLOB support * No cascade delete support. * No Calendar support * No support for updating primary keys. @@ -90,6 +103,7 @@ protected String columnType(int sqlTypeCode) { // for the default Oracle type mode // TypeMode=0 case SqlTypes.BOOLEAN: + case SqlTypes.BIT: case SqlTypes.TINYINT: return "tt_tinyint"; case SqlTypes.SMALLINT: @@ -101,15 +115,26 @@ protected String columnType(int sqlTypeCode) { //note that 'binary_float'/'binary_double' might //be better mappings for Java Float/Double + case SqlTypes.VARCHAR: + case SqlTypes.LONGVARCHAR: + return "varchar2($l)"; + + case SqlTypes.LONGVARBINARY: + return "varbinary($l)"; + //'numeric'/'decimal' are synonyms for 'number' case SqlTypes.NUMERIC: case SqlTypes.DECIMAL: return "number($p,$s)"; + case SqlTypes.FLOAT: + return "binary_float"; + case SqlTypes.DOUBLE: + return "binary_double"; + case SqlTypes.DATE: return "tt_date"; case SqlTypes.TIME: return "tt_time"; - //`timestamp` has more precision than `tt_timestamp` case SqlTypes.TIMESTAMP_WITH_TIMEZONE: return "timestamp($p)"; @@ -157,22 +182,97 @@ public int getDefaultDecimalPrecision() { public void initializeFunctionRegistry(FunctionContributions functionContributions) { super.initializeFunctionRegistry(functionContributions); - CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + final TypeConfiguration typeConfiguration = functionContributions.getTypeConfiguration(); + CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + final BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); + final BasicType timestampType = basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ); + final BasicType stringType = basicTypeRegistry.resolve( StandardBasicTypes.STRING ); + final BasicType longType = basicTypeRegistry.resolve( StandardBasicTypes.LONG ); + final BasicTypeintType = basicTypeRegistry.resolve( StandardBasicTypes.INTEGER ); + + // String Functions functionFactory.trim2(); - functionFactory.soundex(); - functionFactory.trunc(); + functionFactory.characterLength_length( SqlAstNodeRenderingMode.DEFAULT ); + functionFactory.concat_pipeOperator(); functionFactory.toCharNumberDateTimestamp(); - functionFactory.ceiling_ceil(); + functionFactory.char_chr(); functionFactory.instr(); functionFactory.substr(); functionFactory.substring_substr(); - functionFactory.leftRight_substr(); - functionFactory.char_chr(); - functionFactory.rownumRowid(); - functionFactory.sysdate(); + functionFactory.soundex(); + + // Date/Time Functions + functionContributions.getFunctionRegistry().register( + "sysdate", new CurrentFunction("sysdate", "sysdate", timestampType) + ); + functionContributions.getFunctionRegistry().register( + "getdate", new CurrentFunction("getdate", "getdate()", timestampType ) + ); + + // Multi-param date dialect functions functionFactory.addMonths(); functionFactory.monthsBetween(); + // Math functions + functionFactory.ceiling_ceil(); + functionFactory.radians_acos(); + functionFactory.degrees_acos(); + functionFactory.sinh(); + functionFactory.tanh(); + functionContributions.getFunctionRegistry().register( + "trunc", + new OracleTruncFunction( functionContributions.getTypeConfiguration() ) + ); + functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); + functionFactory.round(); + + // Bitwise functions + functionContributions.getFunctionRegistry() + .patternDescriptorBuilder( "bitor", "(?1+?2-bitand(?1,?2))") + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers + .ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); + + functionContributions.getFunctionRegistry() + .patternDescriptorBuilder( "bitxor", "(?1+?2-2*bitand(?1,?2))") + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers + .ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); + + // Misc. functions + functionContributions.getFunctionRegistry().namedDescriptorBuilder( "nvl" ) + .setMinArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.useFirstNonNull() ) + .register(); + + functionContributions.getFunctionRegistry().register( + "user", new CurrentFunction("user", "user", stringType) + ); + functionContributions.getFunctionRegistry().register( + "rowid", new CurrentFunction("rowid", "rowid", stringType) + ); + functionContributions.getFunctionRegistry().register( + "uid", new CurrentFunction("uid", "uid", intType) + ); + functionContributions.getFunctionRegistry().register( + "rownum", new CurrentFunction("rownum", "rownum", longType) + ); + functionContributions.getFunctionRegistry().register( + "vsize", new StandardSQLFunction("vsize", StandardBasicTypes.DOUBLE) + ); + functionContributions.getFunctionRegistry().register( + "SESSION_USER", new CurrentFunction("SESSION_USER","SESSION_USER", stringType) + ); + functionContributions.getFunctionRegistry().register( + "SYSTEM_USER", new CurrentFunction("SYSTEM_USER", "SYSTEM_USER", stringType) + ); + functionContributions.getFunctionRegistry().register( + "CURRENT_USER", new CurrentFunction("CURRENT_USER","CURRENT_USER", stringType) + ); + functionContributions.getFunctionRegistry().registerBinaryTernaryPattern( "locate", functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER ), @@ -251,9 +351,10 @@ public RowLockStrategy getWriteRowLockStrategy() { return RowLockStrategy.COLUMN; } + @Override - public String getForUpdateString(String aliases) { - return " for update of " + aliases; + public String getForUpdateString() { + return " for update"; } @Override @@ -426,4 +527,104 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi } } + @Override + public String getNativeIdentifierGeneratorStrategy() { + return "sequence"; + } + + @Override + public String currentDate() { + return "sysdate"; + } + + @Override + public String currentTime() { + return "sysdate"; + } + + @Override + public String currentTimestamp() { + return "sysdate"; + } + + @Override + public int getMaxVarcharLength() { + // 1 to 4,194,304 bytes according to TimesTen Doc + return 4194304; + } + + @Override + public int getMaxVarbinaryLength() { + // 1 to 4,194,304 bytes according to TimesTen Doc + return 4194304; + } + + @Override + public boolean isEmptyStringTreatedAsNull() { + return true; + } + + @Override + public boolean supportsTupleDistinctCounts() { + return false; + } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from dual"; + } + + @Override + public String castPattern(CastType from, CastType to) { + String result; + switch ( to ) { + case INTEGER: + case LONG: + result = BooleanDecoder.toInteger( from ); + if ( result != null ) { + return result; + } + break; + case STRING: + switch ( from ) { + case BOOLEAN: + case INTEGER_BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + return BooleanDecoder.toString( from ); + case DATE: + return "to_char(?1,'YYYY-MM-DD')"; + case TIME: + return "to_char(?1,'HH24:MI:SS')"; + case TIMESTAMP: + return "to_char(?1,'YYYY-MM-DD HH24:MI:SS.FF9')"; + } + break; + case CLOB: + return "to_clob(?1)"; + case DATE: + if ( from == CastType.STRING ) { + return "to_date(?1,'YYYY-MM-DD')"; + } + break; + case TIME: + if ( from == CastType.STRING ) { + return "to_date(?1,'HH24:MI:SS')"; + } + break; + case TIMESTAMP: + if ( from == CastType.STRING ) { + return "to_timestamp(?1,'YYYY-MM-DD HH24:MI:SS.FF9')"; + } + break; + } + return super.castPattern(from, to); + } + + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java index b0eadebbfa06..b4ff40f70993 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java @@ -28,6 +28,8 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.sql.ast.Clause; /** * A SQL AST translator for TimesTen. @@ -143,4 +145,66 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } + + protected void renderRowsToClause(QuerySpec querySpec) { + if ( querySpec.isRoot() && hasLimit() ) { + prepareLimitOffsetParameters(); + renderRowsToClause( getOffsetParameter(), getLimitParameter() ); + } + else { + assertRowsOnlyFetchClauseType( querySpec ); + renderRowsToClause( querySpec.getOffsetClauseExpression(), querySpec.getFetchClauseExpression() ); + } + } + + protected void renderRowsToClause(Expression offsetClauseExpression, Expression fetchClauseExpression) { + // offsetClauseExpression -> firstRow + // fetchClauseExpression -> maxRows + final Stack clauseStack = getClauseStack(); + + if ( offsetClauseExpression == null && fetchClauseExpression != null ) { + // We only have a maxRows/limit. We use 'SELECT FIRST n' syntax + appendSql("first "); + clauseStack.push( Clause.FETCH ); + try { + renderFetchExpression( fetchClauseExpression ); + } + finally { + clauseStack.pop(); + } + } + else if ( offsetClauseExpression != null ) { + // We have an offset. We use 'SELECT ROWS m TO n' syntax + appendSql( "rows " ); + + // Render offset parameter + clauseStack.push( Clause.OFFSET ); + try { + renderOffsetExpression( offsetClauseExpression ); + } + finally { + clauseStack.pop(); + } + + appendSql( " to " ); + + // Render maxRows/limit parameter + clauseStack.push( Clause.FETCH ); + try { + if ( fetchClauseExpression != null ) { + // We need to substract 1 row to fit maxRows + renderFetchPlusOffsetExpressionAsSingleParameter( fetchClauseExpression, offsetClauseExpression, -1 ); + } + else{ + // We dont have a maxRows param, we will just use a MAX_VALUE + appendSql( Integer.MAX_VALUE ); + } + } + finally { + clauseStack.pop(); + } + } + + appendSql( WHITESPACE ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java index 4d95ef2af0df..9249360ac0ad 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java @@ -7,22 +7,60 @@ package org.hibernate.community.dialect.pagination; import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.dialect.pagination.AbstractLimitHandler; /** * A {@link LimitHandler} for TimesTen, which uses {@code ROWS n}, * but at the start of the query instead of at the end. */ -public class TimesTenLimitHandler extends RowsLimitHandler { +public class TimesTenLimitHandler extends AbstractLimitHandler { public static final TimesTenLimitHandler INSTANCE = new TimesTenLimitHandler(); + public TimesTenLimitHandler(){ + } + + @Override + public boolean supportsLimit() { + return true; + } + + @Override + public boolean supportsOffset() { + return false; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public boolean supportsVariableLimit() { + // a limit string using literals instead of parameters is + // required to translate from Hibernate's 0 based row numbers + // to TimesTen 1 based row numbers + return false; + } + @Override - protected String insert(String rows, String sql) { - return insertAfterSelect( rows, sql ); + // TimesTen is 1 based + public int convertToFirstRowValue(int zeroBasedFirstResult) { + return zeroBasedFirstResult + 1; + } + + @Override + public boolean useMaxForLimit() { + return true; } @Override public boolean bindLimitParametersFirst() { return true; } + + protected String limitClause(boolean hasFirstRow) { + return hasFirstRow ? " rows ? to ?" : " first ?"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java index 802aa1b5801d..72f73050436f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java @@ -6,7 +6,6 @@ */ package org.hibernate.community.dialect.sequence; -import org.hibernate.dialect.sequence.NextvalSequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; /** @@ -14,13 +13,44 @@ * * @author Gavin King */ -public final class TimesTenSequenceSupport extends NextvalSequenceSupport { +public final class TimesTenSequenceSupport implements SequenceSupport { public static final SequenceSupport INSTANCE = new TimesTenSequenceSupport(); + + + @Override + public boolean supportsSequences() { + return true; + } + + @Override + public boolean supportsPooledSequences() { + return true; + } + + @Override + public String getSelectSequenceNextValString(String sequenceName) { + return sequenceName + ".nextval"; + } + + @Override + public String getSequenceNextValString(String sequenceName) { + return "select " + sequenceName + ".nextval from sys.dual"; + } + @Override public String getFromDual() { return " from sys.dual"; } + @Override + public String getCreateSequenceString(String sequenceName) { + return "create sequence " + sequenceName; + } + + @Override + public String getDropSequenceString(String sequenceName) { + return "drop sequence " + sequenceName; + } } 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 fa0050b4d72a..55c49e424d85 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 @@ -4685,43 +4685,6 @@ protected void renderTopStartAtClause( } } - protected void renderRowsToClause(QuerySpec querySpec) { - if ( querySpec.isRoot() && hasLimit() ) { - prepareLimitOffsetParameters(); - renderRowsToClause( getOffsetParameter(), getLimitParameter() ); - } - else { - assertRowsOnlyFetchClauseType( querySpec ); - renderRowsToClause( querySpec.getOffsetClauseExpression(), querySpec.getFetchClauseExpression() ); - } - } - - protected void renderRowsToClause(Expression offsetClauseExpression, Expression fetchClauseExpression) { - if ( fetchClauseExpression != null ) { - appendSql( "rows " ); - final Stack clauseStack = getClauseStack(); - clauseStack.push( Clause.FETCH ); - try { - renderFetchExpression( fetchClauseExpression ); - } - finally { - clauseStack.pop(); - } - if ( offsetClauseExpression != null ) { - clauseStack.push( Clause.OFFSET ); - try { - appendSql( " to " ); - // According to RowsLimitHandler this is 1 based so we need to add 1 to the offset - renderFetchPlusOffsetExpression( fetchClauseExpression, offsetClauseExpression, 1 ); - } - finally { - clauseStack.pop(); - } - } - appendSql( WHITESPACE ); - } - } - protected void renderFetchPlusOffsetExpression( Expression fetchClauseExpression, Expression offsetClauseExpression, From b29d4866ec0bc75bf4898d3ce17dbd20a7c18a3f Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 22 Oct 2025 12:22:25 +0200 Subject: [PATCH 2/2] HHH-19888 Ensure static offset is always respected in FetchPlusOffsetParameterBinder --- .../dialect/FetchPlusOffsetParameterTest.java | 155 ++++++++++++++++++ .../sql/ast/spi/AbstractSqlAstTranslator.java | 30 +--- 2 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java 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..786a84c20d99 --- /dev/null +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java @@ -0,0 +1,155 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.community.dialect; + +import java.util.List; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.H2SqlAstTranslator; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.sqm.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 55c49e424d85..26c9fc4665b2 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 @@ -4744,38 +4744,23 @@ protected void renderFetchPlusOffsetExpressionAsSingleParameter( appendSql( PARAM_MARKER ); final JdbcParameter offsetParameter = (JdbcParameter) offsetClauseExpression; final JdbcParameter fetchParameter = (JdbcParameter) fetchClauseExpression; - final OffsetReceivingParameterBinder fetchBinder = new OffsetReceivingParameterBinder( + final FetchPlusOffsetParameterBinder fetchBinder = new FetchPlusOffsetParameterBinder( offsetParameter, fetchParameter, offset ); - // We don't register and bind the special OffsetJdbcParameter as that comes from the query options - // And in this case, we only want to bind a single JDBC parameter - if ( !( offsetParameter instanceof OffsetJdbcParameter ) ) { - jdbcParameters.addParameter( offsetParameter ); - parameterBinders.add( - (statement, startPosition, jdbcParameterBindings, executionContext) -> { - final JdbcParameterBinding binding = jdbcParameterBindings.getBinding( offsetParameter ); - if ( binding == null ) { - throw new ExecutionException( "JDBC parameter value not bound - " + offsetParameter ); - } - fetchBinder.dynamicOffset = (Number) binding.getBindValue(); - } - ); - } jdbcParameters.addParameter( fetchParameter ); parameterBinders.add( fetchBinder ); } } - private static class OffsetReceivingParameterBinder implements JdbcParameterBinder { + private static class FetchPlusOffsetParameterBinder implements JdbcParameterBinder { private final JdbcParameter offsetParameter; private final JdbcParameter fetchParameter; private final int staticOffset; - private Number dynamicOffset; - public OffsetReceivingParameterBinder( + public FetchPlusOffsetParameterBinder( JdbcParameter offsetParameter, JdbcParameter fetchParameter, int staticOffset) { @@ -4806,13 +4791,16 @@ public void bindParameterValue( offsetValue = executionContext.getQueryOptions().getEffectiveLimit().getFirstRow(); } else { - offsetValue = dynamicOffset.intValue() + staticOffset; - dynamicOffset = null; + final JdbcParameterBinding binding = jdbcParameterBindings.getBinding( offsetParameter ); + if ( binding == null ) { + throw new ExecutionException( "JDBC parameter value not bound - " + offsetParameter ); + } + offsetValue = ((Number) binding.getBindValue()).intValue(); } //noinspection unchecked fetchParameter.getExpressionType().getSingleJdbcMapping().getJdbcValueBinder().bind( statement, - bindValue.intValue() + offsetValue, + bindValue.intValue() + offsetValue + staticOffset, startPosition, executionContext.getSession() );