diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/HiLoOptimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/HiLoOptimizer.java index b045dbc6d17f..93b1db162592 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/HiLoOptimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/HiLoOptimizer.java @@ -4,17 +4,22 @@ */ package org.hibernate.id.enhanced; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.id.IntegralDataTypeHolder; +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.query.sqm.BinaryArithmeticOperator; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.jboss.logging.Logger; + import java.io.Serializable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.hibernate.HibernateException; -import org.hibernate.id.IntegralDataTypeHolder; - -import org.jboss.logging.Logger; - /** * Optimizer which applies a 'hilo' algorithm in memory to achieve * optimization. @@ -197,4 +202,20 @@ public IntegralDataTypeHolder getHiValue() { lock.unlock(); } } + + @Override + public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) { + BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class ); + return new BinaryArithmeticExpression( + new BinaryArithmeticExpression( + databaseValue, + BinaryArithmeticOperator.MULTIPLY, + new QueryLiteral<>( incrementSize, integerType ), + integerType + ), + BinaryArithmeticOperator.SUBTRACT, + new QueryLiteral<>( incrementSize - 1, integerType ), + integerType + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/LegacyHiLoAlgorithmOptimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/LegacyHiLoAlgorithmOptimizer.java index a2e3c178526e..48ba44933ce4 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/LegacyHiLoAlgorithmOptimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/LegacyHiLoAlgorithmOptimizer.java @@ -4,17 +4,22 @@ */ package org.hibernate.id.enhanced; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.id.IntegralDataTypeHolder; +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.query.sqm.BinaryArithmeticOperator; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.jboss.logging.Logger; + import java.io.Serializable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.hibernate.HibernateException; -import org.hibernate.id.IntegralDataTypeHolder; - -import org.jboss.logging.Logger; - /** * Slight variation from {@link HiLoOptimizer}, maintaining compatibility with the values generated by the * legacy Hibernate hilo based generators. @@ -150,4 +155,15 @@ public IntegralDataTypeHolder getLastValue() { lock.unlock(); } } + + @Override + public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) { + BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class ); + return new BinaryArithmeticExpression( + databaseValue, + BinaryArithmeticOperator.MULTIPLY, + new QueryLiteral<>( getIncrementSize() + 1, integerType ), + integerType + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/NoopOptimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/NoopOptimizer.java index f3a948ee3c1b..db9f487c80fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/NoopOptimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/NoopOptimizer.java @@ -4,9 +4,11 @@ */ package org.hibernate.id.enhanced; -import java.io.Serializable; - +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.id.IntegralDataTypeHolder; +import org.hibernate.sql.ast.tree.expression.Expression; + +import java.io.Serializable; /** * An optimizer that performs no optimization. A round trip to @@ -52,4 +54,9 @@ public boolean applyIncrementSizeToSourceValues() { // We don't apply an increment size of 1, since it is already the default. return getIncrementSize() != 0 && getIncrementSize() != 1; } + + @Override + public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) { + return databaseValue; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/Optimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/Optimizer.java index 1c045261493b..3ebd2297de3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/Optimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/Optimizer.java @@ -4,9 +4,11 @@ */ package org.hibernate.id.enhanced; -import java.io.Serializable; - +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.id.IntegralDataTypeHolder; +import org.hibernate.sql.ast.tree.expression.Expression; + +import java.io.Serializable; /** * Performs optimization on an optimizable identifier generator. Typically @@ -59,4 +61,16 @@ public interface Optimizer { * case the increment is totally an in memory construct. */ boolean applyIncrementSizeToSourceValues(); + + /** + * Creates an expression representing the low/base value for ID allocation in batch insert operations. + *

+ * Each optimizer implementation should define its own + * strategy for calculating the starting value of a sequence range. + * + * @param databaseValue The expression representing the next value from database sequence + * @param sessionFactory + * @return An expression that calculates the low/base value according to the optimizer strategy + */ + Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory); } diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoOptimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoOptimizer.java index f6d4a2b903bc..3addaafa6854 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoOptimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoOptimizer.java @@ -4,17 +4,19 @@ */ package org.hibernate.id.enhanced; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.id.IntegralDataTypeHolder; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.jboss.logging.Logger; + import java.io.Serializable; import java.lang.invoke.MethodHandles; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.Lock; - -import org.hibernate.HibernateException; -import org.hibernate.id.IntegralDataTypeHolder; -import org.hibernate.internal.CoreMessageLogger; -import org.jboss.logging.Logger; +import java.util.concurrent.locks.ReentrantLock; /** * Variation of {@link PooledOptimizer} which interprets the incoming database @@ -125,4 +127,9 @@ public IntegralDataTypeHolder getLastSourceValue() { public boolean applyIncrementSizeToSourceValues() { return true; } + + @Override + public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) { + return databaseValue; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoThreadLocalOptimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoThreadLocalOptimizer.java index f9f8bf5d0cba..7f26386e0020 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoThreadLocalOptimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoThreadLocalOptimizer.java @@ -4,17 +4,18 @@ */ package org.hibernate.id.enhanced; -import java.io.Serializable; -import java.lang.invoke.MethodHandles; -import java.util.HashMap; -import java.util.Map; - import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.id.IntegralDataTypeHolder; import org.hibernate.internal.CoreMessageLogger; - +import org.hibernate.sql.ast.tree.expression.Expression; import org.jboss.logging.Logger; +import java.io.Serializable; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; + /** * Variation of {@link PooledOptimizer} which interprets the incoming database * value as the lo value, rather than the hi value, as well as using thread local @@ -112,4 +113,9 @@ private Serializable generate(AccessCallback callback, int incrementSize) { return value.makeValueThenIncrement(); } } + + @Override + public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) { + return databaseValue; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledOptimizer.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledOptimizer.java index 01788366c6b2..4e674eee072d 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledOptimizer.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledOptimizer.java @@ -4,6 +4,17 @@ */ package org.hibernate.id.enhanced; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.id.IntegralDataTypeHolder; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.query.sqm.BinaryArithmeticOperator; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.jboss.logging.Logger; + import java.io.Serializable; import java.lang.invoke.MethodHandles; import java.util.Map; @@ -11,12 +22,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.hibernate.HibernateException; -import org.hibernate.id.IntegralDataTypeHolder; -import org.hibernate.internal.CoreMessageLogger; - -import org.jboss.logging.Logger; - /** * Optimizer which uses a pool of values, storing the next low value of the range * in the database. @@ -167,4 +172,15 @@ public IntegralDataTypeHolder getLastValue() { public void injectInitialValue(long initialValue) { this.initialValue = initialValue; } + + @Override + public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) { + BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class ); + return new BinaryArithmeticExpression( + databaseValue, + BinaryArithmeticOperator.SUBTRACT, + new QueryLiteral<>( incrementSize - 1, integerType ), + integerType + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java index 7729bc24f19a..ebc6ee5835ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java @@ -384,12 +384,13 @@ public int execute(DomainQueryExecutionContext executionContext) { generator.determineBulkInsertionIdentifierGenerationSelectFragment( sessionFactory.getSqlStringGenerationContext() ); + + Expression databaseValue = new SelfRenderingSqlFragmentExpression( fragment ); rowsWithSequenceQuery.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - 1, - new SelfRenderingSqlFragmentExpression( fragment ) - ) + new SqlSelectionImpl( 1, + optimizer.createLowValueExpression( databaseValue, sessionFactory ) ) ); + rowsWithSequenceQuery.applyPredicate( new ComparisonPredicate( rowNumberMinusOneModuloIncrement, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithHiLoOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithHiLoOptimizerTest.java new file mode 100644 index 000000000000..f222b5db9a70 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithHiLoOptimizerTest.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.id.enhanced.HiLoOptimizer; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; +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.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link HiLoOptimizer}, + * ensuring proper ID allocation and prevention of duplicates across both operations. + * + * @author Kowsar Atazadeh + */ +@JiraKey("HHH-18818") +@SessionFactory +@RequiresDialects({ + @RequiresDialect(PostgreSQLDialect.class), + @RequiresDialect(DB2Dialect.class) +}) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "hilo")) +@DomainModel(annotatedClasses = Dummy.class) +public class CteInsertWithHiLoOptimizerTest { + @Test + void test(SessionFactoryScope scope) { + // 7 rows inserted with IDs 1-7 + // Database sequence calls: + // - First returns 1 (allocates IDs 1-5) + // - Second returns 2 (allocates IDs 6-10) + // IDs 8-10 reserved from current allocation + scope.inTransaction( session -> { + for ( var id = 1; id <= 7; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + + // 7 rows inserted with IDs 11-17 + // Database sequence calls: + // - First returns 3 (allocates IDs 11-15) + // - Second returns 4 (allocates IDs 16-20) + // IDs 18-20 reserved from current allocation + scope.inTransaction( session -> { + session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ). + executeUpdate(); + var inserted = session.createSelectionQuery( + "SELECT d.id FROM Dummy d WHERE d.id > 7 ORDER BY d.id", Long.class ) + .getResultList(); + assertEquals( 7, inserted.size() ); + for ( int i = 0; i < inserted.size(); i++ ) { + assertEquals( 11 + i, inserted.get( i ) ); + } + } ); + + // 5 rows inserted with IDs 8-10, 21-22 + // Database sequence call returns 5 (allocates IDs 21-25) + // Using previously reserved IDs 8-10 and new allocation IDs 21-22 + // IDs 23-25 reserved from current allocation + scope.inTransaction( session -> { + for ( var id = 8; id <= 10; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + + for ( var id = 21; id <= 22; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithLegacyHiLoOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithLegacyHiLoOptimizerTest.java new file mode 100644 index 000000000000..3129672616e3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithLegacyHiLoOptimizerTest.java @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.id.enhanced.LegacyHiLoAlgorithmOptimizer; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; +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.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link LegacyHiLoAlgorithmOptimizer}, + * ensuring proper ID allocation and prevention of duplicates across both operations. + * + * @author Kowsar Atazadeh + */ +@JiraKey("HHH-18818") +@SessionFactory +@RequiresDialects({ + @RequiresDialect(PostgreSQLDialect.class), + @RequiresDialect(DB2Dialect.class) +}) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "legacy-hilo")) +@DomainModel(annotatedClasses = Dummy.class) +public class CteInsertWithLegacyHiLoOptimizerTest { + @Test + void test(SessionFactoryScope scope) { + // 7 rows inserted with IDs 6-12 + // Database sequence calls: + // - First returns 1 (allocates IDs 6-11) + // - Second returns 2 (allocates IDs 12-17) + // IDs 13-17 reserved from current allocation + scope.inTransaction( session -> { + for ( var id = 6; id <= 12; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + + // 7 rows inserted with IDs 18-22, 24-25 + // Database sequence calls: + // - First returns 3 (allocates IDs 18-22) + // - Second returns 4 (allocates IDs 24-28) + // Note: ID 23 skipped due to different batch sizes between CTE (5) and optimizer (6) + // IDs 26-28 reserved from current allocation + scope.inTransaction( session -> { + session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ). + executeUpdate(); + var inserted = session.createSelectionQuery( + "SELECT d.id FROM Dummy d WHERE d.id > 12 ORDER BY d.id", Long.class ) + .getResultList(); + assertEquals( 7, inserted.size() ); + + int i = 0; + for ( int id = 18; id <= 22; id++, i++ ) { + assertEquals( id, inserted.get( i ) ); + } + for ( int id = 24; id <= 25; id++, i++ ) { + assertEquals( id, inserted.get( i ) ); + } + } ); + + // 8 rows inserted with IDs 13-17, 30-32 + // Using previously reserved IDs 13-17 + // Database sequence call returns 5 (allocates IDs 30-35) + // IDs 33-35 reserved from current allocation + scope.inTransaction( session -> { + for ( var id = 13; id <= 17; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + + for ( var id = 30; id <= 32; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithNoopOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithNoopOptimizerTest.java new file mode 100644 index 000000000000..acc0ed7cfd12 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithNoopOptimizerTest.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.id.enhanced.NoopOptimizer; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link NoopOptimizer}, + * ensuring proper ID allocation and prevention of duplicates across both operations. + * + * @author Kowsar Atazadeh + */ +@JiraKey("HHH-18818") +@SessionFactory +@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "none")) +@DomainModel(annotatedClasses = Dummy.class) +public class CteInsertWithNoopOptimizerTest { + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( var id = 1; id <= 3; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + + scope.inTransaction( session -> { + session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ). + executeUpdate(); + var inserted = session.createSelectionQuery( + "SELECT d.id FROM Dummy d WHERE d.id > 3 ORDER BY d.id", Long.class ) + .getResultList(); + assertEquals( 3, inserted.size() ); + for ( int i = 0; i < inserted.size(); i++ ) { + assertEquals( 4 + i, inserted.get( i ) ); + } + } ); + + scope.inTransaction( session -> { + for ( var i = 7; i <= 9; i++ ) { + Dummy d = new Dummy( "d" + i ); + session.persist( d ); + assertEquals( i, d.getId() ); + } + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledLoOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledLoOptimizerTest.java new file mode 100644 index 000000000000..f7403d6bd774 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledLoOptimizerTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.id.enhanced.PooledLoOptimizer; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link PooledLoOptimizer}, + * ensuring proper ID allocation and prevention of duplicates across both operations. + * + * @author Kowsar Atazadeh + */ +@JiraKey("HHH-18818") +@SessionFactory +@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled-lo")) +@DomainModel(annotatedClasses = Dummy.class) +public class CteInsertWithPooledLoOptimizerTest { + @Test + void test(SessionFactoryScope scope) { + // 9 rows inserted with IDs 1-9 + // Database sequence calls: + // - First returns 1 (allocates IDs 1-5) + // - Second returns 6 (allocates IDs 6-10) + // ID 10 reserved from current allocation + scope.inTransaction( session -> { + for ( var id = 1; id <= 9; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + + // 9 rows inserted with IDs 11-19 + // Database sequence calls: + // - First returns 11 (allocates IDs 11-15) + // - Second returns 16 (allocates IDs 16-20) + // ID 20 reserved from current allocation + scope.inTransaction( session -> { + session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ). + executeUpdate(); + var inserted = session.createSelectionQuery( + "SELECT d.id FROM Dummy d WHERE d.id > 9 ORDER BY d.id", Long.class ) + .getResultList(); + assertEquals( 9, inserted.size() ); + for ( int i = 0; i < inserted.size(); i++ ) { + assertEquals( 11 + i, inserted.get( i ) ); + } + } ); + + // 1 row inserted with ID 10 + // Using previously reserved ID + scope.inTransaction( session -> { + Dummy d = new Dummy( "d10" ); + session.persist( d ); + assertEquals( 10, d.getId() ); + } ); + + // 1 row inserted with ID 21 + // Database sequence call returns 21 (allocates IDs 21-25) + // IDs 22-25 reserved from current allocation + scope.inTransaction( session -> { + Dummy d = new Dummy( "d21" ); + session.persist( d ); + assertEquals( 21, d.getId() ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledLoThreadLocalOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledLoThreadLocalOptimizerTest.java new file mode 100644 index 000000000000..41cfc3d69379 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledLoThreadLocalOptimizerTest.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.id.enhanced.PooledLoThreadLocalOptimizer; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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.Setting; +import org.junit.jupiter.api.Test; + +/** + * Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link PooledLoThreadLocalOptimizer}, + * ensuring proper ID allocation and prevention of duplicates across both operations. + * + * @author Kowsar Atazadeh + */ +@JiraKey("HHH-18818") +@SessionFactory +@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled-lotl")) +@DomainModel(annotatedClasses = Dummy.class) +public class CteInsertWithPooledLoThreadLocalOptimizerTest { + @Test + void test(SessionFactoryScope scope) { + new CteInsertWithPooledLoOptimizerTest().test( scope ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledOptimizerTest.java new file mode 100644 index 000000000000..19bc0b6dc54a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/CteInsertWithPooledOptimizerTest.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.id.enhanced.PooledOptimizer; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link PooledOptimizer}, + * ensuring proper ID allocation and prevention of duplicates across both operations. + * + * @author Kowsar Atazadeh + */ +@JiraKey("HHH-18818") +@SessionFactory +@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled")) +@DomainModel(annotatedClasses = Dummy.class) +public class CteInsertWithPooledOptimizerTest { + @Test + void test(SessionFactoryScope scope) { + // 9 rows inserted with IDs 1-9 + // Database sequence calls: + // - First returns 1 (allocates IDs 1-5) + // - Second returns 6 (allocates IDs 6-10) + // - Third returns 11 (allocates IDs 11-15) + // IDs 10-11 reserved from current allocation + scope.inTransaction( session -> { + for ( var id = 1; id <= 9; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + + // 9 rows inserted with IDs 12-20 + // Database sequence calls: + // - First returns 16 (allocates IDs 16-20) + // - Second returns 21 (allocates IDs 21-25) + // IDs 21-25 reserved from current allocation + scope.inTransaction( session -> { + session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ). + executeUpdate(); + var inserted = session.createSelectionQuery( + "SELECT d.id FROM Dummy d WHERE d.id > 9 ORDER BY d.id", Long.class ) + .getResultList(); + assertEquals( 9, inserted.size() ); + for ( int i = 0; i < inserted.size(); i++ ) { + assertEquals( 12 + i, inserted.get( i ) ); + } + } ); + + // 2 rows inserted with IDs 10-11 + // Using previously reserved IDs + scope.inTransaction( session -> { + for ( var id = 10; id <= 11; id++ ) { + Dummy d = new Dummy( "d" + id ); + session.persist( d ); + assertEquals( id, d.getId() ); + } + } ); + + // 1 row inserted with ID 22 + // Database sequence call returns 26 (allocates IDs 22-26) + // IDs 23-26 reserved from current allocation + scope.inTransaction( session -> { + Dummy d22 = new Dummy( "d22" ); + session.persist( d22 ); + assertEquals( 22, d22.getId() ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/Dummy.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/Dummy.java new file mode 100644 index 000000000000..5b708bbd39ed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/cte/Dummy.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.cte; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; + +@Entity(name = "Dummy") +class Dummy { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "dummy_seq") + @SequenceGenerator(name = "dummy_seq", sequenceName = "dummy_seq", allocationSize = 5) + private Long id; + + private String name; + + public Dummy() { + } + + public Dummy(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +}