diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java
new file mode 100644
index 000000000000..03fa73a60626
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractDatabaseOperation.java
@@ -0,0 +1,170 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.internal;
+
+import org.hibernate.internal.util.collections.CollectionHelper;
+import org.hibernate.sql.exec.spi.ExecutionContext;
+import org.hibernate.sql.exec.spi.DatabaseOperation;
+import org.hibernate.sql.exec.spi.JdbcOperation;
+import org.hibernate.sql.exec.spi.PostAction;
+import org.hibernate.sql.exec.spi.PreAction;
+import org.hibernate.sql.exec.spi.SecondaryAction;
+import org.hibernate.sql.exec.spi.StatementAccess;
+
+import java.sql.Connection;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Abstract support for DatabaseOperation implementations, mainly
+ * managing {@linkplain PreAction pre-} and {@linkplain PostAction post-}
+ * actions.
+ *
+ * @author Steve Ebersole
+ */
+public abstract class AbstractDatabaseOperation
+ implements DatabaseOperation
{
+ protected final PreAction[] preActions;
+ protected final PostAction[] postActions;
+
+ @SuppressWarnings("unused")
+ public AbstractDatabaseOperation() {
+ this( null, null );
+ }
+
+ public AbstractDatabaseOperation(PreAction[] preActions, PostAction[] postActions) {
+ this.preActions = preActions;
+ this.postActions = postActions;
+ }
+
+ protected void performPreActions(
+ StatementAccess statementAccess,
+ Connection jdbcConnection,
+ ExecutionContext executionContext) {
+ if ( preActions != null ) {
+ for ( int i = 0; i < preActions.length; i++ ) {
+ preActions[i].performPreAction( statementAccess, jdbcConnection, executionContext );
+ }
+ }
+ }
+
+ protected void performPostActions(
+ StatementAccess statementAccess,
+ Connection jdbcConnection,
+ ExecutionContext executionContext) {
+ if ( postActions != null ) {
+ for ( int i = 0; i < postActions.length; i++ ) {
+ postActions[i].performPostAction( statementAccess, jdbcConnection, executionContext );
+ }
+ }
+ }
+
+ protected static PreAction[] toPreActionArray(List actions) {
+ if ( CollectionHelper.isEmpty( actions ) ) {
+ return null;
+ }
+ return actions.toArray( new PreAction[0] );
+ }
+
+ protected static PostAction[] toPostActionArray(List actions) {
+ if ( CollectionHelper.isEmpty( actions ) ) {
+ return null;
+ }
+ return actions.toArray( new PostAction[0] );
+ }
+
+ protected abstract static class Builder> {
+ protected List preActions;
+ protected List postActions;
+
+ protected abstract T getThis();
+
+ /**
+ * Appends the {@code actions} to the growing list of pre-actions,
+ * executed (in order) after all currently registered actions.
+ *
+ * @return {@code this}, for method chaining.
+ */
+ public T appendPreAction(PreAction... actions) {
+ if ( preActions == null ) {
+ preActions = new ArrayList<>();
+ }
+ Collections.addAll( preActions, actions );
+ return getThis();
+ }
+
+ /**
+ * Prepends the {@code actions} to the growing list of pre-actions
+ *
+ * @return {@code this}, for method chaining.
+ */
+ public T prependPreAction(PreAction... actions) {
+ if ( preActions == null ) {
+ preActions = new ArrayList<>();
+ }
+ // todo (DatabaseOperation) : should we invert the order of the incoming actions?
+ Collections.addAll( preActions, actions );
+ return getThis();
+ }
+
+ /**
+ * Appends the {@code actions} to the growing list of post-actions
+ *
+ * @return {@code this}, for method chaining.
+ */
+ public T appendPostAction(PostAction... actions) {
+ if ( postActions == null ) {
+ postActions = new ArrayList<>();
+ }
+ Collections.addAll( postActions, actions );
+ return getThis();
+ }
+
+ /**
+ * Prepends the {@code actions} to the growing list of post-actions
+ *
+ * @return {@code this}, for method chaining.
+ */
+ public T prependPostAction(PostAction... actions) {
+ if ( postActions == null ) {
+ postActions = new ArrayList<>();
+ }
+ // todo (DatabaseOperation) : should we invert the order of the incoming actions?
+ Collections.addAll( postActions, actions );
+ return getThis();
+ }
+
+ /**
+ * Adds a secondary action pair.
+ * Assumes the {@code action} implements both {@linkplain PreAction} and {@linkplain PostAction}.
+ *
+ * @apiNote Prefer {@linkplain #addSecondaryActionPair(PreAction, PostAction)} to avoid
+ * the casts needed here.
+ *
+ * @see #prependPreAction
+ * @see #appendPostAction
+ *
+ * @return {@code this}, for method chaining.
+ */
+ public T addSecondaryActionPair(SecondaryAction action) {
+ return addSecondaryActionPair( (PreAction) action, (PostAction) action );
+ }
+
+ /**
+ * Adds a PreAction/PostAction pair.
+ *
+ * @see #prependPreAction
+ * @see #appendPostAction
+ *
+ * @return {@code this}, for method chaining.
+ */
+ public T addSecondaryActionPair(PreAction preAction, PostAction postAction) {
+ prependPreAction( preAction );
+ appendPostAction( postAction );
+ return getThis();
+ }
+ }
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java
new file mode 100644
index 000000000000..e3f524311df7
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/DatabaseOperationSelectImpl.java
@@ -0,0 +1,154 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.internal;
+
+import org.hibernate.engine.jdbc.spi.JdbcServices;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor;
+import org.hibernate.sql.exec.spi.DatabaseOperationSelect;
+import org.hibernate.sql.exec.spi.ExecutionContext;
+import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect;
+import org.hibernate.sql.exec.spi.JdbcParameterBindings;
+import org.hibernate.sql.exec.spi.JdbcSelectExecutor;
+import org.hibernate.sql.exec.spi.PostAction;
+import org.hibernate.sql.exec.spi.PreAction;
+import org.hibernate.sql.results.spi.ResultsConsumer;
+import org.hibernate.sql.results.spi.RowTransformer;
+
+import java.sql.Connection;
+import java.util.Set;
+
+/**
+ * Standard DatabaseOperationSelect implementation.
+ *
+ * @author Steve Ebersole
+ */
+public class DatabaseOperationSelectImpl
+ extends AbstractDatabaseOperation
+ implements DatabaseOperationSelect {
+ private final JdbcOperationQuerySelect primaryOperation;
+
+ public DatabaseOperationSelectImpl(JdbcOperationQuerySelect primaryOperation) {
+ this( null, null, primaryOperation );
+ }
+
+ public DatabaseOperationSelectImpl(
+ PreAction[] preActions,
+ PostAction[] postActions,
+ JdbcOperationQuerySelect primaryOperation) {
+ super( preActions, postActions );
+ this.primaryOperation = primaryOperation;
+ }
+
+ @Override
+ public JdbcOperationQuerySelect getPrimaryOperation() {
+ return primaryOperation;
+ }
+
+ @Override
+ public Set getAffectedTableNames() {
+ return primaryOperation.getAffectedTableNames();
+ }
+
+ @Override
+ public T execute(
+ Class resultType,
+ int expectedNumberOfRows,
+ JdbcSelectExecutor.StatementCreator statementCreator,
+ JdbcParameterBindings jdbcParameterBindings,
+ RowTransformer rowTransformer,
+ ResultsConsumer resultsConsumer,
+ ExecutionContext executionContext) {
+ if ( preActions == null && postActions == null ) {
+ return performPrimaryOperation(
+ resultType,
+ statementCreator,
+ jdbcParameterBindings,
+ rowTransformer,
+ resultsConsumer,
+ executionContext
+ );
+ }
+
+ final SharedSessionContractImplementor session = executionContext.getSession();
+ final LogicalConnectionImplementor logicalConnection = session.getJdbcCoordinator().getLogicalConnection();
+ final SessionFactoryImplementor sessionFactory = session.getSessionFactory();
+
+ final Connection connection = logicalConnection.getPhysicalConnection();
+ final StatementAccessImpl statementAccess = new StatementAccessImpl(
+ connection,
+ logicalConnection,
+ sessionFactory
+ );
+
+ try {
+ try {
+ performPreActions( statementAccess, connection, executionContext );
+ return performPrimaryOperation(
+ resultType,
+ statementCreator,
+ jdbcParameterBindings,
+ rowTransformer,
+ resultsConsumer,
+ executionContext
+ );
+ }
+ finally {
+ performPostActions( statementAccess, connection, executionContext );
+ }
+ }
+ finally {
+ statementAccess.release();
+ }
+ }
+
+ private T performPrimaryOperation(
+ Class resultType,
+ JdbcSelectExecutor.StatementCreator statementCreator,
+ JdbcParameterBindings jdbcParameterBindings,
+ RowTransformer rowTransformer,
+ ResultsConsumer resultsConsumer,
+ ExecutionContext executionContext) {
+ final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory();
+ final JdbcServices jdbcServices = sessionFactory.getJdbcServices();
+ final JdbcSelectExecutor jdbcSelectExecutor = jdbcServices.getJdbcSelectExecutor();
+ return jdbcSelectExecutor.executeQuery(
+ primaryOperation,
+ jdbcParameterBindings,
+ executionContext,
+ rowTransformer,
+ resultType,
+ statementCreator,
+ resultsConsumer
+ );
+ }
+
+ public static Builder builder(JdbcOperationQuerySelect primaryAction) {
+ return new Builder( primaryAction );
+ }
+
+ public static class Builder extends AbstractDatabaseOperation.Builder {
+ private final JdbcOperationQuerySelect primaryAction;
+
+ private Builder(JdbcOperationQuerySelect primaryAction) {
+ this.primaryAction = primaryAction;
+ }
+
+ @Override
+ protected Builder getThis() {
+ return this;
+ }
+
+ public DatabaseOperationSelectImpl build() {
+ if ( preActions == null && postActions == null ) {
+ return new DatabaseOperationSelectImpl( primaryAction );
+ }
+ final PreAction[] preActions = toPreActionArray( this.preActions );
+ final PostAction[] postActions = toPostActionArray( this.postActions );
+ return new DatabaseOperationSelectImpl( preActions, postActions, primaryAction );
+ }
+ }
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java
new file mode 100644
index 000000000000..de370033df26
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcAction.java
@@ -0,0 +1,33 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.internal;
+
+import org.hibernate.sql.exec.spi.ExecutionContext;
+import org.hibernate.sql.exec.spi.DatabaseOperation;
+import org.hibernate.sql.exec.spi.StatementAccess;
+
+import java.sql.Connection;
+
+/**
+ * An action to be performed before or after the primary action of a DatabaseOperation.
+ *
+ * @see DatabaseOperation#getPrimaryOperation()
+ *
+ * @author Steve Ebersole
+ */
+public interface JdbcAction {
+ /**
+ * Perform the action.
+ *
+ * Generally the action should use the passed {@code jdbcStatement} to interact with the
+ * database, although the {@code jdbcConnection} can be used to create specialized statements,
+ * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc.
+ *
+ * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action.
+ * @param jdbcConnection The JDBC Connection.
+ * @param executionContext Access to contextual information useful while executing.
+ */
+ void perform(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext);
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java
new file mode 100644
index 000000000000..db8b85e9fa84
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StatementAccessImpl.java
@@ -0,0 +1,62 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.internal;
+
+import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.resource.jdbc.LogicalConnection;
+import org.hibernate.sql.exec.spi.StatementAccess;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/**
+ * Lazy access to a JDBC {@linkplain Statement}.
+ * Manages various tasks around creation and ensuring it gets cleaned up.
+ *
+ * @author Steve Ebersole
+ */
+public class StatementAccessImpl implements StatementAccess {
+ private final Connection jdbcConnection;
+ private final LogicalConnection logicalConnection;
+ private final SessionFactoryImplementor factory;
+
+ private Statement jdbcStatement;
+
+ public StatementAccessImpl(Connection jdbcConnection, LogicalConnection logicalConnection, SessionFactoryImplementor factory) {
+ this.jdbcConnection = jdbcConnection;
+ this.logicalConnection = logicalConnection;
+ this.factory = factory;
+ }
+
+ @Override public Statement getJdbcStatement() {
+ if ( jdbcStatement == null ) {
+ try {
+ jdbcStatement = jdbcConnection.createStatement();
+ logicalConnection.getResourceRegistry().register( jdbcStatement, false );
+ }
+ catch (SQLException e) {
+ throw factory.getJdbcServices()
+ .getSqlExceptionHelper()
+ .convert( e, "Unable to create JDBC Statement" );
+ }
+ }
+ return jdbcStatement;
+ }
+
+ public void release() {
+ if ( jdbcStatement != null ) {
+ try {
+ jdbcStatement.close();
+ logicalConnection.getResourceRegistry().release( jdbcStatement );
+ }
+ catch (SQLException e) {
+ throw factory.getJdbcServices()
+ .getSqlExceptionHelper()
+ .convert( e, "Unable to release JDBC Statement" );
+ }
+ }
+ }
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java
new file mode 100644
index 000000000000..69f7a0a88a7b
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperation.java
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import org.hibernate.Incubating;
+
+import java.util.Set;
+
+/**
+ * An operation against the database, comprised of a
+ * {@linkplain #getPrimaryOperation primary operation} and
+ * zero-or-more {@linkplain SecondaryAction secondary actions}.
+ *
+ * @author Steve Ebersole
+ */
+@Incubating
+public interface DatabaseOperation {
+ /**
+ * The primary operation for the group.
+ */
+ P getPrimaryOperation();
+
+ /**
+ * The names of tables referenced or affected by this operation.
+ */
+ Set getAffectedTableNames();
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java
new file mode 100644
index 000000000000..32a404c1b220
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationMutation.java
@@ -0,0 +1,33 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import org.hibernate.Incubating;
+
+import java.sql.PreparedStatement;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * {@linkplain DatabaseOperation} whose primary operation is a mutation.
+ *
+ * @author Steve Ebersole
+ */
+@Incubating
+public interface DatabaseOperationMutation extends DatabaseOperation {
+ /**
+ * Perform the execution.
+ *
+ * @param statementCreator Creator for JDBC {@linkplain PreparedStatement statements}.
+ * @param jdbcParameterBindings Bindings for the JDBC parameters.
+ * @param expectationCheck Check used to verify the outcome of the mutation.
+ * @param executionContext Access to contextual information useful while executing.
+ */
+ int execute(
+ Function statementCreator,
+ JdbcParameterBindings jdbcParameterBindings,
+ BiConsumer expectationCheck,
+ ExecutionContext executionContext);
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java
new file mode 100644
index 000000000000..0e44c40571a2
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/DatabaseOperationSelect.java
@@ -0,0 +1,42 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import org.hibernate.Incubating;
+import org.hibernate.sql.exec.spi.JdbcSelectExecutor.StatementCreator;
+import org.hibernate.sql.results.spi.ResultsConsumer;
+import org.hibernate.sql.results.spi.RowTransformer;
+
+import java.sql.PreparedStatement;
+
+/**
+ * {@linkplain DatabaseOperation} whose primary operation is a {@linkplain JdbcOperationQuerySelect selection}.
+ *
+ * @author Steve Ebersole
+ */
+@Incubating
+public interface DatabaseOperationSelect extends DatabaseOperation {
+ /**
+ * Execute the underlying statements and return the result(s).
+ *
+ * @param resultType The expected type of domain result values.
+ * @param expectedNumberOfRows The number of domain results expected.
+ * @param statementCreator Creator for JDBC {@linkplain PreparedStatement statements}.
+ * @param jdbcParameterBindings Bindings for the JDBC parameters.
+ * @param rowTransformer Any row transformation to apply.
+ * @param resultsConsumer Consumer for each domain result.
+ * @param executionContext Access to contextual information useful while executing.
+ *
+ * @return The indicated result(s).
+ */
+ T execute(
+ Class resultType,
+ int expectedNumberOfRows,
+ StatementCreator statementCreator,
+ JdbcParameterBindings jdbcParameterBindings,
+ RowTransformer rowTransformer,
+ ResultsConsumer resultsConsumer,
+ ExecutionContext executionContext);
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java
new file mode 100644
index 000000000000..dc6a8a4cb0da
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PostAction.java
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import org.hibernate.Incubating;
+
+import java.sql.Connection;
+
+/**
+ * An action to be performed after a {@linkplain DatabaseOperation}'s primary operation.
+ */
+@Incubating
+@FunctionalInterface
+public interface PostAction extends SecondaryAction {
+ /**
+ * Perform the action.
+ *
+ * Generally the action should use the passed {@code jdbcStatementAccess} to interact with the
+ * database, although the {@code jdbcConnection} can be used to create specialized statements,
+ * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc.
+ *
+ * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action.
+ * @param jdbcConnection The JDBC Connection.
+ * @param executionContext Access to contextual information useful while executing.
+ */
+ void performPostAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext);
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java
new file mode 100644
index 000000000000..40332913bdd7
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/PreAction.java
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import org.hibernate.Incubating;
+
+import java.sql.Connection;
+
+/**
+ * An action to be performed before a {@linkplain DatabaseOperation}'s primary operation.
+ */
+@Incubating
+@FunctionalInterface
+public interface PreAction extends SecondaryAction {
+ /**
+ * Perform the action.
+ *
+ * Generally the action should use the passed {@code jdbcStatementAccess} to interact with the
+ * database, although the {@code jdbcConnection} can be used to create specialized statements,
+ * access the {@linkplain java.sql.DatabaseMetaData database metadata}, etc.
+ *
+ * @param jdbcStatementAccess Access to a JDBC Statement object which may be used to perform the action.
+ * @param jdbcConnection The JDBC Connection.
+ * @param executionContext Access to contextual information useful while executing.
+ */
+ void performPreAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext);
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java
new file mode 100644
index 000000000000..9976e754fd36
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/SecondaryAction.java
@@ -0,0 +1,17 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import org.hibernate.Incubating;
+
+/**
+ * Common marker interface for {@linkplain PreAction} and {@linkplain PostAction},
+ * which are split to allow implementing both simultaneously.
+ *
+ * @author Steve Ebersole
+ */
+@Incubating
+public interface SecondaryAction {
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java
new file mode 100644
index 000000000000..3891fa1a878e
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/StatementAccess.java
@@ -0,0 +1,22 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.sql.exec.spi;
+
+import java.sql.Statement;
+
+/**
+ * Access to a JDBC {@linkplain Statement}.
+ *
+ * @apiNote Intended for cases where sharing a common JDBC {@linkplain Statement} is useful, generally for performance.
+ * @implNote Manages various tasks around creation and ensuring it gets cleaned up.
+ *
+ * @author Steve Ebersole
+ */
+public interface StatementAccess {
+ /**
+ * Access the JDBC {@linkplain Statement}.
+ */
+ Statement getJdbcStatement();
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java
new file mode 100644
index 000000000000..1c0d23ce95fa
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/spi/DatabaseOperationSmokeTest.java
@@ -0,0 +1,355 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.test.sql.exec.spi;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Timeout;
+import org.hibernate.ScrollMode;
+import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy;
+import org.hibernate.dialect.lock.spi.LockingSupport;
+import org.hibernate.engine.jdbc.spi.JdbcServices;
+import org.hibernate.engine.spi.LoadQueryInfluencers;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.internal.util.MutableObject;
+import org.hibernate.loader.ast.internal.LoaderSelectBuilder;
+import org.hibernate.metamodel.mapping.EntityMappingType;
+import org.hibernate.persister.entity.EntityPersister;
+import org.hibernate.query.spi.QueryOptions;
+import org.hibernate.sql.ast.tree.expression.JdbcParameter;
+import org.hibernate.sql.ast.tree.select.SelectStatement;
+import org.hibernate.sql.exec.internal.BaseExecutionContext;
+import org.hibernate.sql.exec.internal.CallbackImpl;
+import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl;
+import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl;
+import org.hibernate.sql.exec.internal.StandardStatementCreator;
+import org.hibernate.sql.exec.spi.Callback;
+import org.hibernate.sql.exec.spi.ExecutionContext;
+import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect;
+import org.hibernate.sql.exec.spi.JdbcParameterBindings;
+import org.hibernate.sql.exec.internal.DatabaseOperationSelectImpl;
+import org.hibernate.sql.exec.spi.PostAction;
+import org.hibernate.sql.exec.spi.PreAction;
+import org.hibernate.sql.exec.spi.StatementAccess;
+import org.hibernate.sql.results.spi.SingleResultConsumer;
+import org.hibernate.testing.orm.junit.DialectFeatureChecks;
+import org.hibernate.testing.orm.junit.DomainModel;
+import org.hibernate.testing.orm.junit.RequiresDialectFeature;
+import org.hibernate.testing.orm.junit.SessionFactory;
+import org.hibernate.testing.orm.junit.SessionFactoryScope;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/**
+ * @author Steve Ebersole
+ */
+@SuppressWarnings("JUnitMalformedDeclaration")
+@DomainModel(annotatedClasses = DatabaseOperationSmokeTest.Person.class)
+@SessionFactory
+public class DatabaseOperationSmokeTest {
+ @BeforeEach
+ void createTestData(SessionFactoryScope factoryScope) {
+ factoryScope.inTransaction( (session) -> {
+ session.persist( new Person( 1, "Steve ") );
+ } );
+ }
+
+ @AfterEach
+ void dropTestData(SessionFactoryScope factoryScope) {
+ factoryScope.dropData();
+ }
+
+ @Test
+ void testSimpleSelect(SessionFactoryScope factoryScope) {
+ final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory();
+ final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel().findEntityDescriptor( Person.class );
+
+ final PersonQuery personQuery = createPersonQuery( entityDescriptor, sessionFactory );
+ final JdbcOperationQuerySelect jdbcOperation = personQuery.jdbcOperation();
+ final JdbcParameterBindings jdbcParameterBindings = personQuery.jdbcParameterBindings();
+
+ final DatabaseOperationSelectImpl databaseOperation = new DatabaseOperationSelectImpl( jdbcOperation );
+
+ factoryScope.inTransaction( (session) -> {
+ final Person person = databaseOperation.execute(
+ Person.class,
+ 1,
+ StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ),
+ jdbcParameterBindings,
+ row -> (Person) row[0],
+ SingleResultConsumer.instance(),
+ new SingleIdExecutionContext(
+ session,
+ null,
+ 1,
+ entityDescriptor,
+ QueryOptions.NONE,
+ null
+ )
+ );
+ } );
+ }
+
+ @Test
+ @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsConnectionLockTimeouts.class)
+ void testConnectionLockTimeout(SessionFactoryScope factoryScope) {
+ final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory();
+
+ final LockingSupport lockingSupport = sessionFactory.getJdbcServices().getDialect().getLockingSupport();
+ final ConnectionLockTimeoutStrategy lockTimeoutStrategy = lockingSupport.getConnectionLockTimeoutStrategy();
+ assert lockTimeoutStrategy.getSupportedLevel() != ConnectionLockTimeoutStrategy.Level.NONE;
+
+ final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel().findEntityDescriptor( Person.class );
+
+ final PersonQuery personQuery = createPersonQuery( entityDescriptor, sessionFactory );
+ final JdbcOperationQuerySelect jdbcOperation = personQuery.jdbcOperation();
+ final JdbcParameterBindings jdbcParameterBindings = personQuery.jdbcParameterBindings();
+
+
+ final LockTimeoutHandler lockTimeoutHandler = new LockTimeoutHandler( Timeout.seconds( 2 ), lockTimeoutStrategy );
+
+ final DatabaseOperationSelectImpl databaseOperation = DatabaseOperationSelectImpl.builder( jdbcOperation )
+ .addSecondaryActionPair( lockTimeoutHandler )
+ .build();
+
+ factoryScope.inTransaction( (session) -> {
+ final Person person = databaseOperation.execute(
+ Person.class,
+ 1,
+ StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ),
+ jdbcParameterBindings,
+ row -> (Person) row[0],
+ SingleResultConsumer.instance(),
+ new SingleIdExecutionContext(
+ session,
+ null,
+ 1,
+ entityDescriptor,
+ QueryOptions.NONE,
+ null
+ )
+ );
+ } );
+ }
+
+ @Test
+ void testFollowOnLockingParadigm(SessionFactoryScope factoryScope) {
+ // NOTE: this just tests the principle -
+ // for now, just collect the values loaded.
+ // ultimately, this can be used to apply smarter follow-on locking.
+
+ final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory();
+ final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel().findEntityDescriptor( Person.class );
+
+ final PersonQuery personQuery = createPersonQuery( entityDescriptor, sessionFactory );
+ final JdbcOperationQuerySelect jdbcOperation = personQuery.jdbcOperation();
+ final JdbcParameterBindings jdbcParameterBindings = personQuery.jdbcParameterBindings();
+
+ factoryScope.inTransaction( (session) -> {
+ final LoadedValueCollector loadedValueCollector = new LoadedValueCollector();
+
+ final Callback callback = new CallbackImpl();
+ callback.registerAfterLoadAction( (entity, entityMappingType, session1) -> {
+ loadedValueCollector.loadedValues.add( entity );
+ } );
+
+ final SingleIdExecutionContext executionContext = new SingleIdExecutionContext(
+ session,
+ null,
+ 1,
+ entityDescriptor,
+ QueryOptions.NONE,
+ callback
+ );
+
+
+ final DatabaseOperationSelectImpl.Builder operationBuilder = DatabaseOperationSelectImpl
+ .builder( jdbcOperation )
+ .appendPostAction( loadedValueCollector );
+
+ final ConnectionLockTimeoutStrategy lockTimeoutStrategy = session
+ .getDialect()
+ .getLockingSupport()
+ .getConnectionLockTimeoutStrategy();
+ if ( lockTimeoutStrategy.getSupportedLevel() != ConnectionLockTimeoutStrategy.Level.NONE ) {
+ final LockTimeoutHandler lockTimeoutHandler = new LockTimeoutHandler( Timeout.seconds( 2 ), lockTimeoutStrategy );
+ operationBuilder.addSecondaryActionPair( lockTimeoutHandler, lockTimeoutHandler );
+ }
+
+ final DatabaseOperationSelectImpl databaseOperation = operationBuilder.build();
+ final Person person = databaseOperation.execute(
+ Person.class,
+ 1,
+ StandardStatementCreator.getStatementCreator( ScrollMode.FORWARD_ONLY ),
+ jdbcParameterBindings,
+ row -> (Person) row[0],
+ SingleResultConsumer.instance(),
+ executionContext
+ );
+
+ assertThat( loadedValueCollector.loadedValues ).hasSize( 1 );
+ } );
+
+ }
+
+ private static class LockTimeoutHandler implements PreAction, PostAction {
+ private final ConnectionLockTimeoutStrategy lockTimeoutStrategy;
+ private final Timeout timeout;
+ private Timeout baseline;
+
+ public LockTimeoutHandler(Timeout timeout, ConnectionLockTimeoutStrategy lockTimeoutStrategy) {
+ this.timeout = timeout;
+ this.lockTimeoutStrategy = lockTimeoutStrategy;
+ }
+
+ public Timeout getBaseline() {
+ return baseline;
+ }
+
+ @Override
+ public void performPreAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) {
+ final SessionFactoryImplementor factory = executionContext.getSession().getFactory();
+
+ // first, get the baseline (for post-action)
+ baseline = lockTimeoutStrategy.getLockTimeout( jdbcConnection, factory );
+
+ // now set the timeout
+ lockTimeoutStrategy.setLockTimeout( timeout, jdbcConnection, factory );
+ }
+
+ @Override
+ public void performPostAction(StatementAccess jdbcStatementAccess, Connection jdbcConnection, ExecutionContext executionContext) {
+ final SessionFactoryImplementor factory = executionContext.getSession().getFactory();
+
+ // reset the timeout
+ lockTimeoutStrategy.setLockTimeout( baseline, jdbcConnection, factory );
+ }
+ }
+
+ private static class LoadedValueCollector implements PostAction {
+ private final List