Repository: hibernate/hibernate-reactive
Pull request #2094 (2025-01-30 16:31:12)

Add createMutationQuery method that accept criterias
Fix for https://github.com/hibernate/hibernate-reactive/issues/1885

gavinking: @dreab8 Why did you add a type parameter instead of using my suggestions with wildcarded types?
dreab8: I noticed the other methods are using the same approach
gavinking: > I noticed the other methods are using the same approach, is it wrong?

Well the other methods return `Query<T>`, so the type of the parameter is correlated with the return type.

But for these methods there is no such correlation, and so it's simpler and clearer to just use a wildcard.
dreab8: thanks for the explanation, I'm going to remove the type parameters and use the wildcard types

Patch:
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java
index e0a0eb4e..c561a412 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java
@@ -26,6 +26,8 @@
 import org.hibernate.proxy.HibernateProxy;
 import org.hibernate.query.Order;
 import org.hibernate.query.Page;
+import org.hibernate.query.criteria.HibernateCriteriaBuilder;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.reactive.common.AffectedEntities;
 import org.hibernate.reactive.common.Identifier;
 import org.hibernate.reactive.common.ResultSetMapping;
@@ -957,6 +959,39 @@ default Uni<Void> lock(Object entity, LockModeType lockModeType) {
 		 */
 		MutationQuery createMutationQuery(String queryString);
 
+		/**
+		 * Create an instance of {@link MutationQuery} for the given update tree.
+		 *
+		 * @param updateQuery the update criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(CriteriaUpdate)
+		 */
+		MutationQuery createMutationQuery(CriteriaUpdate<?> updateQuery);
+
+		/**
+		 * Create an instance of {@link MutationQuery} for the given delete tree.
+		 *
+		 * @param deleteQuery the delete criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(CriteriaDelete)
+		 */
+		MutationQuery createMutationQuery(CriteriaDelete<?> deleteQuery);
+
+		/**
+		 * Create a {@link MutationQuery} from the given insert select criteria tree
+		 *
+		 * @param insert the insert select criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(JpaCriteriaInsert)
+		 */
+		MutationQuery createMutationQuery(JpaCriteriaInsert<?> insert);
+
 		/**
 		 * Create an instance of {@link Query} for the given HQL/JPQL query
 		 * string or HQL/JPQL update or delete statement. In the case of an
@@ -2327,7 +2362,7 @@ default <T> Uni<T> withStatelessTransaction(Function<StatelessSession, Uni<T>> w
 		 * @return an instance of {@link CriteriaBuilder} for creating
 		 * criteria queries.
 		 */
-		CriteriaBuilder getCriteriaBuilder();
+		HibernateCriteriaBuilder getCriteriaBuilder();
 
 		/**
 		 * Obtain the JPA {@link Metamodel} for the persistence unit.
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java
index aecc99e3..f21bed53 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java
@@ -15,6 +15,7 @@
 import org.hibernate.Cache;
 import org.hibernate.internal.SessionCreationOptions;
 import org.hibernate.internal.SessionFactoryImpl;
+import org.hibernate.query.criteria.HibernateCriteriaBuilder;
 import org.hibernate.reactive.common.spi.Implementor;
 import org.hibernate.reactive.context.Context;
 import org.hibernate.reactive.context.impl.BaseKey;
@@ -30,7 +31,6 @@
 import org.hibernate.stat.Statistics;
 
 import io.smallrye.mutiny.Uni;
-import jakarta.persistence.criteria.CriteriaBuilder;
 import jakarta.persistence.metamodel.Metamodel;
 
 import static org.hibernate.reactive.common.InternalStateAssertions.assertUseOnEventLoop;
@@ -245,7 +245,7 @@ public <T> Uni<T> withStatelessTransaction(String tenantId, BiFunction<Mutiny.St
 	}
 
 	@Override
-	public CriteriaBuilder getCriteriaBuilder() {
+	public HibernateCriteriaBuilder getCriteriaBuilder() {
 		return delegate.getCriteriaBuilder();
 	}
 
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java
index f36dc31e..56cdc727 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java
@@ -22,6 +22,7 @@
 import org.hibernate.LockMode;
 import org.hibernate.LockOptions;
 import org.hibernate.graph.RootGraph;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.reactive.common.AffectedEntities;
 import org.hibernate.reactive.common.Identifier;
 import org.hibernate.reactive.common.ResultSetMapping;
@@ -124,6 +125,21 @@ public MutationQuery createMutationQuery(String queryString) {
 		return new MutinyMutationQueryImpl<>( delegate.createReactiveQuery( queryString ), factory );
 	}
 
+	@Override
+	public MutationQuery createMutationQuery(CriteriaUpdate<?> updateQuery) {
+		return new MutinyMutationQueryImpl<>( delegate.createReactiveMutationQuery( updateQuery ), factory );
+	}
+
+	@Override
+	public MutationQuery createMutationQuery(CriteriaDelete<?> deleteQuery) {
+		return new MutinyMutationQueryImpl<>( delegate.createReactiveMutationQuery( deleteQuery ), factory );
+	}
+
+	@Override
+	public MutationQuery createMutationQuery(JpaCriteriaInsert<?> insert) {
+		return new MutinyMutationQueryImpl<>( delegate.createReactiveMutationQuery( insert ), factory  );
+	}
+
 	@Override @Deprecated
 	public <R> Query<R> createQuery(String queryString) {
 		return new MutinyQueryImpl<>( delegate.createReactiveQuery( queryString ), factory );
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveQueryProducer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveQueryProducer.java
index e67605c8..9b720b5a 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveQueryProducer.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveQueryProducer.java
@@ -11,7 +11,7 @@
 import org.hibernate.dialect.Dialect;
 import org.hibernate.engine.spi.SessionFactoryImplementor;
 import org.hibernate.engine.spi.SharedSessionContractImplementor;
-import org.hibernate.query.criteria.JpaCriteriaInsertSelect;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.reactive.common.AffectedEntities;
 import org.hibernate.reactive.common.ResultSetMapping;
 import org.hibernate.reactive.query.ReactiveMutationQuery;
@@ -79,7 +79,7 @@ public interface ReactiveQueryProducer extends ReactiveConnectionSupplier {
 
 	<R> ReactiveMutationQuery<R> createReactiveMutationQuery(CriteriaDelete<R> deleteQuery);
 
-	<R> ReactiveMutationQuery<R> createReactiveMutationQuery(JpaCriteriaInsertSelect<R> insertSelect);
+	<R> ReactiveMutationQuery<R> createReactiveMutationQuery(JpaCriteriaInsert<R> insert);
 
 	<R> ReactiveMutationQuery<R> createNativeReactiveMutationQuery(String sqlString);
 
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java
index c746c954..f81eab1f 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java
@@ -69,7 +69,7 @@
 import org.hibernate.proxy.HibernateProxy;
 import org.hibernate.proxy.LazyInitializer;
 import org.hibernate.query.IllegalMutationQueryException;
-import org.hibernate.query.criteria.JpaCriteriaInsertSelect;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.query.hql.spi.SqmQueryImplementor;
 import org.hibernate.query.named.NamedResultSetMappingMemento;
 import org.hibernate.query.spi.HqlInterpretation;
@@ -78,7 +78,7 @@
 import org.hibernate.query.sqm.internal.SqmUtil;
 import org.hibernate.query.sqm.tree.SqmStatement;
 import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
-import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
+import org.hibernate.query.sqm.tree.insert.SqmInsertStatement;
 import org.hibernate.query.sqm.tree.select.SqmQueryGroup;
 import org.hibernate.query.sqm.tree.select.SqmQuerySpec;
 import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
@@ -543,10 +543,10 @@ public <R> ReactiveMutationQuery<R> createReactiveMutationQuery(CriteriaDelete<R
 	}
 
 	@Override
-	public <R> ReactiveMutationQuery<R> createReactiveMutationQuery(JpaCriteriaInsertSelect<R> insertSelect) {
+	public <R> ReactiveMutationQuery<R> createReactiveMutationQuery(JpaCriteriaInsert<R> insert) {
 		checkOpen();
 		try {
-			return createReactiveCriteriaQuery( (SqmInsertSelectStatement<R>) insertSelect, null );
+			return createReactiveCriteriaQuery( (SqmInsertStatement<R>) insert, null );
 		}
 		catch ( RuntimeException e ) {
 			throw getExceptionConverter().convert( e );
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java
index 5e02b938..2e9dd3df 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java
@@ -45,7 +45,7 @@
 import org.hibernate.proxy.HibernateProxy;
 import org.hibernate.proxy.LazyInitializer;
 import org.hibernate.query.IllegalMutationQueryException;
-import org.hibernate.query.criteria.JpaCriteriaInsertSelect;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.query.hql.spi.SqmQueryImplementor;
 import org.hibernate.query.named.NamedResultSetMappingMemento;
 import org.hibernate.query.spi.HqlInterpretation;
@@ -55,7 +55,7 @@
 import org.hibernate.query.sqm.internal.SqmUtil;
 import org.hibernate.query.sqm.tree.SqmStatement;
 import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
-import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
+import org.hibernate.query.sqm.tree.insert.SqmInsertStatement;
 import org.hibernate.query.sqm.tree.select.SqmQueryGroup;
 import org.hibernate.query.sqm.tree.select.SqmQuerySpec;
 import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
@@ -998,10 +998,10 @@ public <R> ReactiveMutationQuery<R> createReactiveMutationQuery(CriteriaDelete<R
 	}
 
 	@Override
-	public <R> ReactiveMutationQuery<R> createReactiveMutationQuery(JpaCriteriaInsertSelect<R> insertSelect) {
+	public <R> ReactiveMutationQuery<R> createReactiveMutationQuery(JpaCriteriaInsert<R> insert) {
 		checkOpen();
 		try {
-			return createReactiveCriteriaQuery( (SqmInsertSelectStatement<R>) insertSelect, null );
+			return createReactiveCriteriaQuery( (SqmInsertStatement<R>) insert, null );
 		}
 		catch ( RuntimeException e ) {
 			throw getExceptionConverter().convert( e );
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java
index f009b8dc..7741201a 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java
@@ -27,6 +27,8 @@
 import org.hibernate.proxy.HibernateProxy;
 import org.hibernate.query.Order;
 import org.hibernate.query.Page;
+import org.hibernate.query.criteria.HibernateCriteriaBuilder;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.reactive.common.AffectedEntities;
 import org.hibernate.reactive.common.Identifier;
 import org.hibernate.reactive.common.ResultSetMapping;
@@ -994,6 +996,39 @@ default CompletionStage<Void> lock(Object entity, LockModeType lockModeType) {
 		 */
 		MutationQuery createMutationQuery(String queryString);
 
+		/**
+		 * Create an instance of {@link MutationQuery} for the given update tree.
+		 *
+		 * @param updateQuery the update criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(CriteriaUpdate)
+		 */
+		MutationQuery createMutationQuery(CriteriaUpdate<?> updateQuery);
+
+		/**
+		 * Create an instance of {@link MutationQuery} for the given delete tree.
+		 *
+		 * @param deleteQuery the delete criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(CriteriaDelete)
+		 */
+		MutationQuery createMutationQuery(CriteriaDelete<?> deleteQuery);
+
+		/**
+		 * Create a {@link MutationQuery} from the given insert select criteria tree
+		 *
+		 * @param insert the insert select criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(JpaCriteriaInsert)
+		 */
+		MutationQuery createMutationQuery(JpaCriteriaInsert<?> insert);
+
 		/**
 		 * Create an instance of {@link Query} for the given HQL/JPQL query
 		 * string or HQL/JPQL update or delete statement. In the case of an
@@ -1663,6 +1698,39 @@ default <T> CompletionStage<T> get(Class<T> entityClass, Object id, LockModeType
 		 */
 		MutationQuery createMutationQuery(String queryString);
 
+		/**
+		 * Create an instance of {@link MutationQuery} for the given update tree.
+		 *
+		 * @param updateQuery the update criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(CriteriaUpdate)
+		 */
+		MutationQuery createMutationQuery(CriteriaUpdate<?> updateQuery);
+
+		/**
+		 * Create an instance of {@link MutationQuery} for the given delete tree.
+		 *
+		 * @param deleteQuery the delete criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(CriteriaDelete)
+		 */
+		MutationQuery createMutationQuery(CriteriaDelete<?> deleteQuery);
+
+		/**
+		 * Create a {@link MutationQuery} from the given insert select criteria tree
+		 *
+		 * @param insert the insert select criteria query
+		 *
+		 * @return The {@link MutationQuery} instance for manipulation and execution
+		 *
+		 * @see org.hibernate.query.QueryProducer#createMutationQuery(JpaCriteriaInsert)
+		 */
+		MutationQuery createMutationQuery(JpaCriteriaInsert<?> insert);
+
 		/**
 		 * Create an instance of {@link Query} for the given SQL query string,
 		 * or SQL update, insert, or delete statement. In the case of an update,
@@ -2362,7 +2430,7 @@ default <T> CompletionStage<T> withStatelessTransaction(Function<StatelessSessio
 		 * @return an instance of {@link CriteriaBuilder} for creating
 		 * criteria queries.
 		 */
-		CriteriaBuilder getCriteriaBuilder();
+		HibernateCriteriaBuilder getCriteriaBuilder();
 
 		/**
 		 * Obtain the JPA {@link Metamodel} for the persistence unit.
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java
index b03df1fc..78d52e68 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java
@@ -5,11 +5,11 @@
  */
 package org.hibernate.reactive.stage.impl;
 
-import jakarta.persistence.criteria.CriteriaBuilder;
 import jakarta.persistence.metamodel.Metamodel;
 import org.hibernate.Cache;
 import org.hibernate.internal.SessionCreationOptions;
 import org.hibernate.internal.SessionFactoryImpl;
+import org.hibernate.query.criteria.HibernateCriteriaBuilder;
 import org.hibernate.reactive.common.spi.Implementor;
 import org.hibernate.reactive.context.Context;
 import org.hibernate.reactive.context.impl.BaseKey;
@@ -274,7 +274,7 @@ public boolean isOpen() {
 	}
 
 	@Override
-	public CriteriaBuilder getCriteriaBuilder() {
+	public HibernateCriteriaBuilder getCriteriaBuilder() {
 		return delegate.getCriteriaBuilder();
 	}
 
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java
index fc6914c6..20f9644a 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java
@@ -22,6 +22,7 @@
 import org.hibernate.LockOptions;
 import org.hibernate.graph.RootGraph;
 import org.hibernate.jpa.internal.util.LockModeTypeHelper;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.reactive.common.AffectedEntities;
 import org.hibernate.reactive.common.Identifier;
 import org.hibernate.reactive.common.ResultSetMapping;
@@ -117,6 +118,21 @@ public MutationQuery createMutationQuery(String queryString) {
 		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( queryString ) );
 	}
 
+	@Override
+	public MutationQuery createMutationQuery(CriteriaUpdate<?> updateQuery) {
+		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( updateQuery ) );
+	}
+
+	@Override
+	public MutationQuery createMutationQuery(CriteriaDelete<?> deleteQuery) {
+		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( deleteQuery ) );
+	}
+
+	@Override
+	public MutationQuery createMutationQuery(JpaCriteriaInsert<?> insert) {
+		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( insert ) );
+	}
+
 	@Override
 	public <T> CompletionStage<T> find(Class<T> entityClass, Object primaryKey) {
 		return delegate.reactiveFind( entityClass, primaryKey, null, null );
diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java
index 6e05c747..0d7c5d83 100644
--- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java
+++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java
@@ -11,6 +11,7 @@
 import jakarta.persistence.criteria.CriteriaUpdate;
 import org.hibernate.LockMode;
 import org.hibernate.graph.spi.RootGraphImplementor;
+import org.hibernate.query.criteria.JpaCriteriaInsert;
 import org.hibernate.reactive.common.ResultSetMapping;
 import org.hibernate.reactive.pool.ReactiveConnection;
 import org.hibernate.reactive.session.ReactiveStatelessSession;
@@ -276,6 +277,21 @@ public MutationQuery createMutationQuery(String queryString) {
 		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( queryString ) );
 	}
 
+	@Override
+	public MutationQuery createMutationQuery(CriteriaUpdate<?> updateQuery) {
+		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( updateQuery ) );
+	}
+
+	@Override
+	public MutationQuery createMutationQuery(CriteriaDelete<?> deleteQuery) {
+		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( deleteQuery )  );
+	}
+
+	@Override
+	public MutationQuery createMutationQuery(JpaCriteriaInsert<?> insert) {
+		return new StageMutationQueryImpl<>( delegate.createReactiveMutationQuery( insert ) );
+	}
+
 	@Override
 	public <R> SelectionQuery<R> createQuery(String queryString, Class<R> resultType) {
 		return new StageSelectionQueryImpl<>( delegate.createReactiveQuery( queryString, resultType ) );


Test patch:
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CriteriaMutationQueryTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CriteriaMutationQueryTest.java
new file mode 100644
index 00000000..356a93bd
--- /dev/null
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CriteriaMutationQueryTest.java
@@ -0,0 +1,326 @@
+/* Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright: Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.reactive;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+import org.hibernate.query.criteria.HibernateCriteriaBuilder;
+import org.hibernate.query.criteria.JpaCriteriaInsertSelect;
+import org.hibernate.query.criteria.JpaCriteriaInsertValues;
+import org.hibernate.query.criteria.JpaCriteriaQuery;
+import org.hibernate.query.criteria.JpaRoot;
+import org.hibernate.reactive.mutiny.Mutiny;
+import org.hibernate.reactive.stage.Stage;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.vertx.junit5.Timeout;
+import io.vertx.junit5.VertxTestContext;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Tuple;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaDelete;
+import jakarta.persistence.criteria.CriteriaUpdate;
+import jakarta.persistence.criteria.Root;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Timeout(value = 10, timeUnit = MINUTES)
+public class CriteriaMutationQueryTest extends BaseReactiveTest {
+	private static final Integer SPELT_ID = 1;
+	private static final String SPELT_NAME = "Spelt";
+	private static final String SPELT_TYPE = "Wheat flour";
+	Flour spelt = new Flour( SPELT_ID, SPELT_NAME, "An ancient grain, is a hexaploid species of wheat.", SPELT_TYPE );
+	Flour rye = new Flour( 2, "Rye", "Used to bake the traditional sourdough breads of Germany.", "Wheat flour" );
+	Flour almond = new Flour( 3, "Almond", "made from ground almonds.", "Gluten free" );
+
+	@Override
+	protected Collection<Class<?>> annotatedEntities() {
+		return List.of( Flour.class );
+	}
+
+	@BeforeEach
+	public void populateDb(VertxTestContext context) {
+		test( context, getSessionFactory().withTransaction( s -> s.persist( spelt, rye, almond ) ) );
+	}
+
+	@Test
+	public void testStageUpdateCriteriaQuery(VertxTestContext context) {
+		String updatedDescription = "Most rye breads use a mix of rye and wheat flours";
+		test( context, getSessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( criteriaUpdate( getCriteriaBuilder( s ), updatedDescription, rye ) )
+						.executeUpdate()
+				)
+				.thenAccept( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.thenCompose( v -> getSessionFactory()
+						.withTransaction( s -> s.find( Flour.class, rye.getId() ) ) )
+				.thenAccept( result -> assertThat( result.getDescription() ).isEqualTo( updatedDescription ) )
+		);
+	}
+
+	@Test
+	public void testMutinyUpdateCriteriaQuery(VertxTestContext context) {
+		String updatedDescription = "made from ground almonds.";
+		test( context, getMutinySessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( criteriaUpdate( getCriteriaBuilder( s ), updatedDescription, almond ) )
+						.executeUpdate()
+				)
+				.invoke( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.chain( v -> getMutinySessionFactory()
+						.withTransaction( s -> s.find( Flour.class, almond.getId() ) ) )
+				.invoke( result -> assertThat( result.getDescription() ).isEqualTo( updatedDescription ) )
+		);
+	}
+
+	@Test
+	public void testStageDeleteCriteriaQuery(VertxTestContext context) {
+		test( context, getSessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( criteriaUpdate( getCriteriaBuilder( s ) ) )
+						.executeUpdate()
+				)
+				.thenAccept( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.thenCompose( v -> getSessionFactory()
+						.withTransaction( s -> s.find( Flour.class, spelt.getId() ) ) )
+				.thenAccept( result -> assertThat( result ).isNull() )
+		);
+	}
+
+	@Test
+	public void testMutinyDeleteCriteriaQuery(VertxTestContext context) {
+		test( context, getMutinySessionFactory()
+				.withTransaction( s -> {
+					return s.createMutationQuery( criteriaUpdate( getCriteriaBuilder( s ) ) ).executeUpdate();
+				} )
+				.invoke( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.chain( v -> getMutinySessionFactory()
+						.withTransaction( s -> s.find( Flour.class, spelt.getId() ) ) )
+				.invoke( result -> assertThat( result ).isNull() )
+		);
+	}
+
+	@Test
+	public void testStageInsertCriteriaQuery(VertxTestContext context) {
+		final int id = 4;
+		final String flourName = "Rye";
+		final String flourDescription = "Used to bake the traditional sourdough breads of Germany.";
+		final String flourType = "Wheat flour";
+		test( context, getSessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( insertCriteria( getCriteriaBuilder( s ), id, flourName, flourDescription, flourType ) )
+						.executeUpdate()
+				)
+				.thenAccept( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.thenCompose( v -> getSessionFactory()
+						.withTransaction( s -> s.find( Flour.class, id ) ) )
+				.thenAccept( result -> {
+					assertThat( result ).isNotNull();
+					assertThat( result.name ).isEqualTo( flourName );
+					assertThat( result.description ).isEqualTo( flourDescription );
+					assertThat( result.type ).isEqualTo( flourType );
+				} )
+		);
+	}
+
+	@Test
+	public void testMutinyInsertCriteriaQuery(VertxTestContext context) {
+		final int id = 4;
+		final String flourName = "Almond";
+		final String flourDescription = "made from ground almonds.";
+		final String flourType = "Gluten free";
+		test( context, getMutinySessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( insertCriteria( getCriteriaBuilder( s ), id, flourName, flourDescription, flourType ) )
+						.executeUpdate()
+				)
+				.invoke( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.chain( v -> getMutinySessionFactory()
+						.withTransaction( s -> s.find( Flour.class, id ) ) )
+				.invoke( result -> {
+					assertThat( result ).isNotNull();
+					assertThat( result.name ).isEqualTo( flourName );
+					assertThat( result.description ).isEqualTo( flourDescription );
+					assertThat( result.type ).isEqualTo( flourType );
+				} )
+		);
+	}
+
+	@Test
+	public void testStageInsertSelectCriteriaQuery(VertxTestContext context) {
+		final int idOfTheNewFlour = 4;
+		test( context, getSessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( insertSelectCriteria( getCriteriaBuilder( s ), idOfTheNewFlour ) )
+						.executeUpdate()
+				)
+				.thenAccept( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.thenCompose( v -> getSessionFactory()
+						.withTransaction( s -> s.find( Flour.class, idOfTheNewFlour ) ) )
+				.thenAccept( result -> {
+					assertThat( result ).isNotNull();
+					assertThat( result.name ).isEqualTo( SPELT_NAME );
+					assertThat( result.description ).isNull();
+					assertThat( result.type ).isEqualTo( SPELT_TYPE );
+				} )
+		);
+	}
+
+	@Test
+	public void testMutinyInsertSelectCriteriaQuery(VertxTestContext context) {
+		final int idOfTheNewFlour = 4;
+		test( context, getMutinySessionFactory()
+				.withTransaction( s -> s
+						.createMutationQuery( insertSelectCriteria( getCriteriaBuilder( s ), idOfTheNewFlour ) )
+						.executeUpdate()
+				)
+				.invoke( resultCount -> assertThat( resultCount ).isEqualTo( 1 ) )
+				.chain( v -> getMutinySessionFactory()
+						.withTransaction( s -> s.find( Flour.class, idOfTheNewFlour ) ) )
+				.invoke( result -> {
+					assertThat( result ).isNotNull();
+					assertThat( result.name ).isEqualTo( SPELT_NAME );
+					assertThat( result.description ).isNull();
+					assertThat( result.type ).isEqualTo( SPELT_TYPE );
+				} )
+		);
+	}
+
+	private CriteriaUpdate<Flour> criteriaUpdate(CriteriaBuilder cb, String updatedDescription, Flour rye) {
+		CriteriaUpdate<Flour> criteriaUpdate = cb.createCriteriaUpdate( Flour.class );
+		Root<Flour> from = criteriaUpdate.from( Flour.class );
+		criteriaUpdate.set( from.get( "description" ), updatedDescription );
+		criteriaUpdate.where( cb.equal( from.get( "id" ), rye.getId() ) );
+		return criteriaUpdate;
+	}
+
+	private CriteriaDelete<Flour> criteriaUpdate(CriteriaBuilder criteriaBuilder) {
+		CriteriaDelete<Flour> criteriaDelete = criteriaBuilder.createCriteriaDelete( Flour.class );
+		Root<Flour> from = criteriaDelete.from( Flour.class );
+		criteriaDelete.where( criteriaBuilder.equal( from.get( "id" ), spelt.getId() ) );
+		return criteriaDelete;
+	}
+
+	private static JpaCriteriaInsertValues<Flour> insertCriteria(HibernateCriteriaBuilder cb, int id, String name, String description, String type) {
+		JpaCriteriaInsertValues<Flour> insert = cb.createCriteriaInsertValues( Flour.class );
+		JpaRoot<Flour> flour = insert.getTarget();
+		insert.setInsertionTargetPaths(	flour.get( "id" ), flour.get( "name" ), flour.get( "description" ), flour.get( "type" ) );
+		insert.values( cb.values( cb.value( id ), cb.value( name ), cb.value( description ), cb.value( type ) ) );
+		return insert;
+	}
+
+	private static JpaCriteriaInsertSelect<Flour> insertSelectCriteria(
+			HibernateCriteriaBuilder cb,
+			int idOfTheNewFlour) {
+    	/*
+		 The query executes and insert of Flour with id equals to 2 a name and type
+		 selected from the existing spelt flour saved in the db
+	 	*/
+		JpaCriteriaInsertSelect<Flour> insertSelect = cb.createCriteriaInsertSelect( Flour.class );
+		// columns to insert
+		JpaRoot<Flour> flour = insertSelect.getTarget();
+		insertSelect.setInsertionTargetPaths( flour.get( "id" ), flour.get( "name" ), flour.get( "type" ) );
+		// select query
+		JpaCriteriaQuery<Tuple> select = cb.createQuery( Tuple.class );
+		JpaRoot<Flour> root = select.from( Flour.class );
+		select.multiselect( cb.literal( idOfTheNewFlour ), root.get( "name" ), root.get( "type" )  );
+		select.where( cb.equal( root.get( "id" ), SPELT_ID ) );
+
+		insertSelect.select( select );
+		return insertSelect;
+	}
+
+	private static HibernateCriteriaBuilder getCriteriaBuilder(Stage.Session session) {
+		return session.getFactory().getCriteriaBuilder();
+	}
+
+	private static HibernateCriteriaBuilder getCriteriaBuilder(Mutiny.Session session) {
+		return session.getFactory().getCriteriaBuilder();
+	}
+
+	@Entity(name = "Flour")
+	@Table(name = "Flour")
+	public static class Flour {
+		@Id
+		private Integer id;
+		private String name;
+		private String description;
+		private String type;
+
+		public Flour() {
+		}
+
+		public Flour(Integer id, String name, String description, String type) {
+			this.id = id;
+			this.name = name;
+			this.description = description;
+			this.type = type;
+		}
+
+		public Integer getId() {
+			return id;
+		}
+
+		public void setId(Integer id) {
+			this.id = id;
+		}
+
+		public String getName() {
+			return name;
+		}
+
+		public void setName(String name) {
+			this.name = name;
+		}
+
+		public String getDescription() {
+			return description;
+		}
+
+		public void setDescription(String description) {
+			this.description = description;
+		}
+
+		public String getType() {
+			return type;
+		}
+
+		public void setType(String type) {
+			this.type = type;
+		}
+
+		@Override
+		public String toString() {
+			return name;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if ( this == o ) {
+				return true;
+			}
+			if ( o == null || getClass() != o.getClass() ) {
+				return false;
+			}
+			Flour flour = (Flour) o;
+			return Objects.equals( name, flour.name ) &&
+					Objects.equals( description, flour.description ) &&
+					Objects.equals( type, flour.type );
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash( name, description, type );
+		}
+	}
+}