Skip to content

Commit 7c7114f

Browse files
committed
HHH-18818 Fix ID conflicts between CTE batch inserts and optimizer strategies
1 parent c6dc40f commit 7c7114f

15 files changed

+575
-4
lines changed

hibernate-core/src/main/java/org/hibernate/id/enhanced/HiLoOptimizer.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
import java.util.concurrent.locks.ReentrantLock;
1212

1313
import org.hibernate.HibernateException;
14+
import org.hibernate.engine.spi.SessionFactoryImplementor;
1415
import org.hibernate.id.IntegralDataTypeHolder;
1516

17+
import org.hibernate.metamodel.mapping.BasicValuedMapping;
18+
import org.hibernate.query.sqm.BinaryArithmeticOperator;
19+
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
20+
import org.hibernate.sql.ast.tree.expression.Expression;
21+
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
1622
import org.jboss.logging.Logger;
1723

1824
/**
@@ -197,4 +203,20 @@ public IntegralDataTypeHolder getHiValue() {
197203
lock.unlock();
198204
}
199205
}
206+
207+
@Override
208+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
209+
BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class );
210+
return new BinaryArithmeticExpression(
211+
new BinaryArithmeticExpression(
212+
databaseValue,
213+
BinaryArithmeticOperator.MULTIPLY,
214+
new QueryLiteral<>( incrementSize, integerType ),
215+
integerType
216+
),
217+
BinaryArithmeticOperator.SUBTRACT,
218+
new QueryLiteral<>( incrementSize - 1, integerType ),
219+
integerType
220+
);
221+
}
200222
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/LegacyHiLoAlgorithmOptimizer.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
import java.util.concurrent.locks.ReentrantLock;
1212

1313
import org.hibernate.HibernateException;
14+
import org.hibernate.engine.spi.SessionFactoryImplementor;
1415
import org.hibernate.id.IntegralDataTypeHolder;
1516

17+
import org.hibernate.metamodel.mapping.BasicValuedMapping;
18+
import org.hibernate.query.sqm.BinaryArithmeticOperator;
19+
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
20+
import org.hibernate.sql.ast.tree.expression.Expression;
21+
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
1622
import org.jboss.logging.Logger;
1723

1824
/**
@@ -150,4 +156,15 @@ public IntegralDataTypeHolder getLastValue() {
150156
lock.unlock();
151157
}
152158
}
159+
160+
@Override
161+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
162+
BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class );
163+
return new BinaryArithmeticExpression(
164+
databaseValue,
165+
BinaryArithmeticOperator.MULTIPLY,
166+
new QueryLiteral<>( getIncrementSize() + 1, integerType ),
167+
integerType
168+
);
169+
}
153170
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/NoopOptimizer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import java.io.Serializable;
88

9+
import org.hibernate.engine.spi.SessionFactoryImplementor;
910
import org.hibernate.id.IntegralDataTypeHolder;
11+
import org.hibernate.sql.ast.tree.expression.Expression;
1012

1113
/**
1214
* An optimizer that performs no optimization. A round trip to
@@ -52,4 +54,9 @@ public boolean applyIncrementSizeToSourceValues() {
5254
// We don't apply an increment size of 1, since it is already the default.
5355
return getIncrementSize() != 0 && getIncrementSize() != 1;
5456
}
57+
58+
@Override
59+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
60+
return databaseValue;
61+
}
5562
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/Optimizer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import java.io.Serializable;
88

9+
import org.hibernate.engine.spi.SessionFactoryImplementor;
910
import org.hibernate.id.IntegralDataTypeHolder;
11+
import org.hibernate.sql.ast.tree.expression.Expression;
1012

1113
/**
1214
* Performs optimization on an optimizable identifier generator. Typically
@@ -59,4 +61,16 @@ public interface Optimizer {
5961
* case the increment is totally an in memory construct.
6062
*/
6163
boolean applyIncrementSizeToSourceValues();
64+
65+
/**
66+
* Creates an expression representing the low/base value for ID allocation in batch insert operations.
67+
* <p>
68+
* Each optimizer implementation should define its own
69+
* strategy for calculating the starting value of a sequence range.
70+
*
71+
* @param databaseValue The expression representing the next value from database sequence
72+
* @param sessionFactory
73+
* @return An expression that calculates the low/base value according to the optimizer strategy
74+
*/
75+
Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory);
6276
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoOptimizer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import java.util.concurrent.locks.Lock;
1313

1414
import org.hibernate.HibernateException;
15+
import org.hibernate.engine.spi.SessionFactoryImplementor;
1516
import org.hibernate.id.IntegralDataTypeHolder;
1617
import org.hibernate.internal.CoreMessageLogger;
18+
import org.hibernate.sql.ast.tree.expression.Expression;
1719
import org.jboss.logging.Logger;
1820

1921
/**
@@ -125,4 +127,9 @@ public IntegralDataTypeHolder getLastSourceValue() {
125127
public boolean applyIncrementSizeToSourceValues() {
126128
return true;
127129
}
130+
131+
@Override
132+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
133+
return databaseValue;
134+
}
128135
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoThreadLocalOptimizer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import java.util.Map;
1111

1212
import org.hibernate.HibernateException;
13+
import org.hibernate.engine.spi.SessionFactoryImplementor;
1314
import org.hibernate.id.IntegralDataTypeHolder;
1415
import org.hibernate.internal.CoreMessageLogger;
1516

17+
import org.hibernate.sql.ast.tree.expression.Expression;
1618
import org.jboss.logging.Logger;
1719

1820
/**
@@ -112,4 +114,9 @@ private Serializable generate(AccessCallback callback, int incrementSize) {
112114
return value.makeValueThenIncrement();
113115
}
114116
}
117+
118+
@Override
119+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
120+
return databaseValue;
121+
}
115122
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledOptimizer.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@
1212
import java.util.concurrent.locks.ReentrantLock;
1313

1414
import org.hibernate.HibernateException;
15+
import org.hibernate.engine.spi.SessionFactoryImplementor;
1516
import org.hibernate.id.IntegralDataTypeHolder;
1617
import org.hibernate.internal.CoreMessageLogger;
1718

19+
import org.hibernate.metamodel.mapping.BasicValuedMapping;
20+
import org.hibernate.query.sqm.BinaryArithmeticOperator;
21+
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
22+
import org.hibernate.sql.ast.tree.expression.Expression;
23+
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
1824
import org.jboss.logging.Logger;
1925

2026
/**
@@ -167,4 +173,15 @@ public IntegralDataTypeHolder getLastValue() {
167173
public void injectInitialValue(long initialValue) {
168174
this.initialValue = initialValue;
169175
}
176+
177+
@Override
178+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
179+
BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class );
180+
return new BinaryArithmeticExpression(
181+
databaseValue,
182+
BinaryArithmeticOperator.SUBTRACT,
183+
new QueryLiteral<>( incrementSize - 1, integerType ),
184+
integerType
185+
);
186+
}
170187
}

hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,13 @@ public int execute(DomainQueryExecutionContext executionContext) {
384384
generator.determineBulkInsertionIdentifierGenerationSelectFragment(
385385
sessionFactory.getSqlStringGenerationContext()
386386
);
387+
388+
Expression databaseValue = new SelfRenderingSqlFragmentExpression( fragment );
387389
rowsWithSequenceQuery.getSelectClause().addSqlSelection(
388-
new SqlSelectionImpl(
389-
1,
390-
new SelfRenderingSqlFragmentExpression( fragment )
391-
)
390+
new SqlSelectionImpl( 1,
391+
optimizer.createLowValueExpression( databaseValue, sessionFactory ) )
392392
);
393+
393394
rowsWithSequenceQuery.applyPredicate(
394395
new ComparisonPredicate(
395396
rowNumberMinusOneModuloIncrement,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.id.cte;
6+
7+
import org.hibernate.cfg.AvailableSettings;
8+
import org.hibernate.dialect.DB2Dialect;
9+
import org.hibernate.dialect.PostgreSQLDialect;
10+
import org.hibernate.id.enhanced.HiLoOptimizer;
11+
import org.hibernate.testing.orm.junit.DomainModel;
12+
import org.hibernate.testing.orm.junit.JiraKey;
13+
import org.hibernate.testing.orm.junit.RequiresDialect;
14+
import org.hibernate.testing.orm.junit.RequiresDialects;
15+
import org.hibernate.testing.orm.junit.ServiceRegistry;
16+
import org.hibernate.testing.orm.junit.SessionFactory;
17+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
18+
import org.hibernate.testing.orm.junit.Setting;
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
/**
24+
* Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link HiLoOptimizer},
25+
* ensuring proper ID allocation and prevention of duplicates across both operations.
26+
*
27+
* @author Kowsar Atazadeh
28+
*/
29+
@JiraKey("HHH-18818")
30+
@SessionFactory
31+
@RequiresDialects({
32+
@RequiresDialect(PostgreSQLDialect.class),
33+
@RequiresDialect(DB2Dialect.class)
34+
})
35+
@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "hilo"))
36+
@DomainModel(annotatedClasses = Dummy.class)
37+
public class CteInsertWithHiLoOptimizerTest {
38+
@Test
39+
void test(SessionFactoryScope scope) {
40+
// 7 rows inserted with IDs 1-7
41+
// Database sequence calls:
42+
// - First returns 1 (allocates IDs 1-5)
43+
// - Second returns 2 (allocates IDs 6-10)
44+
// IDs 8-10 reserved from current allocation
45+
scope.inTransaction( session -> {
46+
for ( var id = 1; id <= 7; id++ ) {
47+
Dummy d = new Dummy( "d" + id );
48+
session.persist( d );
49+
assertEquals( id, d.getId() );
50+
}
51+
} );
52+
53+
// 7 rows inserted with IDs 11-17
54+
// Database sequence calls:
55+
// - First returns 3 (allocates IDs 11-15)
56+
// - Second returns 4 (allocates IDs 16-20)
57+
// IDs 18-20 reserved from current allocation
58+
scope.inTransaction( session -> {
59+
session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ).
60+
executeUpdate();
61+
var inserted = session.createSelectionQuery(
62+
"SELECT d.id FROM Dummy d WHERE d.id > 7 ORDER BY d.id", Long.class )
63+
.getResultList();
64+
assertEquals( 7, inserted.size() );
65+
for ( int i = 0; i < inserted.size(); i++ ) {
66+
assertEquals( 11 + i, inserted.get( i ) );
67+
}
68+
} );
69+
70+
// 5 rows inserted with IDs 8-10, 21-22
71+
// Database sequence call returns 5 (allocates IDs 21-25)
72+
// Using previously reserved IDs 8-10 and new allocation IDs 21-22
73+
// IDs 23-25 reserved from current allocation
74+
scope.inTransaction( session -> {
75+
for ( var id = 8; id <= 10; id++ ) {
76+
Dummy d = new Dummy( "d" + id );
77+
session.persist( d );
78+
assertEquals( id, d.getId() );
79+
}
80+
81+
for ( var id = 21; id <= 22; id++ ) {
82+
Dummy d = new Dummy( "d" + id );
83+
session.persist( d );
84+
assertEquals( id, d.getId() );
85+
}
86+
} );
87+
}
88+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.id.cte;
6+
7+
import org.hibernate.cfg.AvailableSettings;
8+
import org.hibernate.dialect.DB2Dialect;
9+
import org.hibernate.dialect.PostgreSQLDialect;
10+
import org.hibernate.id.enhanced.LegacyHiLoAlgorithmOptimizer;
11+
import org.hibernate.testing.orm.junit.DomainModel;
12+
import org.hibernate.testing.orm.junit.JiraKey;
13+
import org.hibernate.testing.orm.junit.RequiresDialect;
14+
import org.hibernate.testing.orm.junit.RequiresDialects;
15+
import org.hibernate.testing.orm.junit.ServiceRegistry;
16+
import org.hibernate.testing.orm.junit.SessionFactory;
17+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
18+
import org.hibernate.testing.orm.junit.Setting;
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
/**
24+
* Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link LegacyHiLoAlgorithmOptimizer},
25+
* ensuring proper ID allocation and prevention of duplicates across both operations.
26+
*
27+
* @author Kowsar Atazadeh
28+
*/
29+
@JiraKey("HHH-18818")
30+
@SessionFactory
31+
@RequiresDialects({
32+
@RequiresDialect(PostgreSQLDialect.class),
33+
@RequiresDialect(DB2Dialect.class)
34+
})
35+
@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "legacy-hilo"))
36+
@DomainModel(annotatedClasses = Dummy.class)
37+
public class CteInsertWithLegacyHiLoOptimizerTest {
38+
@Test
39+
void test(SessionFactoryScope scope) {
40+
// 7 rows inserted with IDs 6-12
41+
// Database sequence calls:
42+
// - First returns 1 (allocates IDs 6-11)
43+
// - Second returns 2 (allocates IDs 12-17)
44+
// IDs 13-17 reserved from current allocation
45+
scope.inTransaction( session -> {
46+
for ( var id = 6; id <= 12; id++ ) {
47+
Dummy d = new Dummy( "d" + id );
48+
session.persist( d );
49+
assertEquals( id, d.getId() );
50+
}
51+
} );
52+
53+
// 7 rows inserted with IDs 18-22, 24-25
54+
// Database sequence calls:
55+
// - First returns 3 (allocates IDs 18-22)
56+
// - Second returns 4 (allocates IDs 24-28)
57+
// Note: ID 23 skipped due to different batch sizes between CTE (5) and optimizer (6)
58+
// IDs 26-28 reserved from current allocation
59+
scope.inTransaction( session -> {
60+
session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ).
61+
executeUpdate();
62+
var inserted = session.createSelectionQuery(
63+
"SELECT d.id FROM Dummy d WHERE d.id > 12 ORDER BY d.id", Long.class )
64+
.getResultList();
65+
assertEquals( 7, inserted.size() );
66+
67+
int i = 0;
68+
for ( int id = 18; id <= 22; id++, i++ ) {
69+
assertEquals( id, inserted.get( i ) );
70+
}
71+
for ( int id = 24; id <= 25; id++, i++ ) {
72+
assertEquals( id, inserted.get( i ) );
73+
}
74+
} );
75+
76+
// 8 rows inserted with IDs 13-17, 30-32
77+
// Using previously reserved IDs 13-17
78+
// Database sequence call returns 5 (allocates IDs 30-35)
79+
// IDs 33-35 reserved from current allocation
80+
scope.inTransaction( session -> {
81+
for ( var id = 13; id <= 17; id++ ) {
82+
Dummy d = new Dummy( "d" + id );
83+
session.persist( d );
84+
assertEquals( id, d.getId() );
85+
}
86+
87+
for ( var id = 30; id <= 32; id++ ) {
88+
Dummy d = new Dummy( "d" + id );
89+
session.persist( d );
90+
assertEquals( id, d.getId() );
91+
}
92+
} );
93+
}
94+
}

0 commit comments

Comments
 (0)