From f006fbf029ae3a20ad10d1460c15153cd4679ff4 Mon Sep 17 00:00:00 2001
From: Andrea Boriero <andrea@hibernate.org>
Date: Thu, 6 Mar 2025 16:39:44 +0100
Subject: [PATCH 1/2] [#1867] Error when inserting in batch with joined table
 inheritance

---
 .../ReactiveMutationExecutorStandard.java     | 90 +++++++++++++++++--
 1 file changed, 83 insertions(+), 7 deletions(-)

diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java
index 4f1d62325..0a09ff3a1 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java
@@ -5,10 +5,6 @@
  */
 package org.hibernate.reactive.engine.jdbc.mutation.internal;
 
-import java.lang.invoke.MethodHandles;
-import java.sql.SQLException;
-import java.util.concurrent.CompletionStage;
-
 import org.hibernate.engine.jdbc.batch.spi.Batch;
 import org.hibernate.engine.jdbc.mutation.JdbcValueBindings;
 import org.hibernate.engine.jdbc.mutation.OperationResultChecker;
@@ -25,6 +21,7 @@
 import org.hibernate.persister.entity.mutation.EntityTableMapping;
 import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor;
 import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor;
+import org.hibernate.reactive.engine.jdbc.ResultsCheckerUtil;
 import org.hibernate.reactive.engine.jdbc.env.internal.ReactiveMutationExecutor;
 import org.hibernate.reactive.generator.values.ReactiveGeneratedValuesMutationDelegate;
 import org.hibernate.reactive.logging.impl.Log;
@@ -37,9 +34,16 @@
 import org.hibernate.sql.model.TableMapping;
 import org.hibernate.sql.model.ValuesAnalysis;
 
+import java.lang.invoke.MethodHandles;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+
 import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.checkResults;
 import static org.hibernate.reactive.logging.impl.LoggerFactory.make;
 import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture;
+import static org.hibernate.reactive.util.impl.CompletionStages.loop;
 import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture;
 import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture;
 import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER;
@@ -73,10 +77,64 @@ private ReactiveConnection connection(SharedSessionContractImplementor session)
 	@Override
 	public CompletionStage<Void> performReactiveBatchedOperations(
 			ValuesAnalysis valuesAnalysis,
-			TableInclusionChecker inclusionChecker, OperationResultChecker resultChecker,
+			TableInclusionChecker inclusionChecker,
+			OperationResultChecker resultChecker,
 			SharedSessionContractImplementor session) {
-		return ReactiveMutationExecutor.super
-				.performReactiveBatchedOperations( valuesAnalysis, inclusionChecker, resultChecker, session);
+		final PreparedStatementGroup batchedMutationOperationGroup = getBatchedPreparedStatementGroup();
+		if ( batchedMutationOperationGroup != null ) {
+			final List<PreparedStatementDetails> preparedStatementDetailsList = new ArrayList<>(
+					batchedMutationOperationGroup.getNumberOfStatements() );
+			batchedMutationOperationGroup.forEachStatement( (tableName, statementDetails) -> preparedStatementDetailsList
+					.add( statementDetails ) );
+			return loop( preparedStatementDetailsList, statementDetails -> {
+					  if ( statementDetails == null ) {
+						  return voidFuture();
+					  }
+					  final JdbcValueBindings valueBindings = getJdbcValueBindings();
+					  final TableMapping tableDetails = statementDetails.getMutatingTableDetails();
+					  if ( inclusionChecker != null && !inclusionChecker.include( tableDetails ) ) {
+						  if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) {
+							  MODEL_MUTATION_LOGGER.tracef(
+									  "Skipping execution of secondary insert : %s",
+									  tableDetails.getTableName()
+							  );
+						  }
+						  return voidFuture();
+					  }
+
+					  // If we get here the statement is needed - make sure it is resolved
+					  final Object[] paramValues = PreparedStatementAdaptor.bind( statement -> {
+						  PreparedStatementDetails details = new PrepareStatementDetailsAdaptor(
+								  statementDetails,
+								  statement,
+								  session.getJdbcServices()
+						  );
+						  valueBindings.beforeStatement( details );
+					  } );
+
+					  final ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection();
+					  final String sql = statementDetails.getSqlString();
+					  return reactiveConnection.update(
+							  sql,
+							  paramValues,
+							  true,
+							  (rowCount, batchPosition, query) -> ResultsCheckerUtil.checkResults(
+									  session,
+									  statementDetails,
+									  resultChecker,
+									  rowCount,
+									  batchPosition
+							  )
+					  ).whenComplete( (o, throwable) -> { //TODO: is this part really needed?
+						  if ( statementDetails.getStatement() != null ) {
+							  statementDetails.releaseStatement( session );
+						  }
+						  valueBindings.afterStatement( tableDetails );
+					  } );
+				  }
+			);
+		}
+		return voidFuture();
 	}
 
 	@Override
@@ -159,6 +217,23 @@ public CompletionStage<GeneratedValues> performReactiveNonBatchedOperations(
 		}
 	}
 
+	@Override
+	public CompletionStage<Void> performReactiveSelfExecutingOperations(
+			ValuesAnalysis valuesAnalysis,
+			TableInclusionChecker inclusionChecker,
+			SharedSessionContractImplementor session) {
+		if ( getSelfExecutingMutations() == null || getSelfExecutingMutations().isEmpty() ) {
+			return voidFuture();
+		}
+
+		return loop( getSelfExecutingMutations(), operation -> {
+			if ( inclusionChecker.include( operation.getTableDetails() ) ) {
+				operation.performMutation( getJdbcValueBindings(), valuesAnalysis, session );
+			}
+			return voidFuture();
+		});
+	}
+
 	private class OperationsForEach {
 
 		private final Object id;
@@ -210,6 +285,7 @@ public CompletionStage<Void> buildLoop() {
 			return loop;
 		}
 	}
+
 	@Override
 	public CompletionStage<Void> performReactiveNonBatchedMutation(
 			PreparedStatementDetails statementDetails,

From 9598c9c60d6fc620af203ca85e9dd4bdf8d2ed10 Mon Sep 17 00:00:00 2001
From: Davide D'Alto <davide@hibernate.org>
Date: Tue, 27 Feb 2024 12:59:51 +0100
Subject: [PATCH 2/2] [#1867] Test case

---
 .../reactive/JoinedInheritanceBatchTest.java  | 130 ++++++++
 .../ManyToManyWithCompositeIdTest.java        | 293 ++++++++++++++++++
 2 files changed, 423 insertions(+)
 create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java
 create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java

diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java
new file mode 100644
index 000000000..0c49d883e
--- /dev/null
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java
@@ -0,0 +1,130 @@
+/* Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright: Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.reactive;
+
+
+import org.junit.jupiter.api.Test;
+
+import io.vertx.junit5.Timeout;
+import io.vertx.junit5.VertxTestContext;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Inheritance;
+import jakarta.persistence.InheritanceType;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture;
+
+@Timeout(value = 10, timeUnit = MINUTES)
+public class JoinedInheritanceBatchTest extends BaseReactiveTest {
+
+	@Override
+	protected Collection<Class<?>> annotatedEntities() {
+		return List.of( ClientA.class, Client.class );
+	}
+
+	@Override
+	protected CompletionStage<Void> cleanDb() {
+		return voidFuture();
+	}
+
+	@Test
+	public void test(VertxTestContext context) {
+		final ClientA client1 = new ClientA("Client 1", "email@c1", "123456");
+
+		test( context, getMutinySessionFactory().withTransaction( session -> {
+						  session.setBatchSize( 5 );
+						  return session.persist( client1 );
+					  } )
+					  .chain( () -> getMutinySessionFactory().withTransaction( session -> session
+							  .createQuery( "select c from Client c", Client.class )
+							  .getResultList()
+							  .invoke( persistedClients -> assertThat( persistedClients )
+									  .as( "Clients has not bee persisted" )
+									  .isNotEmpty() ) ) )
+		);
+	}
+
+	@Entity(name = "Client")
+	@Table(name = "`Client`")
+	@Inheritance(strategy = InheritanceType.JOINED)
+	public static class Client {
+
+		@Id
+		@SequenceGenerator(name = "seq", sequenceName = "id_seq", allocationSize = 1)
+		@GeneratedValue(generator = "seq", strategy = GenerationType.SEQUENCE)
+		private Long id;
+
+		private String name;
+
+		private String email;
+
+		private String phone;
+
+		public Client() {
+		}
+
+		public Client(String name, String email, String phone) {
+			this.name = name;
+			this.email = email;
+			this.phone = phone;
+		}
+
+		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;
+		}
+
+		public String getEmail() {
+			return email;
+		}
+
+		public void setEmail(String email) {
+			this.email = email;
+		}
+
+		public String getPhone() {
+			return phone;
+		}
+
+		public void setPhone(String phone) {
+			this.phone = phone;
+		}
+
+	}
+
+	@Entity
+	@Table(name = "`ClientA`")
+	public static class ClientA extends Client {
+
+		public ClientA() {
+		}
+
+		public ClientA(String name, String email, String phone) {
+			super( name, email, phone );
+		}
+	}
+
+}
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java
new file mode 100644
index 000000000..8c72c38d3
--- /dev/null
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java
@@ -0,0 +1,293 @@
+/* Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright: Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.reactive;
+
+import io.vertx.junit5.Timeout;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import io.vertx.junit5.VertxTestContext;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.IdClass;
+import jakarta.persistence.Inheritance;
+import jakarta.persistence.InheritanceType;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
+import java.util.concurrent.CompletionStage;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture;
+
+@Timeout(value = 10, timeUnit = MINUTES)
+public class ManyToManyWithCompositeIdTest extends BaseReactiveTest {
+
+	@Override
+	protected Collection<Class<?>> annotatedEntities() {
+		return List.of( CarsClients.class, ClientA.class, Client.class, Car.class );
+	}
+
+	@Override
+	protected CompletionStage<Void> cleanDb() {
+		return voidFuture();
+	}
+
+	@Test
+	public void test(VertxTestContext context) {
+		List<Client> clients = new ArrayList<>();
+		for ( int i = 0; i < 5; i++ ) {
+			ClientA client = new ClientA();
+			client.setName( "name" + i );
+			client.setEmail( "email" + i );
+			client.setPhone( "phone" + i );
+			clients.add( client );
+		}
+
+		List<Car> cars = new ArrayList<>();
+		for ( int i = 0; i < 2; i++ ) {
+			Car car = new Car();
+			car.setBrand( "brand" + i );
+			car.setModel( "model" + i );
+			cars.add( car );
+		}
+
+		test( context, getMutinySessionFactory()
+				.withSession( session -> {
+					session.setBatchSize( 5 );
+					return session.persistAll( cars.toArray() )
+							.chain( () -> session
+									.persistAll( clients.toArray() )
+									.chain( session::flush ) )
+							.chain( () -> {
+								List<CarsClients> carsClientsList = new ArrayList<>();
+								for ( Client client : clients ) {
+									for ( Car car : cars ) {
+										CarsClients carsClients = new CarsClients( "location" );
+										carsClientsList.add( carsClients );
+										car.addClient( carsClients );
+										client.addCar( carsClients );
+									}
+								}
+								return session
+										.persistAll( carsClientsList.toArray() )
+										.chain( session::flush );
+							} );
+				} )
+		);
+	}
+
+	@Entity(name = "Car")
+	@Table(name = "Car_Table")
+	public static class Car {
+
+		@Id
+		@SequenceGenerator(name = "seq_car", sequenceName = "id_seq_car", allocationSize = 1)
+		@GeneratedValue(generator = "seq_car", strategy = GenerationType.SEQUENCE)
+		private Long id;
+
+		public String brand;
+
+
+		private String model;
+
+		@OneToMany(mappedBy = "car")
+		private Set<CarsClients> clients = new HashSet<>();
+
+		public Car() {
+		}
+
+		public Long getId() {
+			return id;
+		}
+
+		public void setId(Long id) {
+			this.id = id;
+		}
+
+		public String getBrand() {
+			return brand;
+		}
+
+		public void setBrand(String brand) {
+			this.brand = brand;
+		}
+
+		public String getModel() {
+			return model;
+		}
+
+		public void setModel(String model) {
+			this.model = model;
+		}
+
+		public Set<CarsClients> getClients() {
+			return clients;
+		}
+
+		public void setClients(Set<CarsClients> clients) {
+			this.clients = clients;
+		}
+
+		public void addClient(CarsClients carsClients) {
+			carsClients.setCar( this );
+			clients.add( carsClients );
+		}
+	}
+
+	@Entity
+	@Table(name = "Client_Table")
+	@Inheritance(strategy = InheritanceType.JOINED)
+	public static class Client {
+
+		@Id
+		@SequenceGenerator(name = "seq", sequenceName = "id_seq", allocationSize = 1)
+		@GeneratedValue(generator = "seq", strategy = GenerationType.SEQUENCE)
+		private Long id;
+
+		private String name;
+
+		private String email;
+
+		private String phone;
+
+		@OneToMany(mappedBy = "client")
+		private Set<CarsClients> cars = new HashSet<>();
+
+		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;
+		}
+
+		public String getEmail() {
+			return email;
+		}
+
+		public void setEmail(String email) {
+			this.email = email;
+		}
+
+		public String getPhone() {
+			return phone;
+		}
+
+		public void setPhone(String phone) {
+			this.phone = phone;
+		}
+
+		public Set<CarsClients> getCars() {
+			return cars;
+		}
+
+		public void setCars(Set<CarsClients> cars) {
+			this.cars = cars;
+		}
+
+		public void addCar(CarsClients carsClients) {
+			carsClients.setClient( this );
+			cars.add( carsClients );
+		}
+	}
+
+	@Entity
+	@Table(name = "ClientA_Table")
+	public static class ClientA extends Client {
+
+		public ClientA() {
+		}
+	}
+
+	@Entity
+	@IdClass(CarsClientsId.class)
+	@Table(name = "cars_clients_table")
+	public static class CarsClients {
+
+		@Id
+		@ManyToOne
+		private Car car;
+
+		@Id
+		@ManyToOne
+		private Client client;
+
+		private String location;
+
+		public CarsClients() {
+		}
+
+		public CarsClients(String location) {
+			this.location = location;
+		}
+
+		public Car getCar() {
+			return car;
+		}
+
+		public void setCar(Car car) {
+			this.car = car;
+		}
+
+		public Client getClient() {
+			return client;
+		}
+
+		public void setClient(Client client) {
+			this.client = client;
+		}
+
+		public String getLocation() {
+			return location;
+		}
+
+		public void setLocation(String location) {
+			this.location = location;
+		}
+	}
+
+	public static class CarsClientsId {
+		private Car car;
+
+		private Client client;
+
+		public CarsClientsId() {
+		}
+
+		public Car getCar() {
+			return car;
+		}
+
+		public void setCar(Car car) {
+			this.car = car;
+		}
+
+		public Client getClient() {
+			return client;
+		}
+
+		public void setClient(Client client) {
+			this.client = client;
+		}
+	}
+}