diff --git a/gradle.properties b/gradle.properties index e27179a12..44dfb522f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -29,7 +29,7 @@ org.gradle.java.installations.auto-download=false #db = MSSQL # Enable the SonatypeOS maven repository (mainly for Vert.x snapshots) when present (value ignored) -#enableSonatypeOpenSourceSnapshotsRep = true +enableSonatypeOpenSourceSnapshotsRep = true # Enable the JBoss Snapshot maven repository (mainly for Hibernate ORM snapshots) when present (value ignored) #enableJBossSnapshotsRep = true @@ -38,7 +38,7 @@ org.gradle.java.installations.auto-download=false enableMavenLocalRepo = true # Override default Hibernate ORM version -#hibernateOrmVersion = 6.2.0-SNAPSHOT +hibernateOrmVersion = 6.2.0-SNAPSHOT # If set to true, skip Hibernate ORM version parsing (default is true, if set to null) # this is required when using intervals or weird versions or the build will fail diff --git a/hibernate-reactive-core/build.gradle b/hibernate-reactive-core/build.gradle index 5dbb1967a..5482c3c90 100644 --- a/hibernate-reactive-core/build.gradle +++ b/hibernate-reactive-core/build.gradle @@ -41,7 +41,7 @@ dependencies { testRuntimeOnly "org.postgresql:postgresql:42.5.0" // JDBC driver for Testcontainers with MS SQL Server - testRuntimeOnly "com.microsoft.sqlserver:mssql-jdbc:10.2.1.jre11" + testRuntimeOnly "com.microsoft.sqlserver:mssql-jdbc:12.2.0.jre11" // JDBC driver for Testcontainers with MariaDB Server testRuntimeOnly "org.mariadb.jdbc:mariadb-java-client:3.0.7"; @@ -49,6 +49,9 @@ dependencies { // JDBC driver for Testcontainers with MYSQL Server testRuntimeOnly "com.mysql:mysql-connector-j:8.0.32"; + // JDBC driver for Db2 server, for testing + testRuntimeOnly "com.ibm.db2:jcc:11.5.8.0" + // EHCache testRuntimeOnly ("org.ehcache:ehcache:3.10.0-alpha0") { capabilities { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/PreparedStatementAdaptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/PreparedStatementAdaptor.java index 780e332b0..6ca18a732 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/PreparedStatementAdaptor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/PreparedStatementAdaptor.java @@ -33,8 +33,11 @@ import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; import java.util.Arrays; import java.util.Calendar; +import java.util.function.Function; /** * Collects parameter bindings from Hibernate core code @@ -168,7 +171,16 @@ public void setTimestamp(int parameterIndex, Timestamp x) { @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) { - put( parameterIndex, x.toInstant().atZone( cal.getTimeZone().toZoneId() ).toLocalDateTime() ); + setTimestamp( parameterIndex, x, cal, ZonedDateTime::toOffsetDateTime ); + } + + // Sometimes we need a different approach depending on the database + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal, Function converter) { + put( parameterIndex, converter.apply( x.toInstant().atZone( cal.getTimeZone().toZoneId() ) ) ); + } + + public void setTimestamp(String name, Timestamp x, Calendar cal, Function converter) { + throw new UnsupportedOperationException(); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java index ca282c914..3f253a5c2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/AbstractReactiveSaveEventListener.java @@ -12,6 +12,7 @@ import org.hibernate.LockMode; import org.hibernate.NonUniqueObjectException; import org.hibernate.action.internal.AbstractEntityInsertAction; +import org.hibernate.action.internal.EntityIdentityInsertAction; import org.hibernate.engine.internal.CascadePoint; import org.hibernate.engine.internal.Versioning; import org.hibernate.engine.spi.EntityEntry; @@ -131,7 +132,6 @@ protected CompletionStage reactiveSaveWithGeneratedId( .thenCompose( generatedId -> performSaveWithId( entity, context, source, persister, generator, generatedId ) ); } - // FIXME: I think this should be a reactive type final Object generatedId = ( (BeforeExecutionGenerator) generator ).generate( source, entity, null, INSERT ); return performSaveWithId( entity, context, source, persister, generator, generatedId ); } @@ -282,68 +282,69 @@ protected CompletionStage reactivePerformSaveOrReplicate( ); return cascadeBeforeSave( source, persister, entity, context ) - .thenCompose( v -> { - // We have to do this after cascadeBeforeSave completes, - // since it could result in generation of parent ids, - // which we will need as foreign keys in the insert - - Object[] values = persister.getPropertyValuesToInsert( entity, getMergeMap( context ), source ); - Type[] types = persister.getPropertyTypes(); - - boolean substitute = substituteValuesIfNecessary( entity, id, values, persister, source ); - - if ( persister.hasCollections() ) { - boolean substituteBecauseOfCollections = visitCollectionsBeforeSave( entity, id, values, types, source ); - substitute = substitute || substituteBecauseOfCollections; - } - - if ( substitute ) { - persister.setValues( entity, values ); - } - - TypeHelper.deepCopy( - values, - types, - persister.getPropertyUpdateability(), - values, - source - ); - - CompletionStage insert = addInsertAction( - values, id, entity, persister, useIdentityColumn, source, shouldDelayIdentityInserts - ); - - EntityEntry newEntry = persistenceContext.getEntry( entity ); - - if ( newEntry != original ) { - EntityEntryExtraState extraState = newEntry.getExtraState( EntityEntryExtraState.class ); - if ( extraState == null ) { - newEntry.addExtraState( original.getExtraState( EntityEntryExtraState.class ) ); + .thenCompose( v -> addInsertAction( + // We have to do this after cascadeBeforeSave completes, + // since it could result in generation of parent ids, + // which we will need as foreign keys in the insert + cloneAndSubstituteValues( entity, persister, context, source, id ), + id, + entity, + persister, + useIdentityColumn, + source, + shouldDelayIdentityInserts + ) ) + .thenCompose( insert -> cascadeAfterSave( source, persister, entity, context ) + .thenAccept( unused -> { + final Object finalId = handleGeneratedId( useIdentityColumn, id, insert ); + EntityEntry newEntry = persistenceContext.getEntry( entity ); + + if ( newEntry != original ) { + EntityEntryExtraState extraState = newEntry.getExtraState( EntityEntryExtraState.class ); + if ( extraState == null ) { + newEntry.addExtraState( original.getExtraState( EntityEntryExtraState.class ) ); + } } - } - - return insert; - } ) - .thenCompose( vv -> cascadeAfterSave( source, persister, entity, context ) ); - -// .thenAccept( v -> { - // postpone initializing id in case the insert has non-nullable transient dependencies - // that are not resolved until cascadeAfterSave() is executed - -// Object newId = id; -// if ( useIdentityColumn && insert.isEarlyInsert() ) { -// if ( !EntityIdentityInsertAction.class.isInstance( insert ) ) { -// throw new IllegalStateException( -// "Insert should be using an identity column, but action is of unexpected type: " + -// insert.getClass().getName() -// ); -// } -// newId = ( (EntityIdentityInsertAction) insert ).getGeneratedId(); -// -// insert.handleNaturalIdPostSaveNotifications( newId ); -// } -// return newId; -// } ); + } ) + ); + } + + private static Object handleGeneratedId(boolean useIdentityColumn, Object id, AbstractEntityInsertAction insert) { + if ( useIdentityColumn && insert.isEarlyInsert() ) { + if ( insert instanceof EntityIdentityInsertAction ) { + Object generatedId = ( (EntityIdentityInsertAction) insert ).getGeneratedId(); + insert.handleNaturalIdPostSaveNotifications( generatedId ); + return generatedId; + } + throw new IllegalStateException( + "Insert should be using an identity column, but action is of unexpected type: " + insert.getClass() + .getName() ); + } + + return id; + } + + private Object[] cloneAndSubstituteValues(Object entity, EntityPersister persister, C context, EventSource source, Object id) { + Object[] values = persister.getPropertyValuesToInsert( entity, getMergeMap(context), source ); + Type[] types = persister.getPropertyTypes(); + + boolean substitute = substituteValuesIfNecessary( entity, id, values, persister, source ); + if ( persister.hasCollections() ) { + substitute = visitCollectionsBeforeSave( entity, id, values, types, source ) || substitute; + } + + if ( substitute ) { + persister.setValues( entity, values ); + } + + TypeHelper.deepCopy( + values, + types, + persister.getPropertyUpdateability(), + values, + source + ); + return values; } protected Map getMergeMap(C anything) { 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 8edb2eeb5..6b8127dc2 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 @@ -10,6 +10,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -54,6 +55,23 @@ private static String createInsert(PreparedStatementDetails insertStatementDetai if ( instanceOf( dialect, PostgreSQLDialect.class ) ) { return insertStatementDetails.getSqlString() + " returning " + identifierColumnName; } + if ( instanceOf( dialect, SQLServerDialect.class ) ) { + String sql = insertStatementDetails.getSqlString(); + int index = sql.lastIndexOf( " returning " + identifierColumnName ); + // FIXME: this is a hack for HHH-16365 + if ( index > -1 ) { + sql = sql.substring( 0, index ); + } + if ( sql.endsWith( "default values" ) ) { + index = sql.indexOf( "default values" ); + sql = sql.substring( 0, index ); + sql = sql + "output inserted." + identifierColumnName + " default values"; + } + else { + sql = sql.replace( ") values (", ") output inserted." + identifierColumnName + " values (" ); + } + return sql; + } return insertStatementDetails.getSqlString(); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java index 535c86ebb..d6495cc24 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java @@ -21,7 +21,9 @@ import org.hibernate.engine.spi.SubselectFetch; import org.hibernate.loader.ast.internal.LoaderSelectBuilder; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.PluralAttributeMappingImpl; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.reactive.metamodel.mapping.internal.ReactivePluralAttributeMapping; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -47,11 +49,13 @@ public class ReactiveCollectionLoaderSingleKey implements ReactiveCollectionLoad private final List jdbcParameters; public ReactiveCollectionLoaderSingleKey(PluralAttributeMapping attributeMapping, LoadQueryInfluencers influencers, SessionFactoryImplementor sessionFactory) { - this.attributeMapping = attributeMapping; - this.keyJdbcCount = attributeMapping.getKeyDescriptor().getJdbcTypeCount(); + // PluralAttributeMappingImpl is the only implementation available at the moment in ORM + final PluralAttributeMapping reactivePluralAttributeMapping = new ReactivePluralAttributeMapping( (PluralAttributeMappingImpl) attributeMapping ); + this.attributeMapping = reactivePluralAttributeMapping; + this.keyJdbcCount = reactivePluralAttributeMapping.getKeyDescriptor().getJdbcTypeCount(); this.jdbcParameters = new ArrayList<>(); this.sqlAst = LoaderSelectBuilder.createSelect( - attributeMapping, + reactivePluralAttributeMapping, null, attributeMapping.getKeyDescriptor(), null, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java index db180fb66..b474695c3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java @@ -7,6 +7,8 @@ +import java.sql.SQLWarning; + import jakarta.persistence.PersistenceException; import org.hibernate.HibernateException; @@ -276,4 +278,8 @@ public interface Log extends BasicLogger { @LogMessage(level = WARN) @Message(id = 447, value= "Explicit use of UPGRADE_SKIPLOCKED in lock() calls is not recommended; use normal UPGRADE locking instead") void explicitSkipLockedLockCombo(); + + @LogMessage(level = WARN) + @Message(id = 448, value = "Warnings creating temp table : %s") + void warningsCreatingTempTable(SQLWarning warning); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveOneToManyCollectionPart.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveOneToManyCollectionPart.java new file mode 100644 index 000000000..c2a12b00a --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveOneToManyCollectionPart.java @@ -0,0 +1,44 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.mapping.internal; + +import org.hibernate.engine.FetchTiming; +import org.hibernate.metamodel.mapping.internal.OneToManyCollectionPart; +import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchJoinedImpl; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.entity.EntityFetch; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; + +public class ReactiveOneToManyCollectionPart extends OneToManyCollectionPart { + + public ReactiveOneToManyCollectionPart(OneToManyCollectionPart delegate) { + super( delegate ); + } + + @Override + public EntityFetch generateFetch( + FetchParent fetchParent, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + boolean selected, + String resultVariable, + DomainResultCreationState creationState) { + EntityFetch entityFetch = super.generateFetch( + fetchParent, + fetchablePath, + fetchTiming, + selected, + resultVariable, + creationState + ); + if ( entityFetch instanceof EntityFetchJoinedImpl ) { + return new ReactiveEntityFetchJoinedImpl( (EntityFetchJoinedImpl) entityFetch ); + } + return entityFetch; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactivePluralAttributeMapping.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactivePluralAttributeMapping.java new file mode 100644 index 000000000..6a132c56b --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactivePluralAttributeMapping.java @@ -0,0 +1,59 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.mapping.internal; + +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.PluralAttributeMappingImpl; +import org.hibernate.reactive.sql.results.graph.collection.internal.ReactiveCollectionDomainResult; +import org.hibernate.reactive.sql.results.graph.collection.internal.ReactiveEagerCollectionFetch; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.FetchParent; + +public class ReactivePluralAttributeMapping extends PluralAttributeMappingImpl implements PluralAttributeMapping { + + public ReactivePluralAttributeMapping(PluralAttributeMappingImpl original) { + super( original ); + } + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + final TableGroup collectionTableGroup = creationState.getSqlAstCreationState() + .getFromClauseAccess() + .getTableGroup( navigablePath ); + + assert collectionTableGroup != null; + + // This is only used for collection initialization where we know the owner is available, so we mark it as visited + // which will cause bidirectional to-one associations to be treated as such and avoid a join + creationState.registerVisitedAssociationKey( getKeyDescriptor().getAssociationKey() ); + + return new ReactiveCollectionDomainResult<>( navigablePath, this, resultVariable, tableGroup, creationState ); + } + + @Override + protected Fetch buildEagerCollectionFetch( + NavigablePath fetchedPath, + PluralAttributeMapping fetchedAttribute, + TableGroup collectionTableGroup, + FetchParent fetchParent, + DomainResultCreationState creationState) { + return new ReactiveEagerCollectionFetch( + fetchedPath, + fetchedAttribute, + collectionTableGroup, + fetchParent, + creationState + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveToOneAttributeMapping.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveToOneAttributeMapping.java index 1dd466283..13ca55f41 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveToOneAttributeMapping.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveToOneAttributeMapping.java @@ -5,64 +5,28 @@ */ package org.hibernate.reactive.metamodel.mapping.internal; -import java.util.List; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import org.hibernate.annotations.NotFoundAction; -import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.generator.Generator; -import org.hibernate.internal.util.IndexedConsumer; -import org.hibernate.metamodel.mapping.AttributeMapping; -import org.hibernate.metamodel.mapping.AttributeMetadata; -import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; -import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; -import org.hibernate.metamodel.mapping.MappingType; -import org.hibernate.metamodel.mapping.ModelPart; -import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.SelectableConsumer; -import org.hibernate.metamodel.mapping.SelectableMapping; -import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; -import org.hibernate.metamodel.model.domain.NavigableRole; -import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchJoinedImpl; import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchSelectImpl; import org.hibernate.reactive.sql.results.internal.domain.ReactiveCircularFetchImpl; -import org.hibernate.spi.DotIdentifierSequence; import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.spi.SqlAliasBase; -import org.hibernate.sql.ast.spi.SqlAstCreationState; -import org.hibernate.sql.ast.spi.SqlSelection; -import org.hibernate.sql.ast.tree.from.LazyTableGroup; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupProducer; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; -import org.hibernate.sql.results.graph.FetchOptions; import org.hibernate.sql.results.graph.FetchParent; -import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.entity.EntityFetch; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl; import org.hibernate.sql.results.internal.domain.CircularFetchImpl; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.java.MutabilityPlan; -public class ReactiveToOneAttributeMapping extends ToOneAttributeMapping { +import static java.util.Objects.requireNonNull; - private final ToOneAttributeMapping delegate; +public class ReactiveToOneAttributeMapping extends ToOneAttributeMapping { public ReactiveToOneAttributeMapping(ToOneAttributeMapping delegate) { - super( delegate ); - this.delegate = delegate; + super( requireNonNull( delegate ) ); } @Override @@ -73,7 +37,7 @@ public EntityFetch generateFetch( boolean selected, String resultVariable, DomainResultCreationState creationState) { - EntityFetch entityFetch = delegate.generateFetch( + EntityFetch entityFetch = super.generateFetch( fetchParent, fetchablePath, fetchTiming, @@ -81,6 +45,9 @@ public EntityFetch generateFetch( resultVariable, creationState ); + if ( entityFetch instanceof EntityFetchJoinedImpl ) { + return new ReactiveEntityFetchJoinedImpl( (EntityFetchJoinedImpl) entityFetch ); + } if (entityFetch instanceof EntityFetchSelectImpl) { return new ReactiveEntityFetchSelectImpl( (EntityFetchSelectImpl) entityFetch ); } @@ -88,8 +55,12 @@ public EntityFetch generateFetch( } @Override - public Fetch resolveCircularFetch(NavigablePath fetchablePath, FetchParent fetchParent, FetchTiming fetchTiming, DomainResultCreationState creationState) { - Fetch fetch = delegate.resolveCircularFetch( fetchablePath, fetchParent, fetchTiming, creationState ); + public Fetch resolveCircularFetch( + NavigablePath fetchablePath, + FetchParent fetchParent, + FetchTiming fetchTiming, + DomainResultCreationState creationState) { + Fetch fetch = super.resolveCircularFetch( fetchablePath, fetchParent, fetchTiming, creationState ); if ( fetch instanceof CircularFetchImpl ) { return new ReactiveCircularFetchImpl( (CircularFetchImpl) fetch ); } @@ -100,695 +71,6 @@ public Fetch resolveCircularFetch(NavigablePath fetchablePath, FetchParent fetch public ReactiveToOneAttributeMapping copy( ManagedMappingType declaringType, TableGroupProducer declaringTableGroupProducer) { - return new ReactiveToOneAttributeMapping( delegate.copy( declaringType, declaringTableGroupProducer ) ); - } - - @Override - public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { - delegate.setForeignKeyDescriptor( foreignKeyDescriptor ); - } - - @Override - public String getIdentifyingColumnsTableExpression() { - return delegate.getIdentifyingColumnsTableExpression(); - } - - @Override - public void setIdentifyingColumnsTableExpression(String tableExpression) { - delegate.setIdentifyingColumnsTableExpression( tableExpression ); - } - - @Override - public ForeignKeyDescriptor getForeignKeyDescriptor() { - return delegate.getForeignKeyDescriptor(); - } - - @Override - public ForeignKeyDescriptor.Nature getSideNature() { - return delegate.getSideNature(); - } - - @Override - public boolean isReferenceToPrimaryKey() { - return delegate.isReferenceToPrimaryKey(); - } - - @Override - public boolean isFkOptimizationAllowed() { - return delegate.isFkOptimizationAllowed(); - } - - @Override - public boolean hasPartitionedSelectionMapping() { - return delegate.hasPartitionedSelectionMapping(); - } - - @Override - public String getReferencedPropertyName() { - return delegate.getReferencedPropertyName(); - } - - @Override - public String getTargetKeyPropertyName() { - return delegate.getTargetKeyPropertyName(); - } - - @Override - public Set getTargetKeyPropertyNames() { - return delegate.getTargetKeyPropertyNames(); - } - - @Override - public Cardinality getCardinality() { - return delegate.getCardinality(); - } - - @Override - public EntityMappingType getMappedType() { - return delegate.getMappedType(); - } - - @Override - public EntityMappingType getEntityMappingType() { - return delegate.getEntityMappingType(); - } - - @Override - public NavigableRole getNavigableRole() { - return delegate.getNavigableRole(); - } - - @Override - public ModelPart findSubPart(String name) { - return delegate.findSubPart( name ); - } - - @Override - public ModelPart findSubPart(String name, EntityMappingType targetType) { - return delegate.findSubPart( name, targetType ); - } - - @Override - public boolean isBidirectionalAttributeName( - NavigablePath parentNavigablePath, - ModelPart parentModelPart, - NavigablePath fetchablePath, - DomainResultCreationState creationState) { - return super.isBidirectionalAttributeName( - parentNavigablePath, - parentModelPart, - fetchablePath, - creationState - ); - } - @Override - public DomainResult createSnapshotDomainResult( - NavigablePath navigablePath, - TableGroup tableGroup, - String resultVariable, - DomainResultCreationState creationState) { - return delegate.createSnapshotDomainResult( navigablePath, tableGroup, resultVariable, creationState ); - } - - @Override - public TableGroup createTableGroupInternal( - boolean canUseInnerJoins, - NavigablePath navigablePath, - boolean fetched, - String sourceAlias, - SqlAliasBase sqlAliasBase, - SqlAstCreationState creationState) { - return delegate.createTableGroupInternal( - canUseInnerJoins, - navigablePath, - fetched, - sourceAlias, - sqlAliasBase, - creationState - ); - } - - @Override - public TableGroupJoin createTableGroupJoin( - NavigablePath navigablePath, - TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, - boolean fetched, - boolean addsPredicate, - SqlAstCreationState creationState) { - return delegate.createTableGroupJoin( - navigablePath, - lhs, - explicitSourceAlias, - explicitSqlAliasBase, - requestedJoinType, - fetched, - addsPredicate, - creationState - ); - } - - @Override - public LazyTableGroup createRootTableGroupJoin( - NavigablePath navigablePath, - TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, - boolean fetched, - Consumer predicateConsumer, - SqlAstCreationState creationState) { - return delegate.createRootTableGroupJoin( - navigablePath, - lhs, - explicitSourceAlias, - explicitSqlAliasBase, - requestedJoinType, - fetched, - predicateConsumer, - creationState - ); - } - - @Override - public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) { - return delegate.getDefaultSqlAstJoinType( parentTableGroup ); - } - - @Override - public boolean isSimpleJoinPredicate(Predicate predicate) { - return delegate.isSimpleJoinPredicate( predicate ); - } - - @Override - public int getNumberOfFetchables() { - return delegate.getNumberOfFetchables(); - } - - @Override - public Fetchable getFetchable(int position) { - return delegate.getFetchable( position ); - } - - @Override - public String getSqlAliasStem() { - return delegate.getSqlAliasStem(); - } - - @Override - public boolean isNullable() { - return delegate.isNullable(); - } - - @Override - public boolean isOptional() { - return delegate.isOptional(); - } - - @Override - public boolean isInternalLoadNullable() { - return delegate.isInternalLoadNullable(); - } - - @Override - public NotFoundAction getNotFoundAction() { - return delegate.getNotFoundAction(); - } - - @Override - public boolean isIgnoreNotFound() { - return delegate.isIgnoreNotFound(); - } - - @Override - public boolean hasNotFoundAction() { - return delegate.hasNotFoundAction(); - } - - @Override - public boolean isUnwrapProxy() { - return delegate.isUnwrapProxy(); - } - - @Override - public EntityMappingType getAssociatedEntityMappingType() { - return delegate.getAssociatedEntityMappingType(); - } - - @Override - public ModelPart getKeyTargetMatchPart() { - return delegate.getKeyTargetMatchPart(); - } - - @Override - public String toString() { - return delegate.toString(); - } - - @Override - public int breakDownJdbcValues( - Object domainValue, - int offset, - X x, - Y y, - JdbcValueBiConsumer valueConsumer, - SharedSessionContractImplementor session) { - return delegate.breakDownJdbcValues( domainValue, offset, x, y, valueConsumer, session ); - } - - @Override - public int forEachSelectable(int offset, SelectableConsumer consumer) { - return delegate.forEachSelectable( offset, consumer ); - } - - @Override - public void applySqlSelections( - NavigablePath navigablePath, - TableGroup tableGroup, - DomainResultCreationState creationState) { - delegate.applySqlSelections( navigablePath, tableGroup, creationState ); - } - - @Override - public void applySqlSelections( - NavigablePath navigablePath, - TableGroup tableGroup, - DomainResultCreationState creationState, - BiConsumer selectionConsumer) { - delegate.applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); - } - - @Override - public String getContainingTableExpression() { - return delegate.getContainingTableExpression(); - } - - @Override - public int getJdbcTypeCount() { - return delegate.getJdbcTypeCount(); - } - - @Override - public SelectableMapping getSelectable(int columnIndex) { - return delegate.getSelectable( columnIndex ); - } - - @Override - public int forEachJdbcType(int offset, IndexedConsumer action) { - return delegate.forEachJdbcType( offset, action ); - } - - @Override - public Object disassemble(Object value, SharedSessionContractImplementor session) { - return delegate.disassemble( value, session ); - } - - @Override - public int forEachDisassembledJdbcValue( - Object value, - int offset, - X x, - Y y, - JdbcValuesBiConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachDisassembledJdbcValue( value, offset, x, y, valuesConsumer, session ); - } - - @Override - public int forEachJdbcValue( - Object value, - int offset, - X x, - Y y, - JdbcValuesBiConsumer consumer, - SharedSessionContractImplementor session) { - return delegate.forEachJdbcValue( value, offset, x, y, consumer, session ); - } - - @Override - public PropertyAccess getPropertyAccess() { - return delegate.getPropertyAccess(); - } - - @Override - public Generator getGenerator() { - return delegate.getGenerator(); - } - - @Override - public int getStateArrayPosition() { - return delegate.getStateArrayPosition(); - } - - @Override - public AttributeMetadata getAttributeMetadata() { - return delegate.getAttributeMetadata(); - } - - @Override - public String getFetchableName() { - return delegate.getFetchableName(); - } - - @Override - public FetchOptions getMappedFetchOptions() { - return delegate.getMappedFetchOptions(); - } - - @Override - public FetchStyle getStyle() { - return delegate.getStyle(); - } - - @Override - public FetchTiming getTiming() { - return delegate.getTiming(); - } - - @Override - public ManagedMappingType getDeclaringType() { - return delegate.getDeclaringType(); - } - - @Override - public String getAttributeName() { - return delegate.getAttributeName(); - } - - @Override - public int getFetchableKey() { - return delegate.getFetchableKey(); - } - - @Override - public MappingType getPartMappingType() { - return delegate.getPartMappingType(); - } - - @Override - public JavaType getJavaType() { - return delegate.getJavaType(); - } - - @Override - public String getPartName() { - return delegate.getPartName(); - } - - @Override - public Object getValue(Object container) { - return delegate.getValue( container ); - } - - @Override - public void setValue(Object container, Object value) { - delegate.setValue( container, value ); - } - - @Override - public EntityMappingType findContainingEntityMapping() { - return delegate.findContainingEntityMapping(); - } - - @Override - public MutabilityPlan getExposedMutabilityPlan() { - return delegate.getExposedMutabilityPlan(); - } - - @Override - public int compare(Object value1, Object value2) { - return delegate.compare( value1, value2 ); - } - - @Override - public AttributeMapping asAttributeMapping() { - return delegate.asAttributeMapping(); - } - - @Override - public PluralAttributeMapping asPluralAttributeMapping() { - return delegate.asPluralAttributeMapping(); - } - - @Override - public boolean isPluralAttributeMapping() { - return delegate.isPluralAttributeMapping(); - } - - @Override - public EmbeddedAttributeMapping asEmbeddedAttributeMapping() { - return delegate.asEmbeddedAttributeMapping(); - } - - @Override - public boolean isEmbeddedAttributeMapping() { - return delegate.isEmbeddedAttributeMapping(); - } - - @Override - public List getJdbcMappings() { - return delegate.getJdbcMappings(); - } - - @Override - public JdbcMapping getJdbcMapping(int index) { - return delegate.getJdbcMapping( index ); - } - - @Override - public JdbcMapping getSingleJdbcMapping() { - return delegate.getSingleJdbcMapping(); - } - - @Override - public int forEachSelectable(SelectableConsumer consumer) { - return delegate.forEachSelectable( consumer ); - } - - @Override - public void forEachInsertable(SelectableConsumer consumer) { - delegate.forEachInsertable( consumer ); - } - - @Override - public void forEachUpdatable(SelectableConsumer consumer) { - delegate.forEachUpdatable( consumer ); - } - - @Override - public boolean isVirtual() { - return delegate.isVirtual(); - } - - @Override - public boolean isEntityIdentifierMapping() { - return delegate.isEntityIdentifierMapping(); - } - - @Override - public DomainResult createDomainResult( - NavigablePath navigablePath, - TableGroup tableGroup, - String resultVariable, - DomainResultCreationState creationState) { - return delegate.createDomainResult( navigablePath, tableGroup, resultVariable, creationState ); - } - - @Override - public int breakDownJdbcValues( - Object domainValue, - JdbcValueConsumer valueConsumer, - SharedSessionContractImplementor session) { - return delegate.breakDownJdbcValues( domainValue, valueConsumer, session ); - } - - @Override - public int decompose( - Object domainValue, - JdbcValueConsumer valueConsumer, - SharedSessionContractImplementor session) { - return delegate.decompose( domainValue, valueConsumer, session ); - } - - @Override - public int decompose( - Object domainValue, - int offset, - X x, - Y y, - JdbcValueBiConsumer valueConsumer, - SharedSessionContractImplementor session) { - return delegate.decompose( domainValue, offset, x, y, valueConsumer, session ); - } - - @Override - public boolean areEqual(Object one, Object other, SharedSessionContractImplementor session) { - return delegate.areEqual( one, other, session ); - } - - @Override - public int forEachJdbcType(IndexedConsumer action) { - return delegate.forEachJdbcType( action ); - } - - @Override - public int forEachDisassembledJdbcValue( - Object value, - X x, - Y y, - JdbcValuesBiConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachDisassembledJdbcValue( value, x, y, valuesConsumer, session ); - } - - @Override - public int forEachDisassembledJdbcValue( - Object value, - JdbcValuesConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachDisassembledJdbcValue( value, valuesConsumer, session ); - } - - @Override - public int forEachDisassembledJdbcValue( - Object value, - int offset, - JdbcValuesConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachDisassembledJdbcValue( value, offset, valuesConsumer, session ); - } - - @Override - public int forEachJdbcValue( - Object value, - X x, - Y y, - JdbcValuesBiConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachJdbcValue( value, x, y, valuesConsumer, session ); - } - - @Override - public int forEachJdbcValue( - Object value, - JdbcValuesConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachJdbcValue( value, valuesConsumer, session ); - } - - @Override - public int forEachJdbcValue( - Object value, - int offset, - JdbcValuesConsumer valuesConsumer, - SharedSessionContractImplementor session) { - return delegate.forEachJdbcValue( value, offset, valuesConsumer, session ); - } - - @Override - public JavaType getExpressibleJavaType() { - return delegate.getExpressibleJavaType(); - } - - @Override - public X treatAs(Class targetType) { - return delegate.treatAs( targetType ); - } - - @Override - public boolean incrementFetchDepth() { - return delegate.incrementFetchDepth(); - } - - @Override - public void forEachSubPart(IndexedConsumer consumer, EntityMappingType treatTarget) { - delegate.forEachSubPart( consumer, treatTarget ); - } - - @Override - public void visitSubParts(Consumer consumer, EntityMappingType targetType) { - delegate.visitSubParts( consumer, targetType ); - } - - @Override - public int getNumberOfKeyFetchables() { - return delegate.getNumberOfKeyFetchables(); - } - - @Override - public int getNumberOfFetchableKeys() { - return delegate.getNumberOfFetchableKeys(); - } - - @Override - public Fetchable getKeyFetchable(int position) { - return delegate.getKeyFetchable( position ); - } - - @Override - public void visitKeyFetchables(Consumer fetchableConsumer, EntityMappingType treatTargetType) { - delegate.visitKeyFetchables( fetchableConsumer, treatTargetType ); - } - - @Override - public void visitKeyFetchables( - IndexedConsumer fetchableConsumer, - EntityMappingType treatTargetType) { - delegate.visitKeyFetchables( fetchableConsumer, treatTargetType ); - } - - @Override - public void visitKeyFetchables( - int offset, - IndexedConsumer fetchableConsumer, - EntityMappingType treatTargetType) { - delegate.visitKeyFetchables( offset, fetchableConsumer, treatTargetType ); - } - - @Override - public void visitFetchables(Consumer fetchableConsumer, EntityMappingType treatTargetType) { - delegate.visitFetchables( fetchableConsumer, treatTargetType ); - } - - @Override - public void visitFetchables( - IndexedConsumer fetchableConsumer, - EntityMappingType treatTargetType) { - delegate.visitFetchables( fetchableConsumer, treatTargetType ); - } - - @Override - public void visitFetchables( - int offset, - IndexedConsumer fetchableConsumer, - EntityMappingType treatTargetType) { - delegate.visitFetchables( offset, fetchableConsumer, treatTargetType ); - } - - @Override - public int getSelectableIndex(String selectableName) { - return delegate.getSelectableIndex( selectableName ); - } - - @Override - public void forEachSubPart(IndexedConsumer consumer) { - delegate.forEachSubPart( consumer ); - } - - @Override - public ModelPart findByPath(String path) { - return delegate.findByPath( path ); - } - - @Override - public ModelPart findByPath(DotIdentifierSequence path) { - return delegate.findByPath( path ); - } - - @Override - public boolean containsTableReference(String tableExpression) { - return delegate.containsTableReference( tableExpression ); + return new ReactiveToOneAttributeMapping( super.copy( declaringType, declaringTableGroupProducer ) ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java index 9b33b1675..6e43d290b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java @@ -12,7 +12,9 @@ import java.util.Map; import java.util.concurrent.CompletionStage; +import org.hibernate.FetchMode; import org.hibernate.LockOptions; +import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; @@ -20,13 +22,20 @@ import org.hibernate.id.IdentityGenerator; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.GeneratedValuesProcessor; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; +import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.query.named.NamedQueryMemento; import org.hibernate.reactive.loader.ast.internal.ReactiveMultiIdLoaderStandard; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdEntityLoaderDynamicBatch; @@ -39,11 +48,14 @@ import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.metamodel.mapping.internal.ReactivePluralAttributeMapping; +import org.hibernate.reactive.metamodel.mapping.internal.ReactiveToOneAttributeMapping; import org.hibernate.reactive.sql.results.internal.ReactiveEntityResultImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.EntityType; import static org.hibernate.pretty.MessageHelper.infoString; @@ -64,7 +76,13 @@ public ReactiveAbstractPersisterDelegate( final PersistentClass persistentClass, final RuntimeModelCreationContext creationContext) { SessionFactoryImplementor factory = creationContext.getSessionFactory(); - singleIdEntityLoader = createReactiveSingleIdEntityLoader( entityPersister, persistentClass, creationContext, factory, entityPersister.getEntityName() ); + singleIdEntityLoader = createReactiveSingleIdEntityLoader( + entityPersister, + persistentClass, + creationContext, + factory, + entityPersister.getEntityName() + ); multiIdEntityLoader = new ReactiveMultiIdLoaderStandard<>( entityPersister, persistentClass, factory ); entityDescriptor = entityPersister; } @@ -80,18 +98,20 @@ public DomainResult createDomainResult( TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - final ReactiveEntityResultImpl entityResult = new ReactiveEntityResultImpl( - navigablePath, - assemblerCreationState, - tableGroup, - resultVariable + final ReactiveEntityResultImpl entityResult = new ReactiveEntityResultImpl( navigablePath, + assemblerCreationState, + tableGroup, + resultVariable ); entityResult.afterInitialize( entityResult, creationState ); //noinspection unchecked return entityResult; } - public CompletionStage> multiLoad(K[] ids, EventSource session, MultiIdLoadOptions loadOptions) { + public CompletionStage> multiLoad( + K[] ids, + EventSource session, + MultiIdLoadOptions loadOptions) { return multiIdEntityLoader.load( ids, loadOptions, session ); } @@ -104,8 +124,11 @@ private static ReactiveSingleIdEntityLoader createReactiveSingleIdEntity int batchSize = batchSize( bootDescriptor, factory ); if ( bootDescriptor.getLoaderName() != null ) { // We must resolve the named query on-demand through the boot model because it isn't initialized yet - final NamedQueryMemento namedQueryMemento = factory.getQueryEngine().getNamedObjectRepository() - .resolve( factory, creationContext.getBootModel(), bootDescriptor.getLoaderName() ); + final NamedQueryMemento namedQueryMemento = factory.getQueryEngine().getNamedObjectRepository().resolve( + factory, + creationContext.getBootModel(), + bootDescriptor.getLoaderName() + ); if ( namedQueryMemento == null ) { throw new IllegalArgumentException( "Could not resolve named load-query [" + entityName + "] : " + bootDescriptor.getLoaderName() ); } @@ -127,70 +150,152 @@ private static int batchSize(PersistentClass bootDescriptor, SessionFactoryImple return batchSize; } - public CompletionStage processInsertGeneratedProperties(Object id, Object entity, Object[] state, GeneratedValuesProcessor processor, SharedSessionContractImplementor session, String entityName) { + public CompletionStage processInsertGeneratedProperties( + Object id, + Object entity, + Object[] state, + GeneratedValuesProcessor processor, + SharedSessionContractImplementor session, + String entityName) { if ( processor == null ) { throw new UnsupportedOperationException( "Entity has no insert-generated properties - `" + entityName + "`" ); } ReactiveGeneratedValuesProcessor reactiveGeneratedValuesProcessor = new ReactiveGeneratedValuesProcessor( - processor.getSelectStatement(), processor.getGeneratedValuesToSelect(), - processor.getJdbcParameters(), processor.getEntityDescriptor(), - processor.getSessionFactory()); - return reactiveGeneratedValuesProcessor.processGeneratedValues(id, entity, state, session); + processor.getSelectStatement(), + processor.getGeneratedValuesToSelect(), + processor.getJdbcParameters(), + processor.getEntityDescriptor(), + processor.getSessionFactory() + ); + return reactiveGeneratedValuesProcessor.processGeneratedValues( id, entity, state, session ); } - public CompletionStage processUpdateGeneratedProperties(Object id, Object entity, Object[] state, GeneratedValuesProcessor processor, SharedSessionContractImplementor session, String entityName) { + public CompletionStage processUpdateGeneratedProperties( + Object id, + Object entity, + Object[] state, + GeneratedValuesProcessor processor, + SharedSessionContractImplementor session, + String entityName) { if ( processor == null ) { throw new UnsupportedOperationException( "Entity has no update-generated properties - `" + entityName + "`" ); } ReactiveGeneratedValuesProcessor reactiveGeneratedValuesProcessor = new ReactiveGeneratedValuesProcessor( - processor.getSelectStatement(), processor.getGeneratedValuesToSelect(), - processor.getJdbcParameters(), processor.getEntityDescriptor(), - processor.getSessionFactory()); - return reactiveGeneratedValuesProcessor.processGeneratedValues(id, entity, state, session); + processor.getSelectStatement(), + processor.getGeneratedValuesToSelect(), + processor.getJdbcParameters(), + processor.getEntityDescriptor(), + processor.getSessionFactory() + ); + return reactiveGeneratedValuesProcessor.processGeneratedValues( id, entity, state, session ); } public Map> getUniqueKeyLoadersNew() { return uniqueKeyLoadersNew; } - protected ReactiveSingleUniqueKeyEntityLoader getReactiveUniqueKeyLoader(EntityPersister entityDescriptor, SingularAttributeMapping attribute) { + protected ReactiveSingleUniqueKeyEntityLoader getReactiveUniqueKeyLoader( + EntityPersister entityDescriptor, + SingularAttributeMapping attribute) { if ( uniqueKeyLoadersNew == null ) { uniqueKeyLoadersNew = new IdentityHashMap<>(); } - return uniqueKeyLoadersNew - .computeIfAbsent( attribute, key -> new ReactiveSingleUniqueKeyEntityLoaderStandard<>( entityDescriptor, key ) ); + return uniqueKeyLoadersNew.computeIfAbsent( + attribute, + key -> new ReactiveSingleUniqueKeyEntityLoaderStandard<>( + entityDescriptor, + key + ) + ); } - public CompletionStage load(EntityPersister persister, Object id, Object optionalObject, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session) { + public CompletionStage load( + EntityPersister persister, + Object id, + Object optionalObject, + LockOptions lockOptions, + Boolean readOnly, + SharedSessionContractImplementor session) { if ( LOG.isTraceEnabled() ) { LOG.tracev( "Fetching entity: {0}", MessageHelper.infoString( persister, id, persister.getFactory() ) ); } - return optionalObject == null - ? singleIdEntityLoader.load( id, lockOptions, readOnly, session ) - : singleIdEntityLoader.load( id, optionalObject, lockOptions, readOnly, session ); + return optionalObject == null ? + singleIdEntityLoader.load( id, lockOptions, readOnly, session ) : + singleIdEntityLoader.load( id, optionalObject, lockOptions, readOnly, session ); } public Generator reactive(Generator generator) { - return generator instanceof IdentityGenerator - ? new ReactiveIdentityGenerator() - : generator; + return generator instanceof IdentityGenerator ? new ReactiveIdentityGenerator() : generator; } public CompletionStage loadEntityIdByNaturalId( - Object[] orderedNaturalIdValues, - LockOptions lockOptions, - SharedSessionContractImplementor session) { + Object[] orderedNaturalIdValues, LockOptions lockOptions, SharedSessionContractImplementor session) { if ( LOG.isTraceEnabled() ) { - LOG.tracef( - "Resolving natural-id [%s] to id : %s ", - Arrays.asList( orderedNaturalIdValues ), - infoString( entityDescriptor ) + LOG.tracef( "Resolving natural-id [%s] to id : %s ", + Arrays.asList( orderedNaturalIdValues ), + infoString( entityDescriptor ) ); } - return ( (ReactiveNaturalIdLoader) entityDescriptor.getNaturalIdLoader() ) - .resolveNaturalIdToId( orderedNaturalIdValues, session ); + return ( (ReactiveNaturalIdLoader) entityDescriptor.getNaturalIdLoader() ).resolveNaturalIdToId( + orderedNaturalIdValues, + session + ); + } + + public AttributeMapping buildSingularAssociationAttributeMapping( + String attrName, + NavigableRole navigableRole, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + EntityPersister declaringEntityPersister, + EntityType attrType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + MappingModelCreationProcess creationProcess) { + return MappingModelCreationHelper + .buildSingularAssociationAttributeMapping( + attrName, + navigableRole, + stateArrayPosition, + fetchableIndex, + bootProperty, + declaringType, + declaringEntityPersister, + attrType, + propertyAccess, + cascadeStyle, + creationProcess, + ReactiveToOneAttributeMapping::new + ); + } + + public AttributeMapping buildPluralAttributeMapping( + String attrName, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + FetchMode fetchMode, + MappingModelCreationProcess creationProcess) { + return MappingModelCreationHelper + .buildPluralAttributeMapping( + attrName, + stateArrayPosition, + fetchableIndex, + bootProperty, + declaringType, + propertyAccess, + cascadeStyle, + fetchMode, + creationProcess, + ReactivePluralAttributeMapping::new + ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java index e82ae661a..fb784f8d9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java @@ -5,11 +5,13 @@ */ package org.hibernate.reactive.persister.entity.impl; +import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.NaturalIdDataAccess; +import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; @@ -18,16 +20,22 @@ import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.ast.spi.SingleUniqueKeyEntityLoader; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.JoinedSubclassEntityPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; +import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleIdEntityLoader; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.persister.entity.mutation.ReactiveDeleteCoordinator; @@ -39,6 +47,7 @@ import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.entity.internal.EntityResultJoinedSubclassImpl; +import org.hibernate.type.EntityType; import java.sql.PreparedStatement; import java.util.List; @@ -63,6 +72,59 @@ public ReactiveJoinedSubclassEntityPersister( reactiveDelegate = new ReactiveAbstractPersisterDelegate( this, persistentClass, creationContext ); } + + @Override + protected AttributeMapping buildSingularAssociationAttributeMapping( + String attrName, + NavigableRole navigableRole, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + EntityPersister declaringEntityPersister, + EntityType attrType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + MappingModelCreationProcess creationProcess) { + return reactiveDelegate.buildSingularAssociationAttributeMapping( + attrName, + navigableRole, + stateArrayPosition, + fetchableIndex, + bootProperty, + declaringType, + declaringEntityPersister, + attrType, + propertyAccess, + cascadeStyle, + creationProcess + ); + } + + @Override + protected AttributeMapping buildPluralAttributeMapping( + String attrName, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + FetchMode fetchMode, + MappingModelCreationProcess creationProcess) { + return reactiveDelegate.buildPluralAttributeMapping( + attrName, + stateArrayPosition, + fetchableIndex, + bootProperty, + declaringType, + propertyAccess, + cascadeStyle, + fetchMode, + creationProcess + ); + } + @Override public String generateSelectVersionString() { String sql = super.generateSelectVersionString(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java index a80848055..7297c9738 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java @@ -9,11 +9,13 @@ import java.util.List; import java.util.concurrent.CompletionStage; +import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.NaturalIdDataAccess; +import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; @@ -22,21 +24,24 @@ import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.ast.spi.SingleUniqueKeyEntityLoader; import org.hibernate.mapping.PersistentClass; -import org.hibernate.metamodel.mapping.*; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; -import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.SingleTableEntityPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleIdEntityLoader; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; -import org.hibernate.reactive.metamodel.mapping.internal.ReactiveToOneAttributeMapping; import org.hibernate.reactive.persister.entity.mutation.ReactiveDeleteCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveInsertCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinator; @@ -45,7 +50,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; -import org.hibernate.tuple.NonIdentifierAttribute; +import org.hibernate.type.EntityType; /** @@ -101,23 +106,55 @@ public DomainResult createDomainResult( } @Override - protected AttributeMapping generateNonIdAttributeMapping( - NonIdentifierAttribute tupleAttrDefinition, - Property bootProperty, + protected AttributeMapping buildSingularAssociationAttributeMapping( + String attrName, + NavigableRole navigableRole, int stateArrayPosition, int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + EntityPersister declaringEntityPersister, + EntityType attrType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, MappingModelCreationProcess creationProcess) { - AttributeMapping attributeMapping = super.generateNonIdAttributeMapping( - tupleAttrDefinition, + return reactiveDelegate.buildSingularAssociationAttributeMapping( + attrName, + navigableRole, + stateArrayPosition, + fetchableIndex, bootProperty, + declaringType, + declaringEntityPersister, + attrType, + propertyAccess, + cascadeStyle, + creationProcess + ); + } + + @Override + protected AttributeMapping buildPluralAttributeMapping( + String attrName, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + FetchMode fetchMode, + MappingModelCreationProcess creationProcess) { + return reactiveDelegate.buildPluralAttributeMapping( + attrName, stateArrayPosition, fetchableIndex, + bootProperty, + declaringType, + propertyAccess, + cascadeStyle, + fetchMode, creationProcess ); - if ( attributeMapping instanceof ToOneAttributeMapping ) { - return new ReactiveToOneAttributeMapping( (ToOneAttributeMapping) attributeMapping ); - } - return attributeMapping; } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java index 2083cabaf..7880672d3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java @@ -5,12 +5,19 @@ */ package org.hibernate.reactive.persister.entity.impl; +import java.lang.invoke.MethodHandles; +import java.sql.PreparedStatement; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.MappingException; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.NaturalIdDataAccess; +import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; @@ -20,15 +27,21 @@ import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.ast.spi.SingleUniqueKeyEntityLoader; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleIdEntityLoader; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; @@ -42,11 +55,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; - -import java.lang.invoke.MethodHandles; -import java.sql.PreparedStatement; -import java.util.List; -import java.util.concurrent.CompletionStage; +import org.hibernate.type.EntityType; /** * An {@link ReactiveEntityPersister} backed by {@link UnionSubclassEntityPersister} @@ -67,6 +76,58 @@ public ReactiveUnionSubclassEntityPersister( reactiveDelegate = new ReactiveAbstractPersisterDelegate( this, persistentClass, creationContext ); } + @Override + protected AttributeMapping buildSingularAssociationAttributeMapping( + String attrName, + NavigableRole navigableRole, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + EntityPersister declaringEntityPersister, + EntityType attrType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + MappingModelCreationProcess creationProcess) { + return reactiveDelegate.buildSingularAssociationAttributeMapping( + attrName, + navigableRole, + stateArrayPosition, + fetchableIndex, + bootProperty, + declaringType, + declaringEntityPersister, + attrType, + propertyAccess, + cascadeStyle, + creationProcess + ); + } + + @Override + protected AttributeMapping buildPluralAttributeMapping( + String attrName, + int stateArrayPosition, + int fetchableIndex, + Property bootProperty, + ManagedMappingType declaringType, + PropertyAccess propertyAccess, + CascadeStyle cascadeStyle, + FetchMode fetchMode, + MappingModelCreationProcess creationProcess) { + return reactiveDelegate.buildPluralAttributeMapping( + attrName, + stateArrayPosition, + fetchableIndex, + bootProperty, + declaringType, + propertyAccess, + cascadeStyle, + fetchMode, + creationProcess + ); + } + @Override public NaturalIdMapping generateNaturalIdMapping(MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { return ReactiveAbstractEntityPersister.super.generateNaturalIdMapping(creationProcess, bootEntityDescriptor); 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 884ab2ca5..4cb08f6ac 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 @@ -6,13 +6,24 @@ package org.hibernate.reactive.provider.impl; import java.lang.reflect.Type; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.TimeZone; import org.hibernate.boot.model.TypeContributions; import org.hibernate.boot.model.TypeContributor; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.dialect.ReactiveDialectWrapper; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.AbstractSingleColumnStandardBasicType; import org.hibernate.type.BasicTypeRegistry; @@ -24,13 +35,18 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.java.StringJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; +import org.hibernate.type.descriptor.jdbc.BasicBinder; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.descriptor.jdbc.ObjectJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; +import org.hibernate.type.spi.TypeConfiguration; import io.vertx.core.json.JsonObject; @@ -52,14 +68,22 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv } private void registerReactiveChanges(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { - BasicTypeRegistry basicTypeRegistry = typeContributions.getTypeConfiguration().getBasicTypeRegistry(); - DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + Dialect dialect = dialect( serviceRegistry ); + TypeConfiguration typeConfiguration = typeContributions.getTypeConfiguration(); + + DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); ddlTypeRegistry.addDescriptor( Types.JAVA_OBJECT, new JavaObjectDdlType() ); - JavaTypeRegistry javaTypeRegistry = typeContributions.getTypeConfiguration().getJavaTypeRegistry(); + JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); javaTypeRegistry.addDescriptor( JsonObjectJavaType.INSTANCE ); - Dialect dialect = serviceRegistry.getService( JdbcEnvironment.class ).getDialect(); + if ( dialect instanceof MySQLDialect ) { + JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptor( TimestampAsLocalDateTimeJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimestampUtcAsLocalDateTimeJdbcType.INSTANCE ); + } + + BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); basicTypeRegistry.register( new JsonType( dialect ) ); // FIXME: I think only Postgres, needs this special type because of the way the driver returns the SQL type // We could only add them for Postgres @@ -67,6 +91,89 @@ private void registerReactiveChanges(TypeContributions typeContributions, Servic basicTypeRegistry.register( new ClobType( dialect ) ); } + private Dialect dialect(ServiceRegistry serviceRegistry) { + Dialect dialect = serviceRegistry.getService( JdbcEnvironment.class ).getDialect(); + return dialect instanceof ReactiveDialectWrapper + ? ( (ReactiveDialectWrapper) dialect ).getWrappedDialect() + : dialect; + } + + /** + * Some databases (MySQL for example) don't like saving temporal types with a timezone. + * + * @see TimestampJdbcType + */ + private static class TimestampAsLocalDateTimeJdbcType extends TimestampJdbcType { + public static final TimestampAsLocalDateTimeJdbcType INSTANCE = new TimestampAsLocalDateTimeJdbcType(); + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, options ); + if ( value instanceof Calendar ) { + ( (PreparedStatementAdaptor) st ) + .setTimestamp( index, timestamp, (Calendar) value, ZonedDateTime::toLocalDateTime ); + } + else if ( options.getJdbcTimeZone() != null ) { + ( (PreparedStatementAdaptor) st ) + .setTimestamp( index, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ), ZonedDateTime::toLocalDateTime ); + } + else { + st.setTimestamp( index, timestamp ); + } + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, options ); + if ( value instanceof Calendar ) { + ( (PreparedStatementAdaptor) st ) + .setTimestamp( name, timestamp, (Calendar) value, ZonedDateTime::toLocalDateTime ); + } + else if ( options.getJdbcTimeZone() != null ) { + ( (PreparedStatementAdaptor) st ) + .setTimestamp( name, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ), ZonedDateTime::toLocalDateTime ); + } + else { + st.setTimestamp( name, timestamp ); + } + } + }; + } + } + + /** + * Some database (MySQL for example) don't like saving temporal types with a timezone. + * + * @see TimestampUtcAsJdbcTimestampJdbcType + */ + private static class TimestampUtcAsLocalDateTimeJdbcType extends TimestampUtcAsJdbcTimestampJdbcType { + private static final Calendar UTC_CALENDAR = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + + public static final TimestampUtcAsLocalDateTimeJdbcType INSTANCE = new TimestampUtcAsLocalDateTimeJdbcType(); + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final Instant instant = javaType.unwrap( value, Instant.class, options ); + ( (PreparedStatementAdaptor) st).setTimestamp( index, Timestamp.from( instant ), UTC_CALENDAR, ZonedDateTime::toLocalDateTime ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final Instant instant = javaType.unwrap( value, Instant.class, options ); + ( (PreparedStatementAdaptor) st).setTimestamp( name, Timestamp.from( instant ), UTC_CALENDAR, ZonedDateTime::toLocalDateTime ); + } + }; + } + } + private static class JavaObjectDdlType implements DdlType { @Override @@ -106,7 +213,6 @@ private static class JsonObjectJavaType implements BasicJavaType { @Override public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { - // FIXME: Check this // use Types.JAVA_OBJECT instead of Types.JSON because // the Dialects have the type 'json' registered under // that JDBC type code @@ -139,11 +245,6 @@ public JsonType(Dialect dialect) { super( ObjectJdbcType.INSTANCE, JsonObjectJavaType.INSTANCE ); } - @Override - public JavaType getJdbcJavaType() { - return super.getJdbcJavaType(); - } - @Override public String getName() { return "json"; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/ReactiveSqmMultiTableMutationStrategyProvider.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/ReactiveSqmMultiTableMutationStrategyProvider.java index 35254cc1e..791ce111f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/ReactiveSqmMultiTableMutationStrategyProvider.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/ReactiveSqmMultiTableMutationStrategyProvider.java @@ -11,11 +11,15 @@ import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.query.sqm.mutation.internal.cte.CteInsertStrategy; import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableInsertStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableMutationStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategyProvider; import org.hibernate.reactive.query.sqm.mutation.internal.cte.ReactiveCteInsertStrategy; import org.hibernate.reactive.query.sqm.mutation.internal.cte.ReactiveCteMutationStrategy; +import org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveLocalTemporaryTableInsertStrategy; +import org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveLocalTemporaryTableMutationStrategy; public class ReactiveSqmMultiTableMutationStrategyProvider implements SqmMultiTableMutationStrategyProvider { @@ -28,6 +32,9 @@ public SqmMultiTableMutationStrategy createMutationStrategy( if ( mutationStrategy instanceof CteMutationStrategy ) { return new ReactiveCteMutationStrategy( rootEntityDescriptor, creationContext ); } + if ( mutationStrategy instanceof LocalTemporaryTableMutationStrategy ) { + return new ReactiveLocalTemporaryTableMutationStrategy( (LocalTemporaryTableMutationStrategy) mutationStrategy ); + } return mutationStrategy; } @@ -49,6 +56,9 @@ public SqmMultiTableInsertStrategy createInsertStrategy( if ( insertStrategy instanceof CteInsertStrategy ) { return new ReactiveCteInsertStrategy( rootEntityDescriptor, creationContext ); } + if ( insertStrategy instanceof LocalTemporaryTableInsertStrategy ) { + return new ReactiveLocalTemporaryTableInsertStrategy( (LocalTemporaryTableInsertStrategy) insertStrategy ); + } return insertStrategy; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java index 32fa09b51..e2d277e2d 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java @@ -252,6 +252,9 @@ protected int dataTypeCode(String typeName) { typeName.toLowerCase().startsWith( "double" ) ) { return Types.DOUBLE; } + if ( typeName.equalsIgnoreCase( "real" ) ) { + return Types.FLOAT; + } return super.dataTypeCode( typeName ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/iternal/ReactiveSimpleDeleteQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/iternal/ReactiveSimpleDeleteQueryPlan.java index 4f8d9af2c..746950975 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/iternal/ReactiveSimpleDeleteQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/iternal/ReactiveSimpleDeleteQueryPlan.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive.query.sqm.iternal; +import java.sql.PreparedStatement; import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; @@ -139,63 +140,65 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter { - if ( missingRestriction ) { - return null; - } - - final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); - final Expression fkColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( - new MutatingTableReferenceGroupWrapper( + return ReactiveSqmMutationStrategyHelper.cleanUpCollectionTables( + entityDescriptor, + (tableReference, attributeMapping) -> { + if ( missingRestriction ) { + return null; + } + + final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); + final Expression fkColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( + new MutatingTableReferenceGroupWrapper( + new NavigablePath( attributeMapping.getRootPathName() ), + attributeMapping, + (NamedTableReference) tableReference + ), + fkDescriptor.getKeyPart(), + null, + factory + ); + + final QuerySpec matchingIdSubQuery = new QuerySpec( false ); + + final MutatingTableReferenceGroupWrapper tableGroup = new MutatingTableReferenceGroupWrapper( new NavigablePath( attributeMapping.getRootPathName() ), attributeMapping, - (NamedTableReference) tableReference - ), - fkDescriptor.getKeyPart(), - null, - factory - ); - - final QuerySpec matchingIdSubQuery = new QuerySpec( false ); - - final MutatingTableReferenceGroupWrapper tableGroup = new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - sqmInterpretation.getSqlAst().getTargetTable() - ); - final Expression fkTargetColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( - tableGroup, - fkDescriptor.getTargetPart(), - sqmInterpretation.getSqlExpressionResolver(), - factory - ); - matchingIdSubQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( - 1, - 0, - fkTargetColumnExpression - ) ); - - matchingIdSubQuery.getFromClause().addRoot( - tableGroup - ); - - matchingIdSubQuery.applyPredicate( sqmInterpretation.getSqlAst().getRestriction() ); - - return new InSubQueryPredicate( fkColumnExpression, matchingIdSubQuery, false ); - }, - ( missingRestriction ? JdbcParameterBindings.NO_BINDINGS : jdbcParameterBindings ), - executionContextAdapter - ); - - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcDelete, - jdbcParameterBindings, - session.getJdbcCoordinator().getStatementPreparer()::prepareStatement, - (i, ps) -> {}, + sqmInterpretation.getSqlAst().getTargetTable() + ); + final Expression fkTargetColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( + tableGroup, + fkDescriptor.getTargetPart(), + sqmInterpretation.getSqlExpressionResolver(), + factory + ); + matchingIdSubQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( + 1, + 0, + fkTargetColumnExpression + ) ); + + matchingIdSubQuery.getFromClause().addRoot( + tableGroup + ); + + matchingIdSubQuery.applyPredicate( sqmInterpretation.getSqlAst().getRestriction() ); + + return new InSubQueryPredicate( fkColumnExpression, matchingIdSubQuery, false ); + }, + ( missingRestriction ? JdbcParameterBindings.NO_BINDINGS : jdbcParameterBindings ), executionContextAdapter + ) + .thenCompose( unused -> StandardReactiveJdbcMutationExecutor.INSTANCE + .executeReactive( + jdbcDelete, + jdbcParameterBindings, + session.getJdbcCoordinator().getStatementPreparer()::prepareStatement, + ReactiveSimpleDeleteQueryPlan::doNothing, + executionContextAdapter ) ); } + + private static void doNothing(Integer i, PreparedStatement ps) { + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java index b58872cd0..0db3d8686 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal; +import java.sql.PreparedStatement; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; @@ -229,7 +230,7 @@ private static CompletionStage cleanUpCollectionTable( jdbcDelete, jdbcParameterBindings, executionContext.getSession().getJdbcCoordinator().getStatementPreparer()::prepareStatement, - (i, ps) -> {}, + ReactiveSqmMutationStrategyHelper::doNothing, executionContext ) .thenCompose( CompletionStages::voidFuture ); @@ -243,4 +244,7 @@ private static void complete(CompletableFuture stage, Throwable throwable) stage.completeExceptionally( throwable ); } } + + private static void doNothing(Integer i, PreparedStatement ps) { + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveInsertExecutionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveInsertExecutionDelegate.java new file mode 100644 index 000000000..1f5ccff36 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveInsertExecutionDelegate.java @@ -0,0 +1,74 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.cte; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.InsertExecutionDelegate; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveTableBasedInsertHandler; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.exec.spi.ExecutionContext; + +/** + * @see InsertExecutionDelegate + */ +public class ReactiveInsertExecutionDelegate extends InsertExecutionDelegate implements ReactiveTableBasedInsertHandler.ReactiveExecutionDelegate { + + + public ReactiveInsertExecutionDelegate( + SqmInsertStatement sqmInsert, + MultiTableSqmMutationConverter sqmConverter, + TemporaryTable entityTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + DomainParameterXref domainParameterXref, + TableGroup insertingTableGroup, + Map tableReferenceByAlias, + List assignments, + InsertSelectStatement insertStatement, + Map, List>> parameterResolutions, + JdbcParameter sessionUidParameter, + Map, MappingModelExpressible> paramTypeResolutions, + DomainQueryExecutionContext executionContext) { + super( + sqmInsert, + sqmConverter, + entityTable, + afterUseAction, + sessionUidAccess, + domainParameterXref, + insertingTableGroup, + tableReferenceByAlias, + assignments, + insertStatement, + parameterResolutions, + sessionUidParameter, + paramTypeResolutions, + executionContext + ); + } + + @Override + public CompletionStage reactiveExecute(ExecutionContext executionContext) { + return null; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java new file mode 100644 index 000000000..2b751cff6 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java @@ -0,0 +1,364 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.sql.PreparedStatement; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.boot.TempTableDdlTransactionHandling; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.dialect.temptable.TemporaryTableColumn; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.BeforeUseAction; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.results.internal.SqlSelectionImpl; + +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +/** + * @see org.hibernate.query.sqm.mutation.internal.temptable.ExecuteWithTemporaryTableHelper + */ +public final class ReactiveExecuteWithTemporaryTableHelper { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private ReactiveExecuteWithTemporaryTableHelper() { + } + + public static CompletionStage saveMatchingIdsIntoIdTable( + MultiTableSqmMutationConverter sqmConverter, + Predicate suppliedPredicate, + TemporaryTable idTable, + Function sessionUidAccess, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup(); + + assert mutatingTableGroup.getModelPart() instanceof EntityMappingType; + final EntityMappingType mutatingEntityDescriptor = (EntityMappingType) mutatingTableGroup.getModelPart(); + + final NamedTableReference idTableReference = new NamedTableReference( + idTable.getTableExpression(), + InsertSelectStatement.DEFAULT_ALIAS + ); + final InsertSelectStatement idTableInsert = new InsertSelectStatement( idTableReference ); + + for ( int i = 0; i < idTable.getColumns().size(); i++ ) { + final TemporaryTableColumn column = idTable.getColumns().get( i ); + idTableInsert.addTargetColumnReferences( + new ColumnReference( + idTableReference, + column.getColumnName(), + // id columns cannot be formulas and cannot have custom read and write expressions + false, + null, + column.getJdbcMapping() + ) + ); + } + + final QuerySpec matchingIdSelection = new QuerySpec( true, 1 ); + idTableInsert.setSourceSelectStatement( matchingIdSelection ); + + matchingIdSelection.getFromClause().addRoot( mutatingTableGroup ); + + mutatingEntityDescriptor.getIdentifierMapping().forEachSelectable( + (jdbcPosition, selection) -> { + final TableReference tableReference = mutatingTableGroup.resolveTableReference( + mutatingTableGroup.getNavigablePath(), + selection.getContainingTableExpression() + ); + matchingIdSelection.getSelectClause().addSqlSelection( + new SqlSelectionImpl( + jdbcPosition, + jdbcPosition + 1, + sqmConverter.getSqlExpressionResolver().resolveSqlExpression( + tableReference, + selection + ) + ) + ); + } + ); + + if ( idTable.getSessionUidColumn() != null ) { + final int jdbcPosition = matchingIdSelection.getSelectClause().getSqlSelections().size(); + matchingIdSelection.getSelectClause().addSqlSelection( + new SqlSelectionImpl( + jdbcPosition, + jdbcPosition + 1, + new QueryLiteral<>( + UUID.fromString( sessionUidAccess.apply( executionContext.getSession() ) ), + (BasicValuedMapping) idTable.getSessionUidColumn().getJdbcMapping() + ) + ) + ); + } + + matchingIdSelection.applyPredicate( suppliedPredicate ); + return saveIntoTemporaryTable( idTableInsert, jdbcParameterBindings, executionContext ); + } + + public static CompletionStage saveIntoTemporaryTable( + InsertSelectStatement temporaryTableInsert, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + final JdbcServices jdbcServices = factory.getJdbcServices(); + final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); + final LockOptions lockOptions = executionContext.getQueryOptions().getLockOptions(); + final LockMode lockMode = lockOptions.getLockMode(); + // Acquire a WRITE lock for the rows that are about to be modified + lockOptions.setLockMode( LockMode.WRITE ); + // Visit the table joins and reset the lock mode if we encounter OUTER joins that are not supported + if ( temporaryTableInsert.getSourceSelectStatement() != null + && !jdbcEnvironment.getDialect().supportsOuterJoinForUpdate() ) { + temporaryTableInsert.getSourceSelectStatement().visitQuerySpecs( + querySpec -> querySpec.getFromClause().visitTableJoins( + tableJoin -> { + if ( tableJoin.getJoinType() != SqlAstJoinType.INNER ) { + lockOptions.setLockMode( lockMode ); + } + } + ) + ); + } + final JdbcOperationQueryInsert jdbcInsert = sqlAstTranslatorFactory.buildInsertTranslator( factory, temporaryTableInsert ) + .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); + lockOptions.setLockMode( lockMode ); + + return StandardReactiveJdbcMutationExecutor.INSTANCE + .executeReactive( + jdbcInsert, + jdbcParameterBindings, + executionContext.getSession().getJdbcCoordinator().getStatementPreparer()::prepareStatement, + ReactiveExecuteWithTemporaryTableHelper::doNothing, + executionContext + + ); + } + + public static QuerySpec createIdTableSelectQuerySpec( + TemporaryTable idTable, + Function sessionUidAccess, + EntityMappingType entityDescriptor, + ExecutionContext executionContext) { + return createIdTableSelectQuerySpec( idTable, null, sessionUidAccess, entityDescriptor, executionContext ); + } + + public static QuerySpec createIdTableSelectQuerySpec( + TemporaryTable idTable, + ModelPart fkModelPart, + Function sessionUidAccess, + EntityMappingType entityDescriptor, + ExecutionContext executionContext) { + final QuerySpec querySpec = new QuerySpec( false ); + + final NamedTableReference idTableReference = new NamedTableReference( + idTable.getTableExpression(), + TemporaryTable.DEFAULT_ALIAS, + true + ); + final TableGroup idTableGroup = new StandardTableGroup( + true, + new NavigablePath( idTableReference.getTableExpression() ), + entityDescriptor, + null, + idTableReference, + null, + executionContext.getSession().getFactory() + ); + + querySpec.getFromClause().addRoot( idTableGroup ); + + applyIdTableSelections( querySpec, idTableReference, idTable, fkModelPart, executionContext ); + applyIdTableRestrictions( querySpec, idTableReference, idTable, sessionUidAccess, executionContext ); + + return querySpec; + } + + private static void applyIdTableSelections( + QuerySpec querySpec, + TableReference tableReference, + TemporaryTable idTable, + ModelPart fkModelPart, + ExecutionContext executionContext) { + if ( fkModelPart == null ) { + final int size = idTable.getEntityDescriptor().getIdentifierMapping().getJdbcTypeCount(); + for ( int i = 0; i < size; i++ ) { + final TemporaryTableColumn temporaryTableColumn = idTable.getColumns().get( i ); + if ( temporaryTableColumn != idTable.getSessionUidColumn() ) { + querySpec.getSelectClause().addSqlSelection( + new SqlSelectionImpl( + i + 1, + i, + new ColumnReference( + tableReference, + temporaryTableColumn.getColumnName(), + false, + null, + temporaryTableColumn.getJdbcMapping() + ) + ) + ); + } + } + } + else { + fkModelPart.forEachSelectable( + (i, selectableMapping) -> { + querySpec.getSelectClause().addSqlSelection( + new SqlSelectionImpl( + i + 1, + i, + new ColumnReference( + tableReference, + selectableMapping.getSelectionExpression(), + false, + null, + selectableMapping.getJdbcMapping() + ) + ) + ); + } + ); + } + } + + private static void applyIdTableRestrictions( + QuerySpec querySpec, + TableReference idTableReference, + TemporaryTable idTable, + Function sessionUidAccess, + ExecutionContext executionContext) { + if ( idTable.getSessionUidColumn() != null ) { + querySpec.applyPredicate( + new ComparisonPredicate( + new ColumnReference( + idTableReference, + idTable.getSessionUidColumn().getColumnName(), + false, + null, + idTable.getSessionUidColumn().getJdbcMapping() + ), + ComparisonOperator.EQUAL, + new QueryLiteral<>( + UUID.fromString( sessionUidAccess.apply( executionContext.getSession() ) ), + (BasicValuedMapping) idTable.getSessionUidColumn().getJdbcMapping() + ) + ) + ); + } + } + + public static CompletionStage performBeforeTemporaryTableUseActions( + TemporaryTable temporaryTable, + ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + final Dialect dialect = factory.getJdbcServices().getDialect(); + if ( dialect.getTemporaryTableBeforeUseAction() == BeforeUseAction.CREATE ) { + final ReactiveTemporaryTableHelper.TemporaryTableCreationWork temporaryTableCreationWork = new ReactiveTemporaryTableHelper + .TemporaryTableCreationWork( temporaryTable, factory ); + + final TempTableDdlTransactionHandling ddlTransactionHandling = dialect.getTemporaryTableDdlTransactionHandling(); + if ( ddlTransactionHandling == TempTableDdlTransactionHandling.NONE ) { + return temporaryTableCreationWork.reactiveExecute( ( (ReactiveConnectionSupplier) executionContext.getSession() ).getReactiveConnection() ); + } + else { + throw LOG.notYetImplemented(); +// final IsolationDelegate isolationDelegate = executionContext.getSession() +// .getJdbcCoordinator() +// .getJdbcSessionOwner() +// .getTransactionCoordinator() +// .createIsolationDelegate(); +// isolationDelegate.delegateWork( temporaryTableCreationWork, ddlTransactionHandling == TempTableDdlTransactionHandling.ISOLATE_AND_TRANSACT ); + } + } + return voidFuture(); + } + + public static CompletionStage performAfterTemporaryTableUseActions( + TemporaryTable temporaryTable, + Function sessionUidAccess, + AfterUseAction afterUseAction, + ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + final Dialect dialect = factory.getJdbcServices().getDialect(); + switch ( afterUseAction ) { + case CLEAN: + return ReactiveTemporaryTableHelper.cleanTemporaryTableRows( + temporaryTable, + dialect.getTemporaryTableExporter(), + sessionUidAccess, + executionContext.getSession() + ); + case DROP: + final ReactiveTemporaryTableHelper.TemporaryTableDropWork temporaryTableDropWork = new ReactiveTemporaryTableHelper.TemporaryTableDropWork( + temporaryTable, + factory + ); + + final TempTableDdlTransactionHandling ddlTransactionHandling = dialect.getTemporaryTableDdlTransactionHandling(); + if ( ddlTransactionHandling == TempTableDdlTransactionHandling.NONE ) { + return temporaryTableDropWork.reactiveExecute( ( (ReactiveConnectionSupplier) executionContext.getSession() ).getReactiveConnection() ); + } + else { + throw LOG.notYetImplemented(); +// final IsolationDelegate isolationDelegate = executionContext.getSession() +// .getJdbcCoordinator() +// .getJdbcSessionOwner() +// .getTransactionCoordinator() +// .createIsolationDelegate(); +// isolationDelegate.delegateWork( +// temporaryTableDropWork, +// ddlTransactionHandling == TempTableDdlTransactionHandling.ISOLATE_AND_TRANSACT +// ); + } + default: + return voidFuture(); + } + } + + private static void doNothing(Integer integer, PreparedStatement preparedStatement) { + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveInsertExectionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveInsertExectionDelegate.java new file mode 100644 index 000000000..742c73385 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveInsertExectionDelegate.java @@ -0,0 +1,12 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +/** + * @see org.hibernate.query.sqm.mutation.internal.temptable.InsertExecutionDelegate + */ +public class ReactiveInsertExectionDelegate { +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java new file mode 100644 index 000000000..df3a8856b --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java @@ -0,0 +1,54 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableInsertStrategy; +import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableInsertStrategy; + +public class ReactiveLocalTemporaryTableInsertStrategy extends LocalTemporaryTableInsertStrategy + implements ReactiveSqmMultiTableInsertStrategy { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveLocalTemporaryTableInsertStrategy(LocalTemporaryTableInsertStrategy insertStrategy) { + super( insertStrategy.getTemporaryTable(), insertStrategy.getSessionFactory() ); + } + + @Override + public CompletionStage reactiveExecuteInsert( + SqmInsertStatement sqmInsertStatement, + DomainParameterXref domainParameterXref, + DomainQueryExecutionContext context) { + return new ReactiveTableBasedInsertHandler( + sqmInsertStatement, + domainParameterXref, + getTemporaryTable(), + afterUserAction(), + ReactiveLocalTemporaryTableInsertStrategy::throwUnexpectedCallToSessionUIDError, + getSessionFactory() + ).reactiveExecute( context ); + } + + private static String throwUnexpectedCallToSessionUIDError(SharedSessionContractImplementor session) { + throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + } + + private AfterUseAction afterUserAction() { + return isDropIdTables() + ? AfterUseAction.DROP + : getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java new file mode 100644 index 000000000..2e4283a09 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java @@ -0,0 +1,71 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableMutationStrategy; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; + +public class ReactiveLocalTemporaryTableMutationStrategy extends LocalTemporaryTableMutationStrategy + implements ReactiveSqmMultiTableMutationStrategy { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveLocalTemporaryTableMutationStrategy(LocalTemporaryTableMutationStrategy mutationStrategy) { + super( mutationStrategy.getTemporaryTable(), mutationStrategy.getSessionFactory() ); + } + + private static String throwUnexpectedAccessToSessionUID(SharedSessionContractImplementor session) { + // Should probably go in the LOG + throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + } + + @Override + public CompletionStage reactiveExecuteUpdate( + SqmUpdateStatement sqmUpdate, + DomainParameterXref domainParameterXref, + DomainQueryExecutionContext context) { + return new ReactiveTableBasedUpdateHandler( + sqmUpdate, + domainParameterXref, + getTemporaryTable(), + afterUseAction(), + ReactiveLocalTemporaryTableMutationStrategy::throwUnexpectedAccessToSessionUID, + getSessionFactory() + ).reactiveExecute( context ); + } + + @Override + public CompletionStage reactiveExecuteDelete( + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref, + DomainQueryExecutionContext context) { + return new ReactiveTableBasedDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + afterUseAction(), + ReactiveLocalTemporaryTableMutationStrategy::throwUnexpectedAccessToSessionUID, + getSessionFactory() + ).reactiveExecute( context ); + } + + private AfterUseAction afterUseAction() { + return isDropIdTables() + ? AfterUseAction.DROP + : getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveRestrictedDeleteExecutionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveRestrictedDeleteExecutionDelegate.java new file mode 100644 index 000000000..73c00a53d --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveRestrictedDeleteExecutionDelegate.java @@ -0,0 +1,679 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.hibernate.dialect.temptable.TemporaryTable; +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.MutableBoolean; +import org.hibernate.internal.util.MutableInteger; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.Joinable; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.ExecuteWithoutIdTableHelper; +import org.hibernate.query.sqm.mutation.internal.temptable.RestrictedDeleteExecutionDelegate; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveSqmMutationStrategyHelper; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.reactive.util.impl.CompletionStages; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.predicate.PredicateCollector; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryDelete; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import static org.hibernate.query.sqm.mutation.internal.temptable.ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +/** + * The reactive version of {@link RestrictedDeleteExecutionDelegate} + */ +// Basically a copy of RestrictedDeleteExecutionDelegate, we will probably need to refactor this code to avoid +// duplication +public class ReactiveRestrictedDeleteExecutionDelegate + implements ReactiveTableBasedDeleteHandler.ReactiveExecutionDelegate { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private final EntityMappingType entityDescriptor; + private final TemporaryTable idTable; + private final AfterUseAction afterUseAction; + private final SqmDeleteStatement sqmDelete; + private final DomainParameterXref domainParameterXref; + private final SessionFactoryImplementor sessionFactory; + + private final Function sessionUidAccess; + private final MultiTableSqmMutationConverter converter; + + public ReactiveRestrictedDeleteExecutionDelegate( + EntityMappingType entityDescriptor, + TemporaryTable idTable, + AfterUseAction afterUseAction, + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref, + Function sessionUidAccess, + QueryOptions queryOptions, + LoadQueryInfluencers loadQueryInfluencers, + QueryParameterBindings queryParameterBindings, + SessionFactoryImplementor sessionFactory) { + this.entityDescriptor = entityDescriptor; + this.idTable = idTable; + this.afterUseAction = afterUseAction; + this.sqmDelete = sqmDelete; + this.domainParameterXref = domainParameterXref; + this.sessionUidAccess = sessionUidAccess; + this.sessionFactory = sessionFactory; + this.converter = new MultiTableSqmMutationConverter( + entityDescriptor, + sqmDelete, + sqmDelete.getTarget(), + domainParameterXref, + queryOptions, + loadQueryInfluencers, + queryParameterBindings, + sessionFactory + ); + } + + @Override + public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + final EntityPersister entityDescriptor = sessionFactory.getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( sqmDelete.getTarget().getEntityName() ); + final String hierarchyRootTableName = ( (Joinable) entityDescriptor ).getTableName(); + + final TableGroup deletingTableGroup = converter.getMutatingTableGroup(); + + final TableReference hierarchyRootTableReference = deletingTableGroup.resolveTableReference( + deletingTableGroup.getNavigablePath(), + hierarchyRootTableName + ); + assert hierarchyRootTableReference != null; + + final Map, List>> parameterResolutions; + final Map, MappingModelExpressible> paramTypeResolutions; + + if ( domainParameterXref.getSqmParameterCount() == 0 ) { + parameterResolutions = Collections.emptyMap(); + paramTypeResolutions = Collections.emptyMap(); + } + else { + parameterResolutions = new IdentityHashMap<>(); + paramTypeResolutions = new LinkedHashMap<>(); + } + + // Use the converter to interpret the where-clause. We do this for 2 reasons: + // 1) the resolved Predicate is ultimately the base for applying restriction to the deletes + // 2) we also inspect each ColumnReference that is part of the where-clause to see which + // table it comes from. if all of the referenced columns (if any at all) are from the root table + // we can perform all of the deletes without using an id-table + final MutableBoolean needsIdTableWrapper = new MutableBoolean( false ); + final Predicate specifiedRestriction = converter.visitWhereClause( + sqmDelete.getWhereClause(), + columnReference -> { + if ( !hierarchyRootTableReference.getIdentificationVariable() + .equals( columnReference.getQualifier() ) ) { + needsIdTableWrapper.setValue( true ); + } + }, + (sqmParameter, mappingType, jdbcParameters) -> { + parameterResolutions.computeIfAbsent( + sqmParameter, + k -> new ArrayList<>( 1 ) + ).add( jdbcParameters ); + paramTypeResolutions.put( sqmParameter, mappingType ); + } + ); + + final PredicateCollector predicateCollector = new PredicateCollector( specifiedRestriction ); + entityDescriptor.applyBaseRestrictions( + (filterPredicate) -> { + needsIdTableWrapper.setValue( true ); + predicateCollector.applyPredicate( filterPredicate ); + }, + deletingTableGroup, + true, + executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), + null, + converter + ); + + converter.pruneTableGroupJoins(); + + // We need an id table if we want to delete from an intermediate table to avoid FK violations + // The intermediate table has a FK to the root table, so we can't delete from the root table first + // Deleting from the intermediate table first also isn't possible, + // because that is the source for deletion in other tables, hence we need an id table + final boolean needsIdTable = needsIdTableWrapper.getValue() + || entityDescriptor != entityDescriptor.getRootEntityDescriptor(); + + final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( + executionContext ); + + if ( needsIdTable ) { + return executeWithIdTable( + predicateCollector.getPredicate(), + deletingTableGroup, + parameterResolutions, + paramTypeResolutions, + executionContextAdapter + ); + } + else { + return executeWithoutIdTable( + predicateCollector.getPredicate(), + deletingTableGroup, + parameterResolutions, + paramTypeResolutions, + converter.getSqlExpressionResolver(), + executionContextAdapter + ); + } + } + + private CompletionStage executeWithoutIdTable( + Predicate suppliedPredicate, + TableGroup tableGroup, + Map, List>> restrictionSqmParameterResolutions, + Map, MappingModelExpressible> paramTypeResolutions, + SqlExpressionResolver sqlExpressionResolver, + ExecutionContext executionContext) { + assert entityDescriptor == entityDescriptor.getRootEntityDescriptor(); + + final EntityPersister rootEntityPersister = entityDescriptor.getEntityPersister(); + final String rootTableName = ( (Joinable) rootEntityPersister ).getTableName(); + final NamedTableReference rootTableReference = (NamedTableReference) tableGroup.resolveTableReference( + tableGroup.getNavigablePath(), + rootTableName + ); + + final QuerySpec matchingIdSubQuerySpec = ExecuteWithoutIdTableHelper.createIdMatchingSubQuerySpec( + tableGroup.getNavigablePath(), + rootTableReference, + suppliedPredicate, + rootEntityPersister, + sqlExpressionResolver, + sessionFactory + ); + + final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( + executionContext.getQueryParameterBindings(), + domainParameterXref, + SqmUtil.generateJdbcParamsXref( + domainParameterXref, + () -> restrictionSqmParameterResolutions + ), + sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), + navigablePath -> tableGroup, + new SqmParameterMappingModelResolutionAccess() { + @Override + @SuppressWarnings("unchecked") + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) paramTypeResolutions.get( parameter ); + } + }, + executionContext.getSession() + ); + + CompletionStage cleanUpCollectionTablesStage = ReactiveSqmMutationStrategyHelper.cleanUpCollectionTables( + entityDescriptor, + (tableReference, attributeMapping) -> { + // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup + if ( suppliedPredicate == null ) { + return null; + } + final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); + final QuerySpec idSelectFkSubQuery; + // todo (6.0): based on the location of the attribute mapping, we could prune the table group of the subquery + if ( fkDescriptor.getTargetPart().isEntityIdentifierMapping() ) { + idSelectFkSubQuery = matchingIdSubQuerySpec; + } + else { + idSelectFkSubQuery = ExecuteWithoutIdTableHelper.createIdMatchingSubQuerySpec( + tableGroup.getNavigablePath(), + rootTableReference, + suppliedPredicate, + rootEntityPersister, + sqlExpressionResolver, + sessionFactory + ); + } + return new InSubQueryPredicate( + MappingModelCreationHelper.buildColumnReferenceExpression( + new MutatingTableReferenceGroupWrapper( + new NavigablePath( attributeMapping.getRootPathName() ), + attributeMapping, + (NamedTableReference) tableReference + ), + fkDescriptor, + null, + sessionFactory + ), + idSelectFkSubQuery, + false + ); + + }, + jdbcParameterBindings, + executionContext + ); + if ( rootTableReference instanceof UnionTableReference ) { + final CompletionStage[] deleteFromNonRootStages = new CompletionStage[] { voidFuture() }; + final MutableInteger rows = new MutableInteger(); + return cleanUpCollectionTablesStage + .thenCompose( v -> visitUnionTableReferences( + suppliedPredicate, + tableGroup, + sqlExpressionResolver, + executionContext, + matchingIdSubQuerySpec, + jdbcParameterBindings, + deleteFromNonRootStages, + rows + ) ) + .thenApply( o -> rows.get() ); + } + else { + final CompletionStage[] deleteFromNonRootStages = new CompletionStage[] { voidFuture() }; + + entityDescriptor.visitConstraintOrderedTables( + (tableExpression, tableKeyColumnVisitationSupplier) -> { + if ( !tableExpression.equals( rootTableName ) ) { + final NamedTableReference tableReference = (NamedTableReference) tableGroup.getTableReference( + tableGroup.getNavigablePath(), + tableExpression, + true, + true + ); + final QuerySpec idMatchingSubQuerySpec; + // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup + idMatchingSubQuerySpec = suppliedPredicate == null ? null : matchingIdSubQuerySpec; + CompletableFuture future = new CompletableFuture<>(); + deleteFromNonRootStages[0] = deleteFromNonRootStages[0] + .thenCompose( v -> future ); + try { + deleteFromNonRootTableWithoutIdTable( + tableReference, + tableKeyColumnVisitationSupplier, + sqlExpressionResolver, + tableGroup, + idMatchingSubQuerySpec, + jdbcParameterBindings, + executionContext + ) + .thenCompose( CompletionStages::voidFuture ) + .whenComplete( (unused, throwable) -> { + if ( throwable == null ) { + future.complete( unused ); + } + else { + future.completeExceptionally( throwable ); + } + } ); + } + catch (Throwable t) { + future.completeExceptionally( t ); + } + } + } + ); + + return deleteFromNonRootStages[0] + .thenCompose( v -> deleteFromRootTableWithoutIdTable( + rootTableReference, + suppliedPredicate, + jdbcParameterBindings, + executionContext + ) ); + } + } + + private CompletionStage visitUnionTableReferences( + Predicate suppliedPredicate, + TableGroup tableGroup, + SqlExpressionResolver sqlExpressionResolver, + ExecutionContext executionContext, + QuerySpec matchingIdSubQuerySpec, + JdbcParameterBindings jdbcParameterBindings, + CompletionStage[] deleteFromNonRootStages, + MutableInteger rows) { + entityDescriptor.visitConstraintOrderedTables( + (tableExpression, tableKeyColumnVisitationSupplier) -> { + final NamedTableReference tableReference = new NamedTableReference( + tableExpression, + tableGroup.getPrimaryTableReference().getIdentificationVariable() + ); + final QuerySpec idMatchingSubQuerySpec; + // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup + idMatchingSubQuerySpec = suppliedPredicate == null ? null : matchingIdSubQuerySpec; + CompletableFuture future = new CompletableFuture<>(); + deleteFromNonRootStages[0] = deleteFromNonRootStages[0] + .thenCompose( v -> future ); + deleteFromNonRootTableWithoutIdTable( + tableReference, + tableKeyColumnVisitationSupplier, + sqlExpressionResolver, + tableGroup, + idMatchingSubQuerySpec, + jdbcParameterBindings, + executionContext + ) + .thenAccept( rows::plus ) + .whenComplete( (unused, throwable) -> { + if ( throwable == null ) { + future.complete( unused ); + } + else { + future.completeExceptionally( throwable ); + } + } ); + } + ); + return deleteFromNonRootStages[0]; + } + + private CompletionStage deleteFromRootTableWithoutIdTable( + NamedTableReference rootTableReference, + Predicate predicate, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + return executeSqlDelete( + new DeleteStatement( rootTableReference, predicate ), + jdbcParameterBindings, + executionContext + ); + } + + private CompletionStage deleteFromNonRootTableWithoutIdTable( + NamedTableReference targetTableReference, + Supplier> tableKeyColumnVisitationSupplier, + SqlExpressionResolver sqlExpressionResolver, + TableGroup rootTableGroup, + QuerySpec matchingIdSubQuerySpec, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + assert targetTableReference != null; + LOG.tracef( "deleteFromNonRootTable - %s", targetTableReference.getTableExpression() ); + + final NamedTableReference deleteTableReference = new NamedTableReference( + targetTableReference.getTableExpression(), + DeleteStatement.DEFAULT_ALIAS, + true + ); + final Predicate tableDeletePredicate; + if ( matchingIdSubQuerySpec == null ) { + tableDeletePredicate = null; + } + else { + /* + * delete from sub_table + * where sub_id in ( + * select root_id from root_table + * where {predicate} + * ) + */ + + /* + * Create the `sub_id` reference as the LHS of the in-subquery predicate + */ + final List deletingTableColumnRefs = new ArrayList<>(); + tableKeyColumnVisitationSupplier.get().accept( + (columnIndex, selection) -> { + assert deleteTableReference.getTableReference( selection.getContainingTableExpression() ) != null; + + final Expression expression = sqlExpressionResolver.resolveSqlExpression( + deleteTableReference, + selection + ); + + deletingTableColumnRefs.add( (ColumnReference) expression ); + } + ); + + final Expression deletingTableColumnRefsExpression = deletingTableColumnRefs.size() == 1 + ? deletingTableColumnRefs.get( 0 ) + : new SqlTuple( deletingTableColumnRefs, entityDescriptor.getIdentifierMapping() ); + + tableDeletePredicate = new InSubQueryPredicate( + deletingTableColumnRefsExpression, + matchingIdSubQuerySpec, + false + ); + } + + final DeleteStatement sqlAstDelete = new DeleteStatement( deleteTableReference, tableDeletePredicate ); + return executeSqlDelete( + sqlAstDelete, + jdbcParameterBindings, + executionContext + ).thenApply( rows -> { + LOG.debugf( "deleteFromNonRootTable - `%s` : %s rows", targetTableReference, rows ); + return rows; + } ); + } + + private static CompletionStage executeSqlDelete( + DeleteStatement sqlAst, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + final JdbcServices jdbcServices = factory.getJdbcServices(); + + final JdbcOperationQueryDelete jdbcDelete = jdbcServices.getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildDeleteTranslator( factory, sqlAst ) + .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); + + return StandardReactiveJdbcMutationExecutor.INSTANCE + .executeReactive( + jdbcDelete, + jdbcParameterBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> {}, + executionContext + ); + } + + private CompletionStage executeWithIdTable( + Predicate predicate, + TableGroup deletingTableGroup, + Map, List>> restrictionSqmParameterResolutions, + Map, MappingModelExpressible> paramTypeResolutions, + ExecutionContext executionContext) { + final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( + executionContext.getQueryParameterBindings(), + domainParameterXref, + SqmUtil.generateJdbcParamsXref( + domainParameterXref, + () -> restrictionSqmParameterResolutions + ), + sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), + navigablePath -> deletingTableGroup, + new SqmParameterMappingModelResolutionAccess() { + @Override + @SuppressWarnings("unchecked") + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) paramTypeResolutions.get( parameter ); + } + }, + executionContext.getSession() + ); + + return ReactiveExecuteWithTemporaryTableHelper + .performBeforeTemporaryTableUseActions( idTable, executionContext ) + .thenCompose( v -> executeUsingIdTable( predicate, executionContext, jdbcParameterBindings ) + .handle( CompletionStages::handle ) + .thenCompose( resultHandler -> ReactiveExecuteWithTemporaryTableHelper + .performAfterTemporaryTableUseActions( + idTable, + sessionUidAccess, + afterUseAction, + executionContext + ) + .thenCompose( resultHandler::getResultAsCompletionStage ) + ) + ); + } + + private CompletionStage executeUsingIdTable( + Predicate predicate, + ExecutionContext executionContext, + JdbcParameterBindings jdbcParameterBindings) { + return ReactiveExecuteWithTemporaryTableHelper.saveMatchingIdsIntoIdTable( + converter, + predicate, + idTable, + sessionUidAccess, + jdbcParameterBindings, + executionContext ) + .thenCompose( rows -> { + final QuerySpec idTableIdentifierSubQuery = createIdTableSelectQuerySpec( + idTable, + sessionUidAccess, + entityDescriptor, + executionContext + ); + + return ReactiveSqmMutationStrategyHelper.cleanUpCollectionTables( + entityDescriptor, + (tableReference, attributeMapping) -> { + final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); + final QuerySpec idTableFkSubQuery = fkDescriptor.getTargetPart() + .isEntityIdentifierMapping() + ? idTableIdentifierSubQuery + : createIdTableSelectQuerySpec( idTable, fkDescriptor.getTargetPart(), sessionUidAccess, entityDescriptor, executionContext ); + return new InSubQueryPredicate( + MappingModelCreationHelper.buildColumnReferenceExpression( + new MutatingTableReferenceGroupWrapper( + new NavigablePath( attributeMapping.getRootPathName() ), + attributeMapping, + (NamedTableReference) tableReference + ), + fkDescriptor, + null, + sessionFactory + ), + idTableFkSubQuery, + false + ); + + }, + JdbcParameterBindings.NO_BINDINGS, + executionContext + ).thenCompose( unused -> visitConstraintOrderedTables( idTableIdentifierSubQuery, executionContext ) + .thenApply( v -> rows ) ); + } ); + } + + private CompletionStage visitConstraintOrderedTables(QuerySpec idTableIdentifierSubQuery, ExecutionContext executionContext) { + final CompletionStage[] resultStage = new CompletionStage[]{ completedFuture( -1 ) }; + entityDescriptor.visitConstraintOrderedTables( + (tableExpression, tableKeyColumnVisitationSupplier) -> { + resultStage[0] = resultStage[0].thenCompose( ignore -> deleteFromTableUsingIdTable( + tableExpression, + tableKeyColumnVisitationSupplier, + idTableIdentifierSubQuery, + executionContext + ) ); + } + ); + return resultStage[0] + .thenCompose( CompletionStages::voidFuture ); + } + + private CompletionStage deleteFromTableUsingIdTable( + String tableExpression, + Supplier> tableKeyColumnVisitationSupplier, + QuerySpec idTableSubQuery, + ExecutionContext executionContext) { + LOG.tracef( "deleteFromTableUsingIdTable - %s", tableExpression ); + + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + + final TableKeyExpressionCollector keyColumnCollector = new TableKeyExpressionCollector( entityDescriptor ); + final NamedTableReference targetTable = new NamedTableReference( + tableExpression, + DeleteStatement.DEFAULT_ALIAS, + true + ); + + tableKeyColumnVisitationSupplier.get().accept( + (columnIndex, selection) -> { + assert selection.getContainingTableExpression().equals( tableExpression ); + assert !selection.isFormula(); + assert selection.getCustomReadExpression() == null; + assert selection.getCustomWriteExpression() == null; + + keyColumnCollector + .apply( new ColumnReference( targetTable, selection ) ); + } + ); + + final InSubQueryPredicate predicate = new InSubQueryPredicate( + keyColumnCollector.buildKeyExpression(), + idTableSubQuery, + false + ); + + return executeSqlDelete( + new DeleteStatement( targetTable, predicate ), + JdbcParameterBindings.NO_BINDINGS, + executionContext + ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java new file mode 100644 index 000000000..c7af71107 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java @@ -0,0 +1,72 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedDeleteHandler; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; + +public class ReactiveTableBasedDeleteHandler extends TableBasedDeleteHandler implements ReactiveAbstractMutationHandler { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public interface ReactiveExecutionDelegate extends TableBasedDeleteHandler.ExecutionDelegate { + + @Override + default int execute(DomainQueryExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } + + CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext); + } + + public ReactiveTableBasedDeleteHandler( + SqmDeleteStatement sqmDeleteStatement, + DomainParameterXref domainParameterXref, + TemporaryTable idTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + SessionFactoryImplementor sessionFactory) { + super( sqmDeleteStatement, domainParameterXref, idTable, afterUseAction, sessionUidAccess, sessionFactory ); + } + + @Override + public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + if ( LOG.isTraceEnabled() ) { + LOG.tracef( + "Starting multi-table delete execution - %s", + getSqmDeleteOrUpdateStatement().getRoot().getModel().getName() + ); + } + return resolveDelegate( executionContext ).reactiveExecute( executionContext ); + } + + protected ReactiveExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { + return new ReactiveRestrictedDeleteExecutionDelegate( + getEntityDescriptor(), + getIdTable(), + getAfterUseAction(), + getSqmDeleteOrUpdateStatement(), + getDomainParameterXref(), + getSessionUidAccess(), + executionContext.getQueryOptions(), + executionContext.getSession().getLoadQueryInfluencers(), + executionContext.getQueryParameterBindings(), + getSessionFactory() + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java new file mode 100644 index 000000000..3889b7d6e --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java @@ -0,0 +1,113 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedInsertHandler; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; +import org.hibernate.reactive.query.sqm.mutation.internal.cte.ReactiveInsertExecutionDelegate; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.exec.spi.ExecutionContext; + +public class ReactiveTableBasedInsertHandler extends TableBasedInsertHandler implements ReactiveHandler { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public interface ReactiveExecutionDelegate extends ExecutionDelegate { + @Override + default int execute(ExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } + + CompletionStage reactiveExecute(ExecutionContext executionContext); + } + + public ReactiveTableBasedInsertHandler( + SqmInsertStatement sqmInsert, + DomainParameterXref domainParameterXref, + TemporaryTable entityTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + SessionFactoryImplementor sessionFactory) { + super( sqmInsert, domainParameterXref, entityTable, afterUseAction, sessionUidAccess, sessionFactory ); + } + + @Override + public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + if ( LOG.isTraceEnabled() ) { + LOG.tracef( + "Starting multi-table insert execution - %s", + getSqmInsertStatement().getTarget().getModel().getName() + ); + } + + final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter + .omittingLockingAndPaging( executionContext ); + return resolveDelegate( executionContext ) + .reactiveExecute( executionContextAdapter ); + } + + @Override + protected ReactiveExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { + return (ReactiveExecutionDelegate) super.resolveDelegate( executionContext ); + } + + @Override + protected ExecutionDelegate buildExecutionDelegate( + SqmInsertStatement sqmInsert, + MultiTableSqmMutationConverter sqmConverter, + TemporaryTable entityTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + DomainParameterXref domainParameterXref, + TableGroup insertingTableGroup, + Map tableReferenceByAlias, + List assignments, + InsertSelectStatement insertStatement, + Map, List>> parameterResolutions, + JdbcParameter sessionUidParameter, + Map, MappingModelExpressible> paramTypeResolutions, + DomainQueryExecutionContext executionContext) { + return new ReactiveInsertExecutionDelegate( + sqmInsert, + sqmConverter, + entityTable, + afterUseAction, + sessionUidAccess, + domainParameterXref, + insertingTableGroup, + tableReferenceByAlias, + assignments, + insertStatement, + parameterResolutions, + sessionUidParameter, + paramTypeResolutions, + executionContext + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java new file mode 100644 index 000000000..a9976809f --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java @@ -0,0 +1,111 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedUpdateHandler; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.exec.spi.ExecutionContext; + +public class ReactiveTableBasedUpdateHandler extends TableBasedUpdateHandler implements ReactiveAbstractMutationHandler { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public interface ReactiveExecutionDelegate extends TableBasedUpdateHandler.ExecutionDelegate { + @Override + default int execute(ExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } + + CompletionStage reactiveExecute(ExecutionContext executionContext); + } + + public ReactiveTableBasedUpdateHandler( + SqmUpdateStatement sqmUpdate, + DomainParameterXref domainParameterXref, + TemporaryTable idTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + SessionFactoryImplementor sessionFactory) { + super( sqmUpdate, domainParameterXref, idTable, afterUseAction, sessionUidAccess, sessionFactory ); + } + + @Override + public int execute(DomainQueryExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } + + @Override + public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + if ( LOG.isTraceEnabled() ) { + LOG.tracef( + "Starting multi-table update execution - %s", + getSqmDeleteOrUpdateStatement().getRoot().getModel().getName() + ); + } + + final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ); + return resolveDelegate( executionContext ).reactiveExecute( executionContextAdapter ); + } + + @Override + protected ReactiveExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { + return (ReactiveExecutionDelegate) super.resolveDelegate( executionContext ); + } + + @Override + protected ReactiveUpdateExcutionDelegate buildExecutionDelegate( + MultiTableSqmMutationConverter sqmConverter, + TemporaryTable idTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + DomainParameterXref domainParameterXref, + TableGroup updatingTableGroup, + Map tableReferenceByAlias, + List assignments, + Predicate suppliedPredicate, + Map, List>> parameterResolutions, + Map, MappingModelExpressible> paramTypeResolutions, + DomainQueryExecutionContext executionContext) { + return new ReactiveUpdateExcutionDelegate( + sqmConverter, + idTable, + afterUseAction, + sessionUidAccess, + domainParameterXref, + updatingTableGroup, + tableReferenceByAlias, + assignments, + suppliedPredicate, + parameterResolutions, + paramTypeResolutions, + executionContext + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java new file mode 100644 index 000000000..25e5b9622 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java @@ -0,0 +1,211 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.sql.SQLWarning; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.dialect.temptable.TemporaryTableExporter; +import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; +import org.hibernate.engine.jdbc.spi.SqlStatementLogger; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.pool.ReactiveConnection; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.reactive.util.impl.CompletionStages; + +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +/** + * @see org.hibernate.dialect.temptable.TemporaryTableHelper + */ +public class ReactiveTemporaryTableHelper { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Creation + + /** + * @see org.hibernate.jdbc.Work + */ + public interface ReactiveWork { + CompletionStage reactiveExecute(ReactiveConnection connection); + } + + public static class TemporaryTableCreationWork implements ReactiveWork { + private final TemporaryTable temporaryTable; + private final TemporaryTableExporter exporter; + private final SessionFactoryImplementor sessionFactory; + + public TemporaryTableCreationWork( + TemporaryTable temporaryTable, + SessionFactoryImplementor sessionFactory) { + this( + temporaryTable, + sessionFactory.getJdbcServices().getDialect().getTemporaryTableExporter(), + sessionFactory + ); + } + + public TemporaryTableCreationWork( + TemporaryTable temporaryTable, + TemporaryTableExporter exporter, + SessionFactoryImplementor sessionFactory) { + this.temporaryTable = temporaryTable; + this.exporter = exporter; + this.sessionFactory = sessionFactory; + } + + @Override + public CompletionStage reactiveExecute(ReactiveConnection connection) { + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + + try { + final String creationCommand = exporter.getSqlCreateCommand( temporaryTable ); + logStatement( creationCommand, jdbcServices ); + + return connection.executeUnprepared( creationCommand ) + .handle( (integer, throwable) -> { + logException( "create", creationCommand, temporaryTable, throwable ); + return null; + } ); + } + catch (Exception e) { + logException( "create", null, temporaryTable, e ); + return voidFuture(); + } + } + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Drop + + public static class TemporaryTableDropWork implements ReactiveWork { + private final TemporaryTable temporaryTable; + private final TemporaryTableExporter exporter; + private final SessionFactoryImplementor sessionFactory; + + public TemporaryTableDropWork( + TemporaryTable temporaryTable, + SessionFactoryImplementor sessionFactory) { + this( + temporaryTable, + sessionFactory.getJdbcServices().getDialect().getTemporaryTableExporter(), + sessionFactory + ); + } + + public TemporaryTableDropWork( + TemporaryTable temporaryTable, + TemporaryTableExporter exporter, + SessionFactoryImplementor sessionFactory) { + this.temporaryTable = temporaryTable; + this.exporter = exporter; + this.sessionFactory = sessionFactory; + } + + @Override + public CompletionStage reactiveExecute(ReactiveConnection connection) { + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + + try { + final String dropCommand = exporter.getSqlDropCommand( temporaryTable ); + logStatement( dropCommand, jdbcServices ); + + return connection.update( dropCommand ) + .handle( (integer, throwable) -> { + logException( "drop", dropCommand, temporaryTable, throwable ); + return null; + } ); + } + catch (Exception e) { + logException( "drop", null, temporaryTable, e ); + return voidFuture(); + } + } + } + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Clean + + public static CompletionStage cleanTemporaryTableRows( + TemporaryTable temporaryTable, + TemporaryTableExporter exporter, + Function sessionUidAccess, + SharedSessionContractImplementor session) { + final String sql = exporter.getSqlTruncateCommand( temporaryTable, sessionUidAccess, session ); + Object[] params = PreparedStatementAdaptor.bind( ps -> { + if ( temporaryTable.getSessionUidColumn() != null ) { + final String sessionUid = sessionUidAccess.apply( session ); + ps.setString( 1, sessionUid ); + } + } ); + + return reactiveConnection( session ) + .update( sql, params ) + .thenCompose( CompletionStages::voidFuture ); + } + + private static ReactiveConnection reactiveConnection(SharedSessionContractImplementor session) { + return ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + } + + private static SqlExceptionHelper.WarningHandler WARNING_HANDLER = new SqlExceptionHelper.WarningHandlerLoggingSupport() { + public boolean doProcess() { + return LOG.isDebugEnabled(); + } + + public void prepare(SQLWarning warning) { + LOG.warningsCreatingTempTable( warning ); + } + + @Override + protected void logWarning(String description, String message) { + LOG.debug( description ); + LOG.debug( message ); + } + }; + + private static void logException(String action, String creationCommand, TemporaryTable temporaryTable, Throwable throwable) { + if ( throwable != null ) { + if ( creationCommand != null ) { + // This is what ORM does + LOG.debugf( + "unable to " + action + " temporary table [%s]; `%s` failed : %s", + temporaryTable.getQualifiedTableName(), + creationCommand, + throwable.getMessage() + ); + } + else { + LOG.debugf( + "unable to " + action + " temporary table [%s] : %s", + temporaryTable.getQualifiedTableName(), + throwable.getMessage() + ); + } + } + } + + private static void logException(String sql, JdbcServices jdbcServices) { + final SqlStatementLogger statementLogger = jdbcServices.getSqlStatementLogger(); + statementLogger.logStatement( sql, FormatStyle.BASIC.getFormatter() ); + } + + private static void logStatement(String sql, JdbcServices jdbcServices) { + final SqlStatementLogger statementLogger = jdbcServices.getSqlStatementLogger(); + statementLogger.logStatement( sql, FormatStyle.BASIC.getFormatter() ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveUpdateExcutionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveUpdateExcutionDelegate.java new file mode 100644 index 000000000..4d270ff31 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveUpdateExcutionDelegate.java @@ -0,0 +1,302 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import java.lang.invoke.MethodHandles; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.UpdateExecutionDelegate; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.reactive.util.impl.CompletionStages; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.predicate.ExistsPredicate; +import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; +import org.hibernate.sql.results.internal.SqlSelectionImpl; + +import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec; +import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.performAfterTemporaryTableUseActions; +import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions; +import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.saveMatchingIdsIntoIdTable; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +public class ReactiveUpdateExcutionDelegate extends UpdateExecutionDelegate implements ReactiveTableBasedUpdateHandler.ReactiveExecutionDelegate { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveUpdateExcutionDelegate( + MultiTableSqmMutationConverter sqmConverter, + TemporaryTable idTable, + AfterUseAction afterUseAction, + Function sessionUidAccess, + DomainParameterXref domainParameterXref, + TableGroup updatingTableGroup, + Map tableReferenceByAlias, + List assignments, + Predicate suppliedPredicate, + Map, List>> parameterResolutions, + Map, MappingModelExpressible> paramTypeResolutions, + DomainQueryExecutionContext executionContext) { + super( + sqmConverter, + idTable, + afterUseAction, + sessionUidAccess, + domainParameterXref, + updatingTableGroup, + tableReferenceByAlias, + assignments, + suppliedPredicate, + parameterResolutions, + paramTypeResolutions, + executionContext + ); + } + + private static void doNothing(Integer integer, PreparedStatement preparedStatement) { + } + + @Override + public int execute(ExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } + + @Override + public CompletionStage reactiveExecute(ExecutionContext executionContext) { + return performBeforeTemporaryTableUseActions( + getIdTable(), + executionContext + ) + .thenCompose( v -> saveMatchingIdsIntoIdTable( + getSqmConverter(), + getSuppliedPredicate(), + getIdTable(), + getSessionUidAccess(), + getJdbcParameterBindings(), + executionContext + ) ) + .thenCompose( rows -> { + final QuerySpec idTableSubQuery = createIdTableSelectQuerySpec( + getIdTable(), + getSessionUidAccess(), + getEntityDescriptor(), + executionContext + ); + + CompletionStage[] resultStage = new CompletionStage[] { voidFuture() }; + getEntityDescriptor().visitConstraintOrderedTables( + (tableExpression, tableKeyColumnVisitationSupplier) -> resultStage[0] = resultStage[0].thenCompose( + v -> reactiveUpdateTable( + tableExpression, + tableKeyColumnVisitationSupplier, + rows, + idTableSubQuery, + executionContext + ) ) + ); + return resultStage[0].thenApply( v -> rows ); + }) + .handle( CompletionStages::handle ) + .thenCompose( handler -> performAfterTemporaryTableUseActions( + getIdTable(), + getSessionUidAccess(), + getAfterUseAction(), + executionContext + ) + .thenCompose( handler::getResultAsCompletionStage ) + ); + } + + private CompletionStage reactiveUpdateTable( + String tableExpression, + Supplier> tableKeyColumnVisitationSupplier, + int expectedUpdateCount, + QuerySpec idTableSubQuery, + ExecutionContext executionContext) { + + // update `updatingTableReference` + // set ... + // where `keyExpression` in ( `idTableSubQuery` ) + + final TableReference updatingTableReference = getUpdatingTableGroup().getTableReference( + getUpdatingTableGroup().getNavigablePath(), + tableExpression, + true, + true + ); + + final List assignments = getAssignmentsByTable().get( updatingTableReference ); + if ( assignments == null || assignments.isEmpty() ) { + // no assignments for this table - skip it + return voidFuture(); + } + + final NamedTableReference dmlTableReference = resolveUnionTableReference( updatingTableReference, tableExpression ); + final JdbcServices jdbcServices = getSessionFactory().getJdbcServices(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcServices.getJdbcEnvironment().getSqlAstTranslatorFactory(); + + final Expression keyExpression = resolveMutatingTableKeyExpression( tableExpression, tableKeyColumnVisitationSupplier ); + + return executeUpdate( idTableSubQuery, executionContext, assignments, dmlTableReference, sqlAstTranslatorFactory, keyExpression ) + .thenCompose( updateCount -> { + // We are done when the update count matches + if ( updateCount == expectedUpdateCount ) { + return voidFuture(); + } + // If the table is optional, execute an insert + if ( isTableOptional( tableExpression ) ) { + return executeInsert( + tableExpression, + dmlTableReference, + keyExpression, + tableKeyColumnVisitationSupplier, + idTableSubQuery, + assignments, + sqlAstTranslatorFactory, + executionContext + ) + .thenAccept( insertCount -> { + assert insertCount + updateCount == expectedUpdateCount; + } ); + } + return voidFuture(); + } ); + } + + + private CompletionStage executeUpdate(QuerySpec idTableSubQuery, ExecutionContext executionContext, List assignments, NamedTableReference dmlTableReference, SqlAstTranslatorFactory sqlAstTranslatorFactory, Expression keyExpression) { + final UpdateStatement sqlAst = new UpdateStatement( + dmlTableReference, + assignments, + new InSubQueryPredicate( keyExpression, idTableSubQuery, false ) + ); + + final JdbcOperationQueryUpdate jdbcUpdate = sqlAstTranslatorFactory + .buildUpdateTranslator( getSessionFactory(), sqlAst ) + .translate( getJdbcParameterBindings(), executionContext.getQueryOptions() ); + + return StandardReactiveJdbcMutationExecutor.INSTANCE + .executeReactive( + jdbcUpdate, + getJdbcParameterBindings(), + executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + ::prepareStatement, + ReactiveUpdateExcutionDelegate::doNothing, + executionContext + ); + } + + private CompletionStage executeInsert( + String targetTableExpression, + NamedTableReference targetTableReference, + Expression targetTableKeyExpression, + Supplier> tableKeyColumnVisitationSupplier, + QuerySpec idTableSubQuery, + List assignments, + SqlAstTranslatorFactory sqlAstTranslatorFactory, + ExecutionContext executionContext) { + + // Execute a query in the form - + // + // insert into (...) + // select ... + // from temptable_ + // where not exists ( + // select 1 + // from dml_ + // where dml_. = temptable_. + // ) + + // Create a new QuerySpec for the "insert source" select query. This + // is mostly a copy of the incoming `idTableSubQuery` along with the + // NOT-EXISTS predicate + final QuerySpec insertSourceSelectQuerySpec = makeInsertSourceSelectQuerySpec( idTableSubQuery ); + + // create the `select 1 ...` sub-query and apply the not-exists predicate + final QuerySpec existsSubQuerySpec = createExistsSubQuerySpec( targetTableExpression, tableKeyColumnVisitationSupplier, idTableSubQuery ); + insertSourceSelectQuerySpec.applyPredicate( + new ExistsPredicate( + existsSubQuerySpec, + true, + getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ) + ) + ); + + // Collect the target column references from the key expressions + final List targetColumnReferences = new ArrayList<>(); + if ( targetTableKeyExpression instanceof SqlTuple ) { + //noinspection unchecked + targetColumnReferences.addAll( (Collection) ( (SqlTuple) targetTableKeyExpression ).getExpressions() ); + } + else { + targetColumnReferences.add( (ColumnReference) targetTableKeyExpression ); + } + + // And transform assignments to target column references and selections + for ( Assignment assignment : assignments ) { + targetColumnReferences.addAll( assignment.getAssignable().getColumnReferences() ); + insertSourceSelectQuerySpec.getSelectClause().addSqlSelection( + new SqlSelectionImpl( 0, -1, assignment.getAssignedValue() ) + ); + } + + final InsertSelectStatement insertSqlAst = new InsertSelectStatement( targetTableReference ); + insertSqlAst.addTargetColumnReferences( targetColumnReferences.toArray( new ColumnReference[0] ) ); + insertSqlAst.setSourceSelectStatement( insertSourceSelectQuerySpec ); + + final JdbcOperationQueryInsert jdbcInsert = sqlAstTranslatorFactory + .buildInsertTranslator( getSessionFactory(), insertSqlAst ) + .translate( getJdbcParameterBindings(), executionContext.getQueryOptions() ); + + return StandardReactiveJdbcMutationExecutor.INSTANCE + .executeReactive( + jdbcInsert, + getJdbcParameterBindings(), + executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + ::prepareStatement, + ReactiveUpdateExcutionDelegate::doNothing, + executionContext + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/collection/internal/ReactiveCollectionDomainResult.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/collection/internal/ReactiveCollectionDomainResult.java new file mode 100644 index 000000000..c52f95385 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/collection/internal/ReactiveCollectionDomainResult.java @@ -0,0 +1,51 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.results.graph.collection.internal; + +import org.hibernate.engine.FetchTiming; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchJoinedImpl; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.Fetchable; +import org.hibernate.sql.results.graph.collection.internal.CollectionDomainResult; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; + +public class ReactiveCollectionDomainResult extends CollectionDomainResult { + + public ReactiveCollectionDomainResult( + NavigablePath loadingPath, + PluralAttributeMapping loadingAttribute, + String resultVariable, + TableGroup tableGroup, + DomainResultCreationState creationState) { + super( loadingPath, loadingAttribute, resultVariable, tableGroup, creationState ); + } + + @Override + public Fetch generateFetchableFetch( + Fetchable fetchable, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + boolean selected, + String resultVariable, + DomainResultCreationState creationState) { + Fetch fetch = super.generateFetchableFetch( + fetchable, + fetchablePath, + fetchTiming, + selected, + resultVariable, + creationState + ); + if ( fetch instanceof EntityFetchJoinedImpl ) { + return new ReactiveEntityFetchJoinedImpl( (EntityFetchJoinedImpl) fetch ); + } + return fetch; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/collection/internal/ReactiveEagerCollectionFetch.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/collection/internal/ReactiveEagerCollectionFetch.java new file mode 100644 index 000000000..e5f69bf0c --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/collection/internal/ReactiveEagerCollectionFetch.java @@ -0,0 +1,52 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.results.graph.collection.internal; + +import org.hibernate.engine.FetchTiming; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchJoinedImpl; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.Fetchable; +import org.hibernate.sql.results.graph.collection.internal.EagerCollectionFetch; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; + +public class ReactiveEagerCollectionFetch extends EagerCollectionFetch { + + public ReactiveEagerCollectionFetch( + NavigablePath fetchedPath, + PluralAttributeMapping fetchedAttribute, + TableGroup collectionTableGroup, + FetchParent fetchParent, + DomainResultCreationState creationState) { + super( fetchedPath, fetchedAttribute, collectionTableGroup, fetchParent, creationState ); + } + + @Override + public Fetch generateFetchableFetch( + Fetchable fetchable, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + boolean selected, + String resultVariable, + DomainResultCreationState creationState) { + Fetch fetch = super.generateFetchableFetch( + fetchable, + fetchablePath, + fetchTiming, + selected, + resultVariable, + creationState + ); + if ( fetch instanceof EntityFetchJoinedImpl ) { + return new ReactiveEntityFetchJoinedImpl( (EntityFetchJoinedImpl) fetch ); + } + return fetch; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/ReactiveAbstractEntityInitializer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/ReactiveAbstractEntityInitializer.java index 237153fde..abbfa6e1f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/ReactiveAbstractEntityInitializer.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/ReactiveAbstractEntityInitializer.java @@ -201,8 +201,8 @@ private CompletionStage initializeEntityInstance(Object toInitialize, RowP return reactiveExtractConcreteTypeStateValues( rowProcessingState ) .thenCompose( entityState -> loop( 0, entityState.length, i -> { if ( entityState[i] instanceof CompletionStage ) { - CompletionStage stateStage = (CompletionStage) entityState[i]; - return stateStage.thenAccept( state -> entityState[i] = state ); + return ( (CompletionStage) entityState[i] ) + .thenAccept( state -> entityState[i] = state ); } return voidFuture(); } ).thenAccept( v -> setResolvedEntityState( entityState ) ) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityFetchJoinedImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityFetchJoinedImpl.java new file mode 100644 index 000000000..fb1c67246 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityFetchJoinedImpl.java @@ -0,0 +1,47 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.results.graph.entity.internal; + +import org.hibernate.LockMode; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.Initializer; +import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; +import org.hibernate.sql.results.graph.entity.EntityValuedFetchable; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; + +public class ReactiveEntityFetchJoinedImpl extends EntityFetchJoinedImpl { + public ReactiveEntityFetchJoinedImpl(EntityFetchJoinedImpl entityFetch) { + super( entityFetch ); + } + + @Override + protected Initializer buildEntityJoinedFetchInitializer( + EntityResultGraphNode resultDescriptor, + EntityValuedFetchable referencedFetchable, + NavigablePath navigablePath, + LockMode lockMode, + NotFoundAction notFoundAction, + DomainResult keyResult, + Fetch identifierFetch, + Fetch discriminatorFetch, + AssemblerCreationState creationState) { + return new ReactiveEntityJoinedFetchInitializer( + resultDescriptor, + referencedFetchable, + navigablePath, + lockMode, + notFoundAction, + keyResult, + identifierFetch, + discriminatorFetch, + creationState + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityJoinedFetchInitializer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityJoinedFetchInitializer.java new file mode 100644 index 000000000..dd0816018 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityJoinedFetchInitializer.java @@ -0,0 +1,132 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.results.graph.entity.internal; + +import java.lang.invoke.MethodHandles; + +import org.hibernate.FetchNotFoundException; +import org.hibernate.LockMode; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.sql.results.graph.entity.ReactiveAbstractEntityInitializer; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultAssembler; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.entity.EntityLoadingLogging; +import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; +import org.hibernate.sql.results.graph.entity.EntityValuedFetchable; +import org.hibernate.sql.results.jdbc.spi.RowProcessingState; + +/** + * @see org.hibernate.sql.results.graph.entity.internal.EntityJoinedFetchInitializer + */ +public class ReactiveEntityJoinedFetchInitializer extends ReactiveAbstractEntityInitializer { + + private static final String CONCRETE_NAME = ReactiveEntityJoinedFetchInitializer.class.getSimpleName(); + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + private final EntityValuedFetchable referencedFetchable; + private final DomainResultAssembler keyAssembler; + private final NotFoundAction notFoundAction; + private final boolean isEnhancedForLazyLoading; + + public ReactiveEntityJoinedFetchInitializer( + EntityResultGraphNode resultDescriptor, + EntityValuedFetchable referencedFetchable, + NavigablePath navigablePath, + LockMode lockMode, + NotFoundAction notFoundAction, + DomainResult keyResult, + Fetch identifierFetch, + Fetch discriminatorFetch, + AssemblerCreationState creationState) { + super( + resultDescriptor, + navigablePath, + lockMode, + identifierFetch, + discriminatorFetch, + null, + creationState + ); + this.referencedFetchable = referencedFetchable; + this.notFoundAction = notFoundAction; + + this.keyAssembler = keyResult == null ? null : keyResult.createResultAssembler( this, creationState ); + + if ( getConcreteDescriptor() != null ) { + this.isEnhancedForLazyLoading = getConcreteDescriptor() + .getBytecodeEnhancementMetadata() + .isEnhancedForLazyLoading(); + } + else { + this.isEnhancedForLazyLoading = false; + } + } + + + @Override + public void resolveKey(RowProcessingState rowProcessingState) { + if ( shouldSkipResolveInstance( rowProcessingState ) ) { + missing = true; + return; + } + + super.resolveKey( rowProcessingState ); + + // super processes the foreign-key target column. here we + // need to also look at the foreign-key value column to check + // for a dangling foreign-key + + if ( keyAssembler != null ) { + final Object fkKeyValue = keyAssembler.assemble( rowProcessingState ); + if ( fkKeyValue != null ) { + if ( isMissing() ) { + if ( notFoundAction != NotFoundAction.IGNORE ) { + throw new FetchNotFoundException( + referencedFetchable.getEntityMappingType().getEntityName(), + fkKeyValue + ); + } + else { + EntityLoadingLogging.ENTITY_LOADING_LOGGER.debugf( + "Ignoring dangling foreign-key due to `@NotFound(IGNORE); association will be null - %s", + getNavigablePath() + ); + } + } + } + } + } + + @Override + protected Object getProxy(PersistenceContext persistenceContext) { + ModelPart referencedModelPart = getInitializedPart(); + if ( referencedModelPart instanceof ToOneAttributeMapping ) { + final boolean unwrapProxy = ( (ToOneAttributeMapping) referencedModelPart ).isUnwrapProxy() && isEnhancedForLazyLoading; + if ( unwrapProxy ) { + return null; + } + } + return super.getProxy( persistenceContext ); + } + + @Override + protected String getSimpleConcreteImplName() { + return CONCRETE_NAME; + } + + @Override + protected boolean isEntityReturn() { + return false; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java index 43c0719e2..e17a25dd8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java @@ -229,6 +229,42 @@ public static U nullFuture(Void unused) { return null; } + public static CompletionStageHandler handle(R result, T throwable) { + return new CompletionStageHandler<>( result, throwable ); + } + + public static class CompletionStageHandler { + + private final R result; + private final T throwable; + + public CompletionStageHandler(R result, T throwable) { + this.result = result; + this.throwable = throwable; + } + + public R getResult() throws T { + if ( throwable == null ) { + return result; + } + throw (T) throwable; + } + + public CompletionStage getResultAsCompletionStage() { + if ( throwable == null ) { + return completedFuture( result ); + } + return failedFuture( throwable ); + } + + /** + * Same as {@link #getResultAsCompletionStage()}, but allows method reference + */ + public CompletionStage getResultAsCompletionStage(Void unused) { + return getResultAsCompletionStage(); + } + } + /** * It represents a loop on an iterator that requires * an index of the current element. diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ORMReactivePersistenceTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ORMReactivePersistenceTest.java index bea1ed2b1..eab6ce30b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ORMReactivePersistenceTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ORMReactivePersistenceTest.java @@ -14,8 +14,6 @@ import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.Configuration; -import org.hibernate.dialect.PostgreSQLDialect; -import org.hibernate.reactive.provider.Settings; import org.hibernate.reactive.testing.DatabaseSelectionRule; import org.junit.After; @@ -28,7 +26,10 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; +import static org.hibernate.reactive.provider.Settings.DIALECT; +import static org.hibernate.reactive.provider.Settings.DRIVER; /** * This test class verifies that data can be persisted and queried on the same database @@ -36,8 +37,9 @@ */ public class ORMReactivePersistenceTest extends BaseReactiveTest { + // DB2: The CompletionStage test throw java.lang.IllegalStateException: Needed to have 6 in buffer... @Rule - public DatabaseSelectionRule rule = DatabaseSelectionRule.runOnlyFor( POSTGRESQL ); + public DatabaseSelectionRule skip = DatabaseSelectionRule.skipTestsFor( DB2 ); private SessionFactory ormFactory; @@ -49,8 +51,8 @@ protected Collection> annotatedEntities() { @Before public void prepareOrmFactory() { Configuration configuration = constructConfiguration(); - configuration.setProperty( Settings.DRIVER, "org.postgresql.Driver" ); - configuration.setProperty( Settings.DIALECT, PostgreSQLDialect.class.getName() ); + configuration.setProperty( DRIVER, dbType().getJdbcDriver() ); + configuration.setProperty( DIALECT, dbType().getDialectClass().getName() ); StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder() .applySettings( configuration.getProperties() ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyTest.java index debb6ace6..52bce76b2 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OneToManyTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; import org.hibernate.Hibernate; @@ -21,6 +22,8 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import static org.assertj.core.api.Assertions.assertThat; + public class OneToManyTest extends BaseReactiveTest { @Override @@ -29,7 +32,7 @@ protected Collection> annotatedEntities() { } @Test - public void test(TestContext context) { + public void testPersistAll(TestContext context) { Book book1 = new Book( "Feersum Endjinn" ); Book book2 = new Book( "Use of Weapons" ); Author author = new Author( "Iain M Banks" ); @@ -40,98 +43,165 @@ public void test(TestContext context) { .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .invoke( a -> context.assertFalse( Hibernate.isInitialized( a.books ) ) ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> context.assertEquals( 2, books.size() ) ) + .invoke( a -> context.assertFalse( Hibernate.isInitialized( a.getBooks() ) ) ) + .chain( a -> session.fetch( a.getBooks() ) ) + .invoke( books -> assertThat( books ).containsExactlyInAnyOrder( book1, book2 ) ) ) ) + ); + } + + @Test + public void testFetchJoinQueryGetSingleResult(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + author.books.add( book1 ); + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .createQuery( "select distinct a from Author a left join fetch a.books", Author.class ) .getSingleResult() - .invoke( a -> context.assertTrue( Hibernate.isInitialized( a.books ) ) ) - .invoke( a -> context.assertEquals( 2, a.books.size() ) ) + .invoke( a -> context.assertTrue( Hibernate.isInitialized( a.getBooks() ) ) ) + .invoke( a -> assertThat( a.getBooks() ).containsExactlyInAnyOrder( book1, book2 ) ) ) ) + ); + } + + @Test + public void testFetchJoinQueryGetSingleResultOrNull(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + author.books.add( book1 ); + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session - .createQuery( "select a from Author a left join fetch a.books", Author.class ) + .createQuery( "select distinct a from Author a left join fetch a.books", Author.class ) .getSingleResultOrNull() - .invoke( a -> context.assertTrue( Hibernate.isInitialized( a.books ) ) ) - .invoke( a -> context.assertEquals( 2, a.books.size() ) ) + .invoke( a -> context.assertTrue( Hibernate.isInitialized( a.getBooks() ) ) ) + .invoke( a -> assertThat( a.getBooks() ).containsExactlyInAnyOrder( book1, book2 ) ) ) ) + ); + } + + @Test + public void testFetchJoinQueryWithNull(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + author.books.add( book1 ); + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .createQuery( "select a from Author a left join fetch a.books where 1=0", Author.class ) .getSingleResultOrNull() .invoke( context::assertNull ) ) ) + ); + } + + @Test + public void testFetchAndRemove(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + author.books.add( book1 ); + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> books.remove( 0 ) ) - ) ) - .chain( () -> getMutinySessionFactory().withTransaction( session -> session - .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> { - context.assertEquals( 1, books.size() ); - context.assertEquals( book2.title, books.get( 0 ).title ); - } ) + .chain( a -> session.fetch( a.getBooks() ) ) + // Remove one book from the fetched collection + .invoke( books -> books.remove( book1 ) ) ) ) + .chain( () -> getMutinySessionFactory() + .withTransaction( session -> session.find( Author.class, author.id ) + .chain( a -> session.fetch( a.getBooks() ) ) + .invoke( books -> assertThat( books ).containsExactly( book2 ) ) + ) + ) + ); + } + + @Test + public void testFetchAndAdd(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + // Only book2 this time + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) + .chain( a -> session.fetch( a.getBooks() ) ) + .invoke( books -> assertThat( books ).containsExactly( book2 ) ) .chain( books -> session.find( Book.class, book1.id ).invoke( books::add ) ) ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .invoke( a -> context.assertFalse( Hibernate.isInitialized( a.books ) ) ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> context.assertEquals( 2, books.size() ) ) - ) ) - .chain( () -> getMutinySessionFactory().withTransaction( session -> session - .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> books.remove( 1 ) ) - ) ) - .chain( () -> getMutinySessionFactory().withTransaction( session -> session - .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> { - context.assertEquals( 1, books.size() ); - context.assertEquals( book1.title, books.get( 0 ).title ); - } ) - ) ) - .chain( () -> getMutinySessionFactory().withTransaction( session -> session - .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) - .chain( books -> session.find( Book.class, book2.id ).invoke( books::add ) ) - ) ) - .chain( () -> getMutinySessionFactory().withTransaction( session -> session - .find( Author.class, author.id ) - .invoke( a -> context.assertFalse( Hibernate.isInitialized( a.books ) ) ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> context.assertEquals( 2, books.size() ) ) + .invoke( a -> context.assertFalse( Hibernate.isInitialized( a.getBooks() ) ) ) + .chain( a -> session.fetch( a.getBooks() ) ) + .invoke( books -> assertThat( books ).containsExactlyInAnyOrder( book1, book2 ) ) ) ) + ); + } + + @Test + public void testSetNullAndFetch(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + author.books.add( book1 ); + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .chain( a -> session.fetch( a.books ) ) - .invoke( books -> books.add( books.remove( 0 ) ) ) + .invoke( a -> a.books = null ) ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .invoke( a -> context.assertFalse( Hibernate.isInitialized( a.books ) ) ) .chain( a -> session.fetch( a.books ) ) - .invoke( books -> context.assertEquals( 2, books.size() ) ) + .invoke( books -> assertThat( books ).isEmpty() ) ) ) + ); + } + + @Test + public void testFetchAndClear(TestContext context) { + Book book1 = new Book( "Feersum Endjinn" ); + Book book2 = new Book( "Use of Weapons" ); + Author author = new Author( "Iain M Banks" ); + author.books.add( book1 ); + author.books.add( book2 ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( book1, book2, author ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) - .invoke( a -> a.books = null ) + .chain( a -> session.fetch( a.getBooks() ) ) + .invoke( List::clear ) ) ) .chain( () -> getMutinySessionFactory().withTransaction( session -> session .find( Author.class, author.id ) .chain( a -> session.fetch( a.books ) ) - .invoke( books -> context.assertTrue( books.isEmpty() ) ) + .invoke( books -> assertThat( books ).isEmpty() ) ) ) ); } + @Entity(name = "Book") @Table(name = "OTMBook") static class Book { @@ -148,6 +218,23 @@ static class Book { @Basic(optional = false) String title; + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Book book = (Book) o; + return Objects.equals( title, book.title ); + } + + @Override + public int hashCode() { + return Objects.hash( title ); + } } @Entity(name = "Author") @@ -169,5 +256,46 @@ public Author() { @OneToMany List books = new ArrayList<>(); + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getBooks() { + return books; + } + + public void setBooks(List books) { + this.books = books; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Author author = (Author) o; + return Objects.equals( name, author.name ); + } + + @Override + public int hashCode() { + return Objects.hash( name ); + } } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UTCTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UTCTest.java index 3c57f4cf3..a4fe19b31 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UTCTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UTCTest.java @@ -9,7 +9,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.time.OffsetTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -139,20 +138,6 @@ public void testOffsetDateTime(TestContext context) { ); } - @Test - public void testOffsetTime(TestContext context) { - thing.offsetTime = OffsetTime - .now( ZoneOffset.ofHours( 7 ) ) - .truncatedTo( ChronoUnit.SECONDS ); - - testField( - context, - "offsetTime", - thing::getOffsetTime, - // Same behavior as ORM - entity -> context.assertEquals( thing.offsetTime, entity.offsetTime.withOffsetSameInstant( ZoneOffset.ofHours( 7 ) ) ) ); - } - @Test public void testZonedDateTime(TestContext context) { final ZoneOffset zoneOffset = ZoneOffset.ofHours( 7 ); @@ -204,9 +189,6 @@ public static class Thing { @Column(name = "offsetDateTimeType") OffsetDateTime offsetDateTime; - @Column(name = "offsetTimeType") - OffsetTime offsetTime; - @Column(name = "zonedDateTimeType") ZonedDateTime zonedDateTime; @@ -231,10 +213,6 @@ public OffsetDateTime getOffsetDateTime() { return offsetDateTime; } - public OffsetTime getOffsetTime() { - return offsetTime; - } - public ZonedDateTime getZonedDateTime() { return zonedDateTime; } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java index 3bd7f11b2..f0d497ee2 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DatabaseConfiguration.java @@ -10,6 +10,15 @@ import java.util.Objects; import java.util.stream.Stream; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; + /** * Contains the common constants that we need for the configuration of databases * during tests. @@ -19,13 +28,13 @@ public class DatabaseConfiguration { public static final boolean USE_DOCKER = Boolean.getBoolean("docker"); public enum DBType { - DB2( DB2Database.INSTANCE, 50000 ), - MYSQL( MySQLDatabase.INSTANCE, 3306 ), - MARIA( MariaDatabase.INSTANCE, 3306, "mariadb" ), - POSTGRESQL( PostgreSQLDatabase.INSTANCE, 5432, "POSTGRES", "PG" ), - COCKROACHDB( CockroachDBDatabase.INSTANCE, 26257, "COCKROACH" ), - SQLSERVER( MSSQLServerDatabase.INSTANCE, 1433, "MSSQL", "MSSQLSERVER" ), - ORACLE( OracleDatabase.INSTANCE, 1521 ); + DB2( DB2Database.INSTANCE, 50000, "com.ibm.db2.jcc.DB2Driver", DB2Dialect.class ), + MYSQL( MySQLDatabase.INSTANCE, 3306, "com.mysql.cj.jdbc.Driver", MySQLDialect.class ), + MARIA( MariaDatabase.INSTANCE, 3306, "org.mariadb.jdbc.Driver", MariaDBDialect.class, "mariadb" ), + POSTGRESQL( PostgreSQLDatabase.INSTANCE, 5432, "org.postgresql.Driver", PostgreSQLDialect.class, "POSTGRES", "PG" ), + COCKROACHDB( CockroachDBDatabase.INSTANCE, 26257, "org.postgresql.Driver", CockroachDialect.class, "COCKROACH" ), + SQLSERVER( MSSQLServerDatabase.INSTANCE, 1433, "com.microsoft.sqlserver.jdbc.SQLServerDriver", SQLServerDialect.class, "MSSQL", "MSSQLSERVER" ), + ORACLE( OracleDatabase.INSTANCE, 1521, "oracle.jdbc.OracleDriver", OracleDialect.class ); private final TestableDatabase configuration; private final int defaultPort; @@ -33,10 +42,17 @@ public enum DBType { // A list of alternative names that can be used to select the db private final String[] aliases; - DBType(TestableDatabase configuration, int defaultPort, String... aliases) { + // JDBC Configuration for when we need to compare what we are doing with ORM + private final String jdbcDriver; + + private final Class dialect; + + DBType(TestableDatabase configuration, int defaultPort, String jdbcDriver, Class dialect, String... aliases) { this.configuration = configuration; this.defaultPort = defaultPort; this.aliases = aliases; + this.dialect = dialect; + this.jdbcDriver = jdbcDriver; } @Override @@ -68,6 +84,14 @@ public static DBType fromString(String dbName) { public int getDefaultPort() { return defaultPort; } + + public String getJdbcDriver() { + return jdbcDriver; + } + + public Class getDialectClass() { + return dialect; + } } public static final String USERNAME = "hreact"; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java index d96b3e633..a1d1a6767 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java @@ -22,6 +22,10 @@ import java.util.TimeZone; import java.util.UUID; +import org.hibernate.type.NumericBooleanConverter; +import org.hibernate.type.TrueFalseConverter; +import org.hibernate.type.YesNoConverter; + import org.testcontainers.containers.MSSQLServerContainer; import static org.hibernate.reactive.containers.DockerImage.imageName; @@ -45,19 +49,18 @@ class MSSQLServerDatabase implements TestableDatabase { expectedDBTypeForClass.put( boolean.class, "bit" ); expectedDBTypeForClass.put( Boolean.class, "bit" ); - // FIXME: [ORM-6] Check if we need alternatives - // expectedDBTypeForClass.put( NumericBooleanType.class, "int" ); - // expectedDBTypeForClass.put( TrueFalseType.class, "char" ); - // expectedDBTypeForClass.put( YesNoType.class, "char" ); - // expectedDBTypeForClass.put( PrimitiveByteArrayTypeDescriptor.class, "varbinary" ); + expectedDBTypeForClass.put( NumericBooleanConverter.class, "int" ); + expectedDBTypeForClass.put( YesNoConverter.class, "char" ); + expectedDBTypeForClass.put( TrueFalseConverter.class, "char" ); + expectedDBTypeForClass.put( byte[].class, "varbinary" ); // expectedDBTypeForClass.put( TextType.class, "text" ); expectedDBTypeForClass.put( int.class, "int" ); expectedDBTypeForClass.put( Integer.class, "int" ); expectedDBTypeForClass.put( long.class, "bigint" ); expectedDBTypeForClass.put( Long.class, "bigint" ); - expectedDBTypeForClass.put( float.class, "float" ); - expectedDBTypeForClass.put( Float.class, "float" ); + expectedDBTypeForClass.put( float.class, "real" ); + expectedDBTypeForClass.put( Float.class, "real" ); expectedDBTypeForClass.put( double.class, "float" ); expectedDBTypeForClass.put( Double.class, "float" ); expectedDBTypeForClass.put( byte.class, "smallint" ); @@ -99,7 +102,7 @@ public String getJdbcUrl() { } private String getRegularJdbcUrl() { - return "jdbc:sqlserver://localhost:1433"; + return "jdbc:sqlserver://localhost:1433;Encrypt=false"; } @Override diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java index 8bf13a687..4b0f5a4a4 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java @@ -21,7 +21,7 @@ class MariaDatabase extends MySQLDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MariaDBContainer maria = new MariaDBContainer<>( imageName( "mariadb", "10.9.3" ) ) + public static final MariaDBContainer maria = new MariaDBContainer<>( imageName( "mariadb", "10.11.2" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java index 9ce4ff625..6afb5c615 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java @@ -30,6 +30,7 @@ import org.hibernate.annotations.Type; import org.hibernate.reactive.BaseReactiveTest; +import org.junit.Ignore; import org.junit.Test; import io.vertx.ext.unit.TestContext; @@ -365,6 +366,7 @@ public void testBigIntegerType(TestContext context) { } @Test + @Ignore // Fail for MSSQL because the value changes before it's saved on the db. This also fails for ORM public void testLocalTimeType(TestContext context) { Basic basic = new Basic(); basic.localTime = LocalTime.now(); diff --git a/podman.md b/podman.md index b779d63c8..bbec3ca93 100644 --- a/podman.md +++ b/podman.md @@ -66,7 +66,7 @@ and schema to run the tests: ``` podman run --rm --name HibernateTestingMariaDB \ -e MYSQL_ROOT_PASSWORD=hreact -e MYSQL_DATABASE=hreact -e MYSQL_USER=hreact -e MYSQL_PASSWORD=hreact \ - -p 3306:3306 mariadb:10.9.3 + -p 3306:3306 mariadb:10.11.2 ``` When the database has started, you can run the tests on MariaDB with: diff --git a/tooling/jbang/MariaDBReactiveTest.java.qute b/tooling/jbang/MariaDBReactiveTest.java.qute index 2151f422b..06d35957e 100755 --- a/tooling/jbang/MariaDBReactiveTest.java.qute +++ b/tooling/jbang/MariaDBReactiveTest.java.qute @@ -69,7 +69,7 @@ public class {baseName} { } @ClassRule - public final static MariaDBContainer database = new MariaDBContainer<>( imageName( "docker.io", "mariadb", "10.9.3" ) ); + public final static MariaDBContainer database = new MariaDBContainer<>( imageName( "docker.io", "mariadb", "10.11.2" ) ); private Mutiny.SessionFactory sessionFactory; diff --git a/tooling/jbang/ReactiveTest.java b/tooling/jbang/ReactiveTest.java index 715bbf574..a7f5ce5ec 100755 --- a/tooling/jbang/ReactiveTest.java +++ b/tooling/jbang/ReactiveTest.java @@ -231,7 +231,7 @@ enum Database { POSTGRESQL( () -> new PostgreSQLContainer( "postgres:14" ) ), MYSQL( () -> new MySQLContainer( "mysql:8.0.32" ) ), DB2( () -> new Db2Container( "docker.io/ibmcom/db2:11.5.8.0" ).acceptLicense() ), - MARIADB( () -> new MariaDBContainer( "mariadb:10.9.3" ) ), + MARIADB( () -> new MariaDBContainer( "mariadb:10.11.2" ) ), COCKROACHDB( () -> new CockroachContainer( "cockroachdb/cockroach:v22.1.9" ) ); private final Supplier> containerSupplier;