From 03bb8518e25de0314a8ce9d36c6985d1db942d00 Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Thu, 2 Nov 2023 12:20:32 -0500 Subject: [PATCH 1/2] [#1768] Add upsert support for non-PG DBs --- .../ReactiveOracleSqlAstTranslator.java | 101 +++++ .../ReactiveStandardDialectResolver.java | 40 +- ...veMutationExecutorSingleSelfExecuting.java | 40 +- .../ReactiveAbstractReturningDelegate.java | 10 +- ...eMergeCoordinatorStandardScopeFactory.java | 76 ++++ .../mutation/ReactiveMergeCoordinator.java | 8 + .../impl/ReactiveTypeContributor.java | 3 +- .../StandardReactiveJdbcMutationExecutor.java | 3 +- .../ReactiveDeleteOrUpsertOperation.java | 195 ++++++++++ .../ReactiveOptionalTableUpdateOperation.java | 350 ++++++++++++++++++ .../ReactiveSelfExecutingUpdateOperation.java | 24 ++ .../ReactiveDeferredResultSetAccess.java | 3 +- 12 files changed, 844 insertions(+), 9 deletions(-) create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/dialect/ReactiveOracleSqlAstTranslator.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveDeleteOrUpsertOperation.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveOptionalTableUpdateOperation.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveSelfExecutingUpdateOperation.java diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/dialect/ReactiveOracleSqlAstTranslator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/dialect/ReactiveOracleSqlAstTranslator.java new file mode 100644 index 000000000..04385f9f8 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/dialect/ReactiveOracleSqlAstTranslator.java @@ -0,0 +1,101 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.dialect; + +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DialectDelegateWrapper; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.dialect.OracleSqlAstTranslator; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.reactive.sql.model.ReactiveDeleteOrUpsertOperation; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.UpsertOperation; + +public class ReactiveOracleSqlAstTranslator extends OracleSqlAstTranslator { + public ReactiveOracleSqlAstTranslator( + SessionFactoryImplementor sessionFactory, + Statement statement) { + super( sessionFactory, statement ); + } + + @Override + public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableUpdate) { + renderUpsertStatement( optionalTableUpdate ); + + final UpsertOperation upsertOperation = new UpsertOperation( + optionalTableUpdate.getMutatingTable().getTableMapping(), + optionalTableUpdate.getMutationTarget(), + getSql(), + getParameterBinders() + ); + + return new ReactiveDeleteOrUpsertOperation( + optionalTableUpdate.getMutationTarget(), + (EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(), + upsertOperation, + optionalTableUpdate + ); + } + + public MutationOperation createReactiveMergeOperation(OptionalTableUpdate optionalTableUpdate) { + renderUpsertStatement( optionalTableUpdate ); + + final UpsertOperation upsertOperation = new UpsertOperation( + optionalTableUpdate.getMutatingTable().getTableMapping(), + optionalTableUpdate.getMutationTarget(), + getSql(), + getParameterBinders() + ); + + return new ReactiveDeleteOrUpsertOperation( + optionalTableUpdate.getMutationTarget(), + (EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(), + upsertOperation, + optionalTableUpdate + ); + } + + private void renderUpsertStatement(OptionalTableUpdate optionalTableUpdate) { + // template: + // + // merge into [table] as t + // using values([bindings]) as s ([column-names]) + // on t.[key] = s.[key] + // when not matched + // then insert ... + // when matched + // then update ... + + renderMergeInto( optionalTableUpdate ); + appendSql( " " ); + renderMergeUsing( optionalTableUpdate ); + appendSql( " " ); + renderMergeOn( optionalTableUpdate ); + appendSql( " " ); + renderMergeInsert( optionalTableUpdate ); + appendSql( " " ); + renderMergeUpdate( optionalTableUpdate ); + } + + @Override + protected boolean rendersTableReferenceAlias(Clause clause) { + // todo (6.0) : For now we just skip the alias rendering in the delete and update clauses + // We need some dialect support if we want to support joins in delete and update statements + switch ( clause ) { + case DELETE: + case UPDATE: { + final Dialect realDialect = DialectDelegateWrapper.extractRealDialect( getDialect() ); + return realDialect.getDmlTargetColumnQualifierSupport() == DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + } + return true; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java index 1b24e567d..eb295cc09 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/dialect/internal/ReactiveStandardDialectResolver.java @@ -8,8 +8,19 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.Database; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DialectDelegateWrapper; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.reactive.dialect.ReactiveOracleSqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; import static org.hibernate.dialect.CockroachDialect.parseVersion; @@ -25,7 +36,34 @@ public Dialect resolveDialect(DialectResolutionInfo info) { for ( Database database : Database.values() ) { if ( database.matchesResolutionInfo( info ) ) { - return database.createDialect( info ); + Dialect dialect = database.createDialect( info ); + if ( info.getDatabaseName().toUpperCase().startsWith( "ORACLE" ) ) { + return new DialectDelegateWrapper( dialect ) { + @Override + public MutationOperation createOptionalTableUpdateOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + final ReactiveOracleSqlAstTranslator translator = new ReactiveOracleSqlAstTranslator<>( + factory, + optionalTableUpdate + ); + return translator.createReactiveMergeOperation( optionalTableUpdate ); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new ReactiveOracleSqlAstTranslator<>( sessionFactory, statement ); + } + }; + } + }; + } + return dialect; } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleSelfExecuting.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleSelfExecuting.java index ea39d8a6e..66c232d34 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleSelfExecuting.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleSelfExecuting.java @@ -5,15 +5,53 @@ */ package org.hibernate.reactive.engine.jdbc.mutation.internal; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.jdbc.mutation.TableInclusionChecker; import org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleSelfExecuting; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.reactive.engine.jdbc.env.internal.ReactiveMutationExecutor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.sql.model.ReactiveSelfExecutingUpdateOperation; import org.hibernate.sql.model.SelfExecutingUpdateOperation; +import org.hibernate.sql.model.ValuesAnalysis; + +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; public class ReactiveMutationExecutorSingleSelfExecuting extends MutationExecutorSingleSelfExecuting implements ReactiveMutationExecutor { - public ReactiveMutationExecutorSingleSelfExecuting(SelfExecutingUpdateOperation operation, SharedSessionContractImplementor session) { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private final SelfExecutingUpdateOperation operation; + + public ReactiveMutationExecutorSingleSelfExecuting( + SelfExecutingUpdateOperation operation, + SharedSessionContractImplementor session) { super( operation, session ); + this.operation = operation; + } + + @Override + protected void performSelfExecutingOperations( + ValuesAnalysis valuesAnalysis, + TableInclusionChecker inclusionChecker, + SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "performReactiveSelfExecutingOperation" ); + } + + @Override + public CompletionStage performReactiveSelfExecutingOperations( + ValuesAnalysis valuesAnalysis, + TableInclusionChecker inclusionChecker, + SharedSessionContractImplementor session) { + if ( inclusionChecker.include( operation.getTableDetails() ) && operation instanceof ReactiveSelfExecutingUpdateOperation ) { + return ( (ReactiveSelfExecutingUpdateOperation) operation ) + .performReactiveMutation( getJdbcValueBindings(), valuesAnalysis, session ); + } + return voidFuture(); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java index ba0677a7e..e2794b218 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java @@ -11,6 +11,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DialectDelegateWrapper; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SQLServerDialect; @@ -69,7 +70,8 @@ && getPersister().getFactory().getJdbcServices().getDialect() instanceof Cockroa private static String createInsert(PreparedStatementDetails insertStatementDetails, String identifierColumnName, Dialect dialect) { final String sqlEnd = " returning " + identifierColumnName; - if ( dialect instanceof MySQLDialect ) { + Dialect realDialect = DialectDelegateWrapper.extractRealDialect( dialect ); + if ( realDialect instanceof MySQLDialect ) { // For some reasons ORM generates a query with an invalid syntax String sql = insertStatementDetails.getSqlString(); int index = sql.lastIndexOf( sqlEnd ); @@ -77,7 +79,7 @@ private static String createInsert(PreparedStatementDetails insertStatementDetai ? sql.substring( 0, index ) : sql; } - if ( dialect instanceof SQLServerDialect ) { + if ( realDialect instanceof SQLServerDialect ) { String sql = insertStatementDetails.getSqlString(); int index = sql.lastIndexOf( sqlEnd ); // FIXME: this is a hack for HHH-16365 @@ -94,12 +96,12 @@ private static String createInsert(PreparedStatementDetails insertStatementDetai } return sql; } - if ( dialect instanceof DB2Dialect ) { + if ( realDialect instanceof DB2Dialect ) { // ORM query: select id from new table ( insert into IntegerTypeEntity values ( )) // Correct : select id from new table ( insert into LongTypeEntity (id) values (default)) return insertStatementDetails.getSqlString().replace( " values ( ))", " (" + identifierColumnName + ") values (default))" ); } - if ( dialect instanceof OracleDialect ) { + if ( realDialect instanceof OracleDialect ) { final String valuesStr = " values ( )"; String sql = insertStatementDetails.getSqlString(); int index = sql.lastIndexOf( sqlEnd ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java index 290e0efc8..4ee5164c2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java @@ -5,12 +5,25 @@ */ package org.hibernate.reactive.persister.entity.impl; + +import org.hibernate.dialect.OracleDialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.mutation.MergeCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveMergeCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveScopedUpdateCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinator; +import org.hibernate.reactive.sql.model.ReactiveOptionalTableUpdateOperation; +import org.hibernate.sql.model.ModelMutationLogging; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.ValuesAnalysis; +import org.hibernate.sql.model.ast.MutationGroup; +import org.hibernate.sql.model.ast.TableMutation; +import org.hibernate.sql.model.internal.MutationOperationGroupFactory; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.DeleteOrUpsertOperation; +import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; public class ReactiveMergeCoordinatorStandardScopeFactory extends MergeCoordinator implements ReactiveUpdateCoordinator { @@ -30,4 +43,67 @@ public ReactiveScopedUpdateCoordinator makeScopedCoordinator() { this.getVersionUpdateBatchkey() ); } + + @Override + protected MutationOperationGroup createOperationGroup(ValuesAnalysis valuesAnalysis, MutationGroup mutationGroup) { + final int numberOfTableMutations = mutationGroup.getNumberOfTableMutations(); + switch ( numberOfTableMutations ) { + case 0: + return MutationOperationGroupFactory.noOperations( mutationGroup ); + case 1: { + MutationOperation operation = createOperation( valuesAnalysis, mutationGroup.getSingleTableMutation() ); + return operation == null + ? MutationOperationGroupFactory.noOperations( mutationGroup ) + : MutationOperationGroupFactory.singleOperation( mutationGroup, operation ); + } + default: { + MutationOperation[] operations = new MutationOperation[numberOfTableMutations]; + int outputIndex = 0; + int skipped = 0; + for ( int i = 0; i < mutationGroup.getNumberOfTableMutations(); i++ ) { + final TableMutation tableMutation = mutationGroup.getTableMutation( i ); + MutationOperation operation = createOperation( valuesAnalysis, tableMutation ); + if ( operation != null ) { + operations[outputIndex++] = operation; + } + else { + skipped++; + ModelMutationLogging.MODEL_MUTATION_LOGGER.debugf( + "Skipping table update - %s", + tableMutation.getTableName() + ); + } + } + if ( skipped != 0 ) { + final MutationOperation[] trimmed = new MutationOperation[outputIndex]; + System.arraycopy( operations, 0, trimmed, 0, outputIndex ); + operations = trimmed; + } + return MutationOperationGroupFactory.manyOperations( mutationGroup.getMutationType(), entityPersister, operations ); + } + } + } + + // FIXME: We could add this method in ORM and override only this code + protected MutationOperation createOperation(ValuesAnalysis valuesAnalysis, TableMutation singleTableMutation) { + MutationOperation operation = singleTableMutation.createMutationOperation( valuesAnalysis, factory() ); + if ( operation instanceof OptionalTableUpdateOperation ) { + // We need to plug in our own reactive operation + return new ReactiveOptionalTableUpdateOperation( + operation.getMutationTarget(), + (OptionalTableUpdate) singleTableMutation, + factory() + ); + } + else if ( operation instanceof DeleteOrUpsertOperation && + factory().getJdbcServices().getDialect() instanceof OracleDialect ) { + OracleDialect dialect = ((OracleDialect)factory().getJdbcServices().getDialect()); + return dialect.createOptionalTableUpdateOperation( + ( (OptionalTableUpdate)operation).getMutationTarget(), + (OptionalTableUpdate) operation, + factory() + ); + } + return operation; + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java index 815842ea8..7371e4de3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java @@ -11,6 +11,8 @@ import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.ValuesAnalysis; +import org.hibernate.sql.model.ast.MutationGroup; import org.hibernate.sql.model.ast.builder.AbstractTableUpdateBuilder; import org.hibernate.sql.model.ast.builder.TableMergeBuilder; @@ -33,4 +35,10 @@ public ReactiveMergeCoordinator( protected AbstractTableUpdateBuilder newTableUpdateBuilder(EntityTableMapping tableMapping) { return new TableMergeBuilder<>( entityPersister(), tableMapping, factory() ); } + + @Override + protected MutationOperationGroup createOperationGroup(ValuesAnalysis valuesAnalysis, MutationGroup mutationGroup) { + return super.createOperationGroup( valuesAnalysis, mutationGroup ); + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveTypeContributor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveTypeContributor.java index 7a111e43b..0724718c9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveTypeContributor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveTypeContributor.java @@ -21,6 +21,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DialectDelegateWrapper; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; @@ -99,7 +100,7 @@ private void registerReactiveChanges(TypeContributions typeContributions, Servic } private Dialect dialect(ServiceRegistry serviceRegistry) { - return serviceRegistry.getService( JdbcEnvironment.class ).getDialect(); + return DialectDelegateWrapper.extractRealDialect( serviceRegistry.getService( JdbcEnvironment.class ).getDialect() ); } /** diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveJdbcMutationExecutor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveJdbcMutationExecutor.java index de81e9891..ea272765a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveJdbcMutationExecutor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveJdbcMutationExecutor.java @@ -13,6 +13,7 @@ import java.util.function.Function; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DialectDelegateWrapper; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.query.spi.QueryOptions; @@ -127,7 +128,7 @@ private static String finalSql( .getSessionFactoryOptions() .isCommentsEnabled() ); - final Dialect dialect = executionContext.getSession().getJdbcServices().getDialect(); + final Dialect dialect = DialectDelegateWrapper.extractRealDialect( executionContext.getSession().getJdbcServices().getDialect() ); return Parameters.instance( dialect ).process( sql ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveDeleteOrUpsertOperation.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveDeleteOrUpsertOperation.java new file mode 100644 index 000000000..f6452578f --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveDeleteOrUpsertOperation.java @@ -0,0 +1,195 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.model; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collections; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.jdbc.mutation.internal.MutationQueryOptions; +import org.hibernate.engine.jdbc.mutation.internal.PreparedStatementGroupSingleTable; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.persister.entity.mutation.UpdateValuesAnalysis; +import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.pool.ReactiveConnection; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.model.TableMapping; +import org.hibernate.sql.model.ValuesAnalysis; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.internal.TableDeleteStandard; +import org.hibernate.sql.model.jdbc.DeleteOrUpsertOperation; +import org.hibernate.sql.model.jdbc.JdbcDeleteMutation; +import org.hibernate.sql.model.jdbc.UpsertOperation; + +import org.jboss.logging.Logger; + +import static java.lang.invoke.MethodHandles.lookup; +import static org.hibernate.reactive.logging.impl.LoggerFactory.make; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; + +public class ReactiveDeleteOrUpsertOperation extends DeleteOrUpsertOperation implements ReactiveSelfExecutingUpdateOperation { + private static final Log LOG = make( Log.class, lookup() ); + private final OptionalTableUpdate upsert; + private final UpsertOperation upsertOperation; + private final UpsertStatementInfo upsertStatementInfo; + public ReactiveDeleteOrUpsertOperation( + EntityMutationTarget mutationTarget, + EntityTableMapping tableMapping, + UpsertOperation upsertOperation, + OptionalTableUpdate optionalTableUpdate) { + super( mutationTarget, tableMapping, upsertOperation, optionalTableUpdate ); + this.upsert = optionalTableUpdate; + this.upsertOperation = upsertOperation; + this.upsertStatementInfo = new UpsertStatementInfo( ); + } + + @Override + public void performMutation( + JdbcValueBindings jdbcValueBindings, + ValuesAnalysis valuesAnalysis, + SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "performReactiveMutation" ); + } + + @Override + public CompletionStage performReactiveMutation( + JdbcValueBindings jdbcValueBindings, + ValuesAnalysis incomingValuesAnalysis, + SharedSessionContractImplementor session) { + final UpdateValuesAnalysis valuesAnalysis = (UpdateValuesAnalysis) incomingValuesAnalysis; + if ( !valuesAnalysis.getTablesNeedingUpdate().contains( getTableDetails() ) ) { + return voidFuture(); + } + + return doReactiveMutation( getTableDetails(), jdbcValueBindings, valuesAnalysis, session ); + } + + /** + * + * @see DeleteOrUpsertOperation#performMutation(JdbcValueBindings, ValuesAnalysis, SharedSessionContractImplementor) + * @param tableMapping + * @param jdbcValueBindings + * @param valuesAnalysis + * @param session + * @return + */ + private CompletionStage doReactiveMutation( + TableMapping tableMapping, + JdbcValueBindings jdbcValueBindings, + UpdateValuesAnalysis valuesAnalysis, + SharedSessionContractImplementor session) { + + return voidFuture() + .thenCompose( v -> { + if ( !valuesAnalysis.getTablesWithNonNullValues().contains( tableMapping ) ) { + return performReactiveDelete( jdbcValueBindings, session ); + } + else { + return performReactiveUpsert( jdbcValueBindings,session ); + } + } ) + .whenComplete( (o, throwable) -> jdbcValueBindings.afterStatement( tableMapping ) ); + } + + private CompletionStage performReactiveUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { + final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertOperation, session ); + final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( getTableDetails().getTableName() ); + upsertStatementInfo.setStatementDetails( statementDetails ); + + // If we get here the statement is needed - make sure it is resolved + Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( + statementDetails, + statement, + session.getJdbcServices() + ); + jdbcValueBindings.beforeStatement( details ); + } ); + + ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + String sqlString = statementDetails.getSqlString(); + return reactiveConnection + .update( sqlString, params ).thenCompose(this::checkUpsertResults); + } + + private CompletionStage checkUpsertResults( Integer rowCount ) { + if ( rowCount > 0 ) { + try { + upsert.getExpectation() + .verifyOutcome( + rowCount, + upsertStatementInfo.getStatementDetails(), + -1, + upsertStatementInfo.getStatementSqlString() + ); + return voidFuture(); + } + catch (SQLException e) { + LOG.log( Logger.Level.ERROR, e ); + } + } + return voidFuture(); + } + + private CompletionStage performReactiveDelete(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { + MODEL_MUTATION_LOGGER.tracef( "#performDelete(%s)", getTableDetails().getTableName() ); + + final TableDeleteStandard upsertDeleteAst = new TableDeleteStandard( + upsert.getMutatingTable(), + getMutationTarget(), + "upsert delete", + upsert.getKeyBindings(), + Collections.emptyList(), + Collections.emptyList() + ); + + final SqlAstTranslator translator = session + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildModelMutationTranslator( upsertDeleteAst, session.getFactory() ); + final JdbcDeleteMutation upsertDelete = translator.translate( null, MutationQueryOptions.INSTANCE ); + + final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertDelete, session ); + final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( getTableDetails().getTableName() ); + Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, session.getJdbcServices() ); + jdbcValueBindings.beforeStatement( details ); + } ); + session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + + ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + String sqlString = statementDetails.getSqlString(); + return reactiveConnection + .update( sqlString, params ).thenCompose(this::checkUpsertResults); + } + + static class UpsertStatementInfo { + + PreparedStatementDetails statementDetails; + + public void setStatementDetails(PreparedStatementDetails statementDetails) { + this.statementDetails = statementDetails; + } + + public PreparedStatement getStatementDetails() { + return statementDetails.getStatement(); + } + + public String getStatementSqlString() { + return statementDetails.getSqlString(); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveOptionalTableUpdateOperation.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveOptionalTableUpdateOperation.java new file mode 100644 index 000000000..a13e5fde3 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveOptionalTableUpdateOperation.java @@ -0,0 +1,350 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.model; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.jdbc.mutation.internal.MutationQueryOptions; +import org.hibernate.engine.jdbc.mutation.internal.PreparedStatementGroupSingleTable; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.persister.entity.mutation.UpdateValuesAnalysis; +import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.pool.ReactiveConnection; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.reactive.util.impl.CompletionStages; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.model.MutationTarget; +import org.hibernate.sql.model.TableMapping; +import org.hibernate.sql.model.ValuesAnalysis; +import org.hibernate.sql.model.ast.MutatingTableReference; +import org.hibernate.sql.model.ast.TableDelete; +import org.hibernate.sql.model.ast.TableInsert; +import org.hibernate.sql.model.ast.TableUpdate; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.internal.TableDeleteCustomSql; +import org.hibernate.sql.model.internal.TableDeleteStandard; +import org.hibernate.sql.model.internal.TableInsertCustomSql; +import org.hibernate.sql.model.internal.TableInsertStandard; +import org.hibernate.sql.model.internal.TableUpdateCustomSql; +import org.hibernate.sql.model.internal.TableUpdateStandard; +import org.hibernate.sql.model.jdbc.JdbcDeleteMutation; +import org.hibernate.sql.model.jdbc.JdbcInsertMutation; +import org.hibernate.sql.model.jdbc.JdbcMutationOperation; +import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; + +import static java.lang.invoke.MethodHandles.lookup; +import static org.hibernate.reactive.logging.impl.LoggerFactory.make; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; + +public class ReactiveOptionalTableUpdateOperation extends OptionalTableUpdateOperation + implements ReactiveSelfExecutingUpdateOperation { + private static final Log LOG = make( Log.class, lookup() ); + private final OptionalTableUpdate upsert; + + public ReactiveOptionalTableUpdateOperation( + MutationTarget mutationTarget, + OptionalTableUpdate upsert, + SessionFactoryImplementor factory) { + super( mutationTarget, upsert, factory ); + this.upsert = upsert; + } + + @Override + public void performMutation( + JdbcValueBindings jdbcValueBindings, + ValuesAnalysis valuesAnalysis, + SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "performReactiveMutation" ); + } + + @Override + public CompletionStage performReactiveMutation( + JdbcValueBindings jdbcValueBindings, + ValuesAnalysis incomingValuesAnalysis, + SharedSessionContractImplementor session) { + final UpdateValuesAnalysis valuesAnalysis = (UpdateValuesAnalysis) incomingValuesAnalysis; + if ( !valuesAnalysis.getTablesNeedingUpdate().contains( getTableDetails() ) ) { + return voidFuture(); + } + + return doReactiveMutation( getTableDetails(), jdbcValueBindings, valuesAnalysis, session ); + } + + /** + * + * @see OptionalTableUpdateOperation + * @param tableMapping + * @param jdbcValueBindings + * @param valuesAnalysis + * @param session + * @return + */ + private CompletionStage doReactiveMutation( + TableMapping tableMapping, + JdbcValueBindings jdbcValueBindings, + UpdateValuesAnalysis valuesAnalysis, + SharedSessionContractImplementor session) { + + return voidFuture() + .thenCompose( v -> { + if ( shouldDelete( valuesAnalysis, tableMapping ) ) { + return performReactiveDelete( jdbcValueBindings, session ); + } + else { + return performReactiveUpdate( jdbcValueBindings, session ) + .thenCompose( wasUpdated -> { + if ( !wasUpdated ) { + MODEL_MUTATION_LOGGER.debugf( "Upsert update altered no rows - inserting : %s", tableMapping.getTableName() ); + return performReactiveInsert( jdbcValueBindings, session ); + } + return voidFuture(); + } ); + } + } ) + .whenComplete( (o, throwable) -> jdbcValueBindings.afterStatement( tableMapping ) ); + } + + private boolean shouldDelete(UpdateValuesAnalysis valuesAnalysis, TableMapping tableMapping) { + return !valuesAnalysis.getTablesWithNonNullValues().contains( tableMapping ) + // all the new values for this table were null - possibly delete the row + && valuesAnalysis.getTablesWithPreviousNonNullValues().contains( tableMapping ); + } + + /** + * @see org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation#performDelete(JdbcValueBindings, SharedSessionContractImplementor) + * @param jdbcValueBindings + * @param session + */ + private CompletionStage performReactiveDelete( + JdbcValueBindings jdbcValueBindings, + SharedSessionContractImplementor session) { + final JdbcDeleteMutation jdbcDelete = createJdbcDelete( session ); + + final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( + jdbcDelete, + session + ); + final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( + getTableDetails().getTableName() ); + + session.getJdbcServices().getSqlStatementLogger().logStatement( jdbcDelete.getSqlString() ); + + Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( + statementDetails, + statement, + session.getJdbcServices() + ); + jdbcValueBindings.beforeStatement( details ); + } ); + + ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + return reactiveConnection.update( statementDetails.getSqlString(), params ).thenCompose( CompletionStages::voidFuture); + } + + /** + * @see org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation#performUpdate(JdbcValueBindings, SharedSessionContractImplementor) + * @param jdbcValueBindings + * @param session + * @return + */ + private CompletionStage performReactiveUpdate( + JdbcValueBindings jdbcValueBindings, + SharedSessionContractImplementor session) { + MODEL_MUTATION_LOGGER.tracef( "#performUpdate(%s)", getTableDetails().getTableName() ); + + final TableUpdate tableUpdate = createJdbcUpdate(); + + final SqlAstTranslator translator = session + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildModelMutationTranslator( tableUpdate, session.getFactory() ); + + final JdbcMutationOperation jdbcUpdate = translator.translate( null, MutationQueryOptions.INSTANCE ); + final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( + jdbcUpdate, + session + ); + + final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( + getTableDetails().getTableName() ); + // If we get here the statement is needed - make sure it is resolved + Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( + statementDetails, + statement, + session.getJdbcServices() + ); + jdbcValueBindings.beforeStatement( details ); + } ); + + session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + + ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + String sqlString = statementDetails.getSqlString(); + return reactiveConnection + .update( sqlString, params ) + .thenApply( rowCount -> { + if ( rowCount == 0 ) { + return false; + } + try { + upsert.getExpectation() + .verifyOutcome( + rowCount, + statementDetails.getStatement(), + -1, + statementDetails.getSqlString() + ); + return true; + } + catch (SQLException e) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + e, + "Unable to execute mutation PreparedStatement against table `" + getTableDetails().getTableName() + "`", + statementDetails.getSqlString() + ); + } + } ); + } + + private TableUpdate createJdbcUpdate() { + MutationTarget mutationTarget = super.getMutationTarget(); + if ( getTableDetails().getUpdateDetails() != null + && getTableDetails().getUpdateDetails().getCustomSql() != null ) { + return new TableUpdateCustomSql( + new MutatingTableReference( getTableDetails() ), + mutationTarget, + "upsert update for " + mutationTarget.getRolePath(), + upsert.getValueBindings(), + upsert.getKeyBindings(), + upsert.getOptimisticLockBindings(), + upsert.getParameters() + ); + } + + return new TableUpdateStandard( + new MutatingTableReference( getTableDetails() ), + mutationTarget, + "upsert update for " + mutationTarget.getRolePath(), + upsert.getValueBindings(), + upsert.getKeyBindings(), + upsert.getOptimisticLockBindings(), + upsert.getParameters() + ); + } + + private CompletionStage performReactiveInsert( + JdbcValueBindings jdbcValueBindings, + SharedSessionContractImplementor session) { + final JdbcInsertMutation jdbcInsert = createJdbcInsert( session ); + + final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( jdbcInsert, session ); + final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( getTableDetails().getTableName() ); + // If we get here the statement is needed - make sure it is resolved + Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, session.getJdbcServices() ); + jdbcValueBindings.beforeStatement( details ); + } ); + + ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + return reactiveConnection.update( statementDetails.getSqlString(), params ).thenCompose(CompletionStages::voidFuture); + } + + /** + * @see org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation#createJdbcInsert(SharedSessionContractImplementor) + */ + // Temporary copy of the createJdbcInsert() in ORM + // FIXME: change visibility to protected in ORM and remove this method + private JdbcInsertMutation createJdbcInsert(SharedSessionContractImplementor session) { + final TableInsert tableInsert; + if ( getTableDetails().getInsertDetails() != null + && getTableDetails().getInsertDetails().getCustomSql() != null ) { + tableInsert = new TableInsertCustomSql( + new MutatingTableReference( getTableDetails() ), + getMutationTarget(), + CollectionHelper.combine( upsert.getValueBindings(), upsert.getKeyBindings() ), + upsert.getParameters() + ); + } + else { + tableInsert = new TableInsertStandard( + new MutatingTableReference( getTableDetails() ), + getMutationTarget(), + CollectionHelper.combine( upsert.getValueBindings(), upsert.getKeyBindings() ), + Collections.emptyList(), + upsert.getParameters() + ); + } + + final SessionFactoryImplementor factory = session.getSessionFactory(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = factory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory(); + + final SqlAstTranslator translator = sqlAstTranslatorFactory.buildModelMutationTranslator( + tableInsert, + factory + ); + + return translator.translate( null, MutationQueryOptions.INSTANCE ); + } + + /** + * @see org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation#createJdbcInsert(SharedSessionContractImplementor) + */ + // Temporary copy of the createJdbcInsert() in ORM + // FIXME: change visibility to protected in ORM and remove this method + private JdbcDeleteMutation createJdbcDelete(SharedSessionContractImplementor session) { + final TableDelete tableDelete; + if ( getTableDetails().getDeleteDetails() != null + && getTableDetails().getDeleteDetails().getCustomSql() != null ) { + tableDelete = new TableDeleteCustomSql( + new MutatingTableReference( getTableDetails() ), + getMutationTarget(), + "upsert delete for " + upsert.getMutationTarget().getRolePath(), + upsert.getKeyBindings(), + upsert.getOptimisticLockBindings(), + upsert.getParameters() + ); + } + else { + tableDelete = new TableDeleteStandard( + new MutatingTableReference( getTableDetails() ), + getMutationTarget(), + "upsert delete for " + getMutationTarget().getRolePath(), + upsert.getKeyBindings(), + upsert.getOptimisticLockBindings(), + upsert.getParameters() + ); + } + + final SessionFactoryImplementor factory = session.getSessionFactory(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = factory + .getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory(); + + final SqlAstTranslator translator = sqlAstTranslatorFactory.buildModelMutationTranslator( + tableDelete, + factory + ); + + return translator.translate( null, MutationQueryOptions.INSTANCE ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveSelfExecutingUpdateOperation.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveSelfExecutingUpdateOperation.java new file mode 100644 index 000000000..98bb99a0d --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/model/ReactiveSelfExecutingUpdateOperation.java @@ -0,0 +1,24 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.model; + +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.sql.model.SelfExecutingUpdateOperation; +import org.hibernate.sql.model.ValuesAnalysis; + +/** + * @see org.hibernate.sql.model.SelfExecutingUpdateOperation + */ +public interface ReactiveSelfExecutingUpdateOperation extends SelfExecutingUpdateOperation { + + CompletionStage performReactiveMutation( + JdbcValueBindings jdbcValueBindings, + ValuesAnalysis valuesAnalysis, + SharedSessionContractImplementor session); +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java index c116abb10..1eeff8fa8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java @@ -15,6 +15,7 @@ import org.hibernate.HibernateException; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DialectDelegateWrapper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionEventListenerManager; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -150,7 +151,7 @@ private CompletionStage executeQuery() { .thenCompose( lg -> { LOG.tracef( "Executing query to retrieve ResultSet : %s", getFinalSql() ); - Dialect dialect = executionContext.getSession().getJdbcServices().getDialect(); + Dialect dialect = DialectDelegateWrapper.extractRealDialect( executionContext.getSession().getJdbcServices().getDialect() ); // I'm not sure calling Parameters here is necessary, the query should already have the right parameters final String sql = Parameters.instance( dialect ).process( getFinalSql() ); Object[] parameters = PreparedStatementAdaptor.bind( super::bindParameters ); From e76cc18b0a3ec8090bfeb3f0407236a3d18e2bf7 Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Thu, 2 Nov 2023 12:20:35 -0500 Subject: [PATCH 2/2] [#1768] verify correct upsert sql queries for each DB --- .../org/hibernate/reactive/UpsertTest.java | 93 ++++++++++++++----- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java index 3ba93cbac..77448a564 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java @@ -9,10 +9,12 @@ import java.util.List; import java.util.Objects; -import org.hibernate.reactive.testing.DBSelectionExtension; + +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.reactive.testing.SqlStatementTracker; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; @@ -22,12 +24,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.COCKROACHDB; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; -import static org.hibernate.reactive.testing.DBSelectionExtension.skipTestsFor; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; /** * Same as Hibernate ORM org.hibernate.orm.test.stateless.UpsertTest @@ -38,12 +35,29 @@ */ @Timeout(value = 10, timeUnit = MINUTES) public class UpsertTest extends BaseReactiveTest { + private static SqlStatementTracker sqlTracker; - /** - * Something is missing in HR to make it work for these databases. - */ - @RegisterExtension - public DBSelectionExtension dbSelection = skipTestsFor( COCKROACHDB, DB2, MARIA, MYSQL, ORACLE ); + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + sqlTracker = new SqlStatementTracker( UpsertTest::filter, configuration.getProperties() ); + return configuration; + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean filter(String s) { + String[] accepted = { "insert ", "update ", "merge " }; + for ( String valid : accepted ) { + if ( s.toLowerCase().startsWith( valid ) ) { + return true; + } + } + return false; + } @Override protected Collection> annotatedEntities() { @@ -55,19 +69,22 @@ public void testMutinyUpsert(VertxTestContext context) { test( context, getMutinySessionFactory().withStatelessTransaction( ss -> ss .upsert( new Record( 123L, "hello earth" ) ) .call( () -> ss.upsert( new Record( 456L, "hello mars" ) ) ) + .invoke( v -> assertThat( verifySqlForDB() ).isTrue() ) ) .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) - .invoke( results -> assertThat( results ).containsExactly( - new Record( 123L, "hello earth" ), - new Record( 456L, "hello mars" ) - ) ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .invoke( results -> { + assertThat( results ).containsExactly( + new Record( 123L, "hello earth" ), + new Record( 456L, "hello mars" ) + ); + } ) ) .call( () -> getMutinySessionFactory().withStatelessTransaction( ss -> ss .upsert( new Record( 123L, "goodbye earth" ) ) ) ) .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .invoke( results -> assertThat( results ).containsExactly( new Record( 123L, "goodbye earth" ), new Record( 456L, "hello mars" ) @@ -81,9 +98,10 @@ public void testMutinyUpsertWithEntityName(VertxTestContext context) { test( context, getMutinySessionFactory().withStatelessTransaction( ss -> ss .upsert( Record.class.getName(), new Record( 123L, "hello earth" ) ) .call( () -> ss.upsert( Record.class.getName(), new Record( 456L, "hello mars" ) ) ) + .invoke( v -> assertThat( verifySqlForDB() ).isTrue() ) ) .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .invoke( results -> assertThat( results ).containsExactly( new Record( 123L, "hello earth" ), new Record( 456L, "hello mars" ) @@ -93,7 +111,7 @@ public void testMutinyUpsertWithEntityName(VertxTestContext context) { .upsert( Record.class.getName(), new Record( 123L, "goodbye earth" ) ) ) ) .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .invoke( results -> assertThat( results ).containsExactly( new Record( 123L, "goodbye earth" ), new Record( 456L, "hello mars" ) @@ -107,9 +125,10 @@ public void testStageUpsert(VertxTestContext context) { test( context, getSessionFactory().withStatelessTransaction( ss -> ss .upsert( new Record( 123L, "hello earth" ) ) .thenCompose( v -> ss.upsert( new Record( 456L, "hello mars" ) ) ) + .thenApply( v -> assertThat( verifySqlForDB() ).isTrue() ) ) .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .thenAccept( results -> assertThat( results ).containsExactly( new Record( 123L, "hello earth" ), new Record( 456L, "hello mars" ) @@ -119,7 +138,7 @@ public void testStageUpsert(VertxTestContext context) { .upsert( new Record( 123L, "goodbye earth" ) ) ) ) .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .thenAccept( results -> assertThat( results ).containsExactly( new Record( 123L, "goodbye earth" ), new Record( 456L, "hello mars" ) @@ -133,9 +152,10 @@ public void testStageUpsertWithEntityName(VertxTestContext context) { test( context, getSessionFactory().withStatelessTransaction( ss -> ss .upsert( Record.class.getName(), new Record( 123L, "hello earth" ) ) .thenCompose( v -> ss.upsert( Record.class.getName(), new Record( 456L, "hello mars" ) ) ) + .thenApply( v -> assertThat( verifySqlForDB() ).isTrue() ) ) .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .thenAccept( results -> assertThat( results ).containsExactly( new Record( 123L, "hello earth" ), new Record( 456L, "hello mars" ) @@ -145,7 +165,7 @@ public void testStageUpsertWithEntityName(VertxTestContext context) { .upsert( Record.class.getName(), new Record( 123L, "goodbye earth" ) ) ) ) .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss - .createSelectionQuery( "from Record order by id", Record.class ).getResultList() ) + .createQuery( "from Record order by id", Record.class ).getResultList() ) .thenAccept( results -> assertThat( results ).containsExactly( new Record( 123L, "goodbye earth" ), new Record( 456L, "hello mars" ) @@ -154,6 +174,29 @@ public void testStageUpsertWithEntityName(VertxTestContext context) { ); } + + + private boolean verifySqlForDB() { + String DB_NAME = dbType().getDialectClass().getName().toUpperCase(); + if ( DB_NAME.contains( "DB2" ) || DB_NAME.contains( "MARIA" ) || DB_NAME.contains( "MYSQL" ) || DB_NAME.contains( "COCKROACH" ) ) { + return foundQuery( "update" ) && foundQuery( "insert" ); + } + if ( DB_NAME.contains( "SQLSERVER" ) || DB_NAME.contains( "MSSQL" ) || DB_NAME.contains( "POSTGRES" ) || DB_NAME.contains( + "ORACLE" ) ) { + return foundQuery( "merge" ); + } + return false; + } + + private boolean foundQuery( String startsWithFragment ) { + for ( int i = 0; i < sqlTracker.getLoggedQueries().size(); i++ ) { + if ( sqlTracker.getLoggedQueries().get( i ).startsWith( startsWithFragment ) ) { + return true; + } + } + return false; + } + @Entity(name = "Record") @Table(name = "Record") public static class Record {