Skip to content

Commit 27b4751

Browse files
committed
HHH-18818 Fix ID conflicts between CTE batch inserts and PooledOptimizer sequence allocation
1 parent d5e829d commit 27b4751

File tree

2 files changed

+122
-1
lines changed

2 files changed

+122
-1
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.hibernate.id.BulkInsertionCapableIdentifierGenerator;
2121
import org.hibernate.id.OptimizableGenerator;
2222
import org.hibernate.id.enhanced.Optimizer;
23+
import org.hibernate.id.enhanced.PooledOptimizer;
2324
import org.hibernate.internal.util.collections.CollectionHelper;
2425
import org.hibernate.internal.util.collections.Stack;
2526
import org.hibernate.metamodel.mapping.BasicValuedMapping;
@@ -387,7 +388,14 @@ public int execute(DomainQueryExecutionContext executionContext) {
387388
rowsWithSequenceQuery.getSelectClause().addSqlSelection(
388389
new SqlSelectionImpl(
389390
1,
390-
new SelfRenderingSqlFragmentExpression( fragment )
391+
optimizer instanceof PooledOptimizer ?
392+
new BinaryArithmeticExpression(
393+
new SelfRenderingSqlFragmentExpression( fragment ),
394+
BinaryArithmeticOperator.SUBTRACT,
395+
new QueryLiteral<>( optimizer.getIncrementSize() - 1, integerType ),
396+
integerType
397+
) :
398+
new SelfRenderingSqlFragmentExpression( fragment )
391399
)
392400
);
393401
rowsWithSequenceQuery.applyPredicate(
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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;
6+
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.GeneratedValue;
9+
import jakarta.persistence.GenerationType;
10+
import jakarta.persistence.Id;
11+
import jakarta.persistence.SequenceGenerator;
12+
13+
import org.hibernate.cfg.AvailableSettings;
14+
import org.hibernate.dialect.DB2Dialect;
15+
import org.hibernate.dialect.PostgreSQLDialect;
16+
17+
import org.hibernate.testing.orm.junit.DomainModel;
18+
import org.hibernate.testing.orm.junit.JiraKey;
19+
import org.hibernate.testing.orm.junit.RequiresDialect;
20+
import org.hibernate.testing.orm.junit.RequiresDialects;
21+
import org.hibernate.testing.orm.junit.ServiceRegistry;
22+
import org.hibernate.testing.orm.junit.SessionFactory;
23+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
24+
import org.hibernate.testing.orm.junit.Setting;
25+
import org.junit.jupiter.api.Test;
26+
27+
/**
28+
* Tests sequence generation with pooled optimizer when using CTE-based batch inserts.
29+
* Verifies that ID allocation works correctly across regular persists and batch operations.
30+
*
31+
* @author Kowsar Atazadeh
32+
*/
33+
@JiraKey("HHH-18818")
34+
@SessionFactory
35+
@RequiresDialects(
36+
{
37+
@RequiresDialect(PostgreSQLDialect.class),
38+
@RequiresDialect(DB2Dialect.class),
39+
}
40+
)
41+
@ServiceRegistry(
42+
settings = {
43+
@Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "pooled"),
44+
}
45+
)
46+
@DomainModel(annotatedClasses = { CteInsertStrategyWithPooledOptimizerTest.Dummy.class })
47+
public class CteInsertStrategyWithPooledOptimizerTest {
48+
@Test
49+
void test(SessionFactoryScope scope) {
50+
// 9 rows inserted with IDs 1 to 9
51+
// Two calls to the DB for next sequence value generation: first returns 6, second returns 11
52+
// IDs 10 and 11 are still reserved for the PooledOptimizer
53+
scope.inTransaction( session -> {
54+
for ( var i = 1; i <= 9; i++ ) {
55+
Dummy d = new Dummy( "d" + i );
56+
session.persist( d );
57+
}
58+
} );
59+
60+
// 9 rows inserted (using CteInsertStrategy) with IDs 12 to 20 (before the fix, IDs would be 16 to 24)
61+
// Two calls to the DB for next sequence value generation: first returns 16, second returns 21
62+
scope.inTransaction( session -> {
63+
session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ).
64+
executeUpdate();
65+
} );
66+
67+
// Two rows inserted with the reserved IDs 10 and 11
68+
scope.inTransaction( session -> {
69+
session.persist( new Dummy( "d10" ) );
70+
session.persist( new Dummy( "d11" ) );
71+
} );
72+
73+
// One more row inserted with ID 22
74+
// One call to the DB for next sequence value generation which returns 26 (IDs 22-26 allocated)
75+
// Before the fix, this would result in a duplicate ID error (since batch insert used IDs 16 to 24)
76+
scope.inTransaction( session -> {
77+
session.persist( new Dummy( "d22" ) );
78+
} );
79+
}
80+
81+
@Entity(name = "Dummy")
82+
static class Dummy {
83+
@Id
84+
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "dummy_seq")
85+
@SequenceGenerator(name = "dummy_seq", sequenceName = "dummy_seq", allocationSize = 5)
86+
private Long id;
87+
88+
private String name;
89+
90+
public Dummy() {
91+
}
92+
93+
public Dummy(String name) {
94+
this.name = name;
95+
}
96+
97+
public Long getId() {
98+
return id;
99+
}
100+
101+
public void setId(Long id) {
102+
this.id = id;
103+
}
104+
105+
public String getName() {
106+
return name;
107+
}
108+
109+
public void setName(String name) {
110+
this.name = name;
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)