diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java index eb665467a2..48aed0b374 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java @@ -22,8 +22,8 @@ import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; -import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.spi.Limit; +import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.spi.NavigablePath; import org.hibernate.query.ResultListTransformer; import org.hibernate.query.TupleTransformer; @@ -38,10 +38,8 @@ import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlAstProcessingState; import org.hibernate.sql.ast.spi.SqlAstQueryPartProcessingState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QueryPart; 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.internal.ImmutableFetchList; @@ -108,17 +106,17 @@ public class LoaderSqlAstCreationState } @Override - public void registerTreat(TableGroup tableGroup, EntityDomainType treatType) { + public void registerTreatedFrom(SqmFrom sqmFrom) { throw new UnsupportedOperationException(); } @Override - public void registerTreatUsage(TableGroup tableGroup, EntityDomainType treatType) { + public void registerFromUsage(SqmFrom sqmFrom, boolean downgradeTreatUses) { throw new UnsupportedOperationException(); } @Override - public Map, Boolean>> getTreatRegistrations() { + public Map, Boolean> getFromRegistrations() { return Collections.emptyMap(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ForeignKeyDescriptor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ForeignKeyDescriptor.java index 313186726a..8395ca403f 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ForeignKeyDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ForeignKeyDescriptor.java @@ -175,6 +175,11 @@ public interface ForeignKeyDescriptor extends VirtualModelPart, ValuedModelPart IntFunction selectableMappingAccess, MappingModelCreationProcess creationProcess); + /** + * Return a copy of this foreign key descriptor with the target part as given by the argument. + */ + ForeignKeyDescriptor withTargetPart(ValuedModelPart targetPart); + AssociationKey getAssociationKey(); boolean hasConstraint(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java index 0d64d7a7dd..005c0fcf30 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java @@ -156,6 +156,20 @@ public class EmbeddedForeignKeyDescriptor implements ForeignKeyDescriptor { this.hasConstraint = original.hasConstraint; } + private EmbeddedForeignKeyDescriptor(EmbeddedForeignKeyDescriptor original, EmbeddableValuedModelPart targetPart) { + this.keyTable = original.keyTable; + this.keySelectableMappings = original.keySelectableMappings; + this.keySide = original.keySide; + this.targetTable = targetPart.getContainingTableExpression(); + this.targetSelectableMappings = targetPart; + this.targetSide = new EmbeddedForeignKeyDescriptorSide( + Nature.TARGET, + targetPart + ); + this.associationKey = original.associationKey; + this.hasConstraint = original.hasConstraint; + } + @Override public String getKeyTable() { return keyTable; @@ -232,6 +246,11 @@ public class EmbeddedForeignKeyDescriptor implements ForeignKeyDescriptor { ); } + @Override + public ForeignKeyDescriptor withTargetPart(ValuedModelPart targetPart) { + return new EmbeddedForeignKeyDescriptor( this, (EmbeddableValuedModelPart) targetPart ); + } + @Override public DomainResult createKeyDomainResult( NavigablePath navigablePath, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index df59a57fb1..65d1551f13 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -59,6 +59,7 @@ import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; @@ -1244,7 +1245,19 @@ public class MappingModelCreationHelper { dialect, creationProcess ); - attributeMapping.setForeignKeyDescriptor( referencedAttributeMapping.getForeignKeyDescriptor() ); + foreignKeyDescriptor = referencedAttributeMapping.getForeignKeyDescriptor(); + } + + final EntityMappingType declaringEntityMapping = attributeMapping.findContainingEntityMapping(); + if ( foreignKeyDescriptor.getTargetPart() instanceof EntityIdentifierMapping + && foreignKeyDescriptor.getTargetPart() != declaringEntityMapping.getIdentifierMapping() ) { + // If the many-to-one refers to the super type, but the one-to-many is defined in a subtype, + // it would be wasteful to reuse the FK descriptor of the many-to-one, + // because that refers to the PK column in the root table. + // Joining such an association then requires that we join the root table + attributeMapping.setForeignKeyDescriptor( + foreignKeyDescriptor.withTargetPart( declaringEntityMapping.getIdentifierMapping() ) + ); } else { attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java index 3ccdda6a71..31d2f338d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java @@ -223,6 +223,17 @@ public class SimpleForeignKeyDescriptor implements ForeignKeyDescriptor, BasicVa ); } + @Override + public ForeignKeyDescriptor withTargetPart(ValuedModelPart targetPart) { + return new SimpleForeignKeyDescriptor( + keySide.getModelPart(), + (BasicValuedModelPart) targetPart, + refersToPrimaryKey, + hasConstraint, + false + ); + } + @Override public DomainResult createKeyDomainResult( NavigablePath navigablePath, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java index c4c07ba7b2..8193b6282f 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityNameUse.java @@ -17,13 +17,14 @@ public final class EntityNameUse { public static final EntityNameUse PROJECTION = new EntityNameUse( UseKind.PROJECTION, true ); public static final EntityNameUse EXPRESSION = new EntityNameUse( UseKind.EXPRESSION, true ); public static final EntityNameUse TREAT = new EntityNameUse( UseKind.TREAT, true ); + public static final EntityNameUse BASE_TREAT = new EntityNameUse( UseKind.TREAT, null ); public static final EntityNameUse OPTIONAL_TREAT = new EntityNameUse( UseKind.TREAT, false ); public static final EntityNameUse FILTER = new EntityNameUse( UseKind.FILTER, true ); private final UseKind kind; - private final boolean requiresRestriction; + private final Boolean requiresRestriction; - private EntityNameUse(UseKind kind, boolean requiresRestriction) { + private EntityNameUse(UseKind kind, Boolean requiresRestriction) { this.kind = kind; this.requiresRestriction = requiresRestriction; } @@ -47,15 +48,27 @@ public final class EntityNameUse { } public boolean requiresRestriction() { - return requiresRestriction; + return requiresRestriction != Boolean.FALSE; } public EntityNameUse stronger(EntityNameUse other) { - return other == null || kind.isStrongerThan( other.kind ) ? this : get( other.kind ); + if ( other == null || kind.isStrongerThan( other.kind ) ) { + return this; + } + if ( kind == other.kind && kind == UseKind.TREAT ) { + return requiresRestriction == null ? other : this; + } + return other.kind.isStrongerThan( kind ) ? other : get( other.kind ); } public EntityNameUse weaker(EntityNameUse other) { - return other == null || kind.isWeakerThan( other.kind ) ? this : get( other.kind ); + if ( other == null || kind.isWeakerThan( other.kind ) ) { + return this; + } + if ( kind == other.kind && kind == UseKind.TREAT ) { + return requiresRestriction == null ? other : this; + } + return other.kind.isWeakerThan( kind ) ? other : get( other.kind ); } public enum UseKind { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java index c72ad0f068..4374e1d61f 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java @@ -616,14 +616,7 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { } private void collectSelectableOwners(LinkedHashMap> selectables) { - if ( isAbstract() ) { - for ( EntityMappingType subMappingType : getSubMappingTypes() ) { - if ( !subMappingType.isAbstract() ) { - ( (UnionSubclassEntityPersister) subMappingType ).collectSelectableOwners( selectables ); - } - } - } - else { + if ( !isAbstract() ) { final SelectableConsumer selectableConsumer = (i, selectable) -> { Map selectableMapping = selectables.computeIfAbsent( selectable.getSelectionExpression(), @@ -647,11 +640,6 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { for ( int i = 0; i < size; i++ ) { attributeMappings.get( i ).forEachSelectable( selectableConsumer ); } - for ( EntityMappingType subMappingType : getSubMappingTypes() ) { - if ( !subMappingType.isAbstract() ) { - ( (UnionSubclassEntityPersister) subMappingType ).collectSelectableOwners( selectables ); - } - } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index d6e5d1a5fb..22801790e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -65,6 +65,9 @@ import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.Bindable; import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.DiscriminatorConverter; +import org.hibernate.metamodel.mapping.DiscriminatorMapping; +import org.hibernate.metamodel.mapping.DiscriminatorValueDetails; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; @@ -106,6 +109,7 @@ import org.hibernate.metamodel.model.domain.internal.CompositeSqmPathSource; import org.hibernate.metamodel.model.domain.internal.EntityDiscriminatorSqmPath; import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityNameUse; import org.hibernate.persister.entity.EntityPersister; @@ -496,6 +500,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base private final Stack queryTransformers = new StandardStack<>( List.class ); private boolean inTypeInference; private boolean inImpliedResultTypeInference; + private boolean inNestedContext; private Supplier> functionImpliedResultTypeAccess; private SqmByUnit appliedByUnit; @@ -1971,6 +1976,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base final Predicate originalAdditionalRestrictions = additionalRestrictions; additionalRestrictions = null; + final boolean oldInNestedContext = inNestedContext; + inNestedContext = false; final boolean trackAliasedNodePositions; if ( trackSelectionsForGroup ) { @@ -2054,24 +2061,15 @@ public abstract class BaseSqmToSqlAstConverter extends Base applyCollectionFilterPredicates( sqlQuerySpec ); } - // Look for treat registrations that have never been used in the query. - // These treats become "global" i.e. we need to apply filtering for all subtypes. - // This tracking is necessary to differentiate between the HQLs - // - from Root r join treat(r.attribute as Subtype) a where a.id = 1 or 1=1 - // - from Root r join r.attribute a where treat(a as Subtype).id = 1 or 1=1 - for ( Map.Entry, Boolean>> entry : processingState.getTreatRegistrations().entrySet() ) { - final TableGroup actualTableGroup = entry.getKey(); - final Map, Boolean> treatUses = entry.getValue(); - for ( Map.Entry, Boolean> treatUseEntry : treatUses.entrySet() ) { - final EntityDomainType treatTargetType = treatUseEntry.getKey(); - if ( !treatUseEntry.getValue() ) { - // The treat registration was not used in the query - registerEntityNameUsage( - actualTableGroup, - EntityNameUse.TREAT, - treatTargetType.getHibernateEntityName() - ); - } + // Look for treated SqmFrom registrations that have uses of the untreated SqmFrom. + // These SqmFrom nodes are then not treat-joined but rather treated only in expressions + // Consider the following two queries. The latter also uses the untreated SqmFrom + // and hence has different semantics i.e. the treat is not filtering, but just applies where it is used + // - select a.id from Root r join treat(r.attribute as Subtype) a where a.id = 1 + // - select a.id from Root r join r.attribute a where treat(a as Subtype).id = 1 + for ( Map.Entry, Boolean> entry : processingState.getFromRegistrations().entrySet() ) { + if ( entry.getValue() == Boolean.TRUE ) { + downgradeTreatUses( getFromClauseIndex().getTableGroup( entry.getKey().getNavigablePath() ) ); } } @@ -2090,6 +2088,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base sqlQuerySpec.applyPredicate( additionalRestrictions ); } additionalRestrictions = originalAdditionalRestrictions; + inNestedContext = oldInNestedContext; popProcessingStateStack(); queryTransformers.pop(); currentSqmQueryPart = sqmQueryPart; @@ -2097,6 +2096,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } + private void downgradeTreatUses(TableGroup tableGroup) { + final Map entityNameUses = tableGroupEntityNameUses.get( tableGroup ); + if ( entityNameUses != null ) { + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue().getKind() == EntityNameUse.UseKind.TREAT ) { + entry.setValue( EntityNameUse.EXPRESSION ); + } + } + } + } + protected void visitOrderByOffsetAndFetch(SqmQueryPart sqmQueryPart, QueryPart sqlQueryPart) { if ( sqmQueryPart.getOrderByClause() != null ) { currentClauseStack.push( Clause.ORDER ); @@ -2865,14 +2875,27 @@ public abstract class BaseSqmToSqlAstConverter extends Base final EntityDomainType treatedType; if ( projectedPath instanceof SqmTreatedPath ) { treatedType = ( (SqmTreatedPath) projectedPath ).getTreatTarget(); - registerEntityNameUsage( tableGroup, EntityNameUse.TREAT, treatedType.getHibernateEntityName() ); + registerEntityNameUsage( tableGroup, EntityNameUse.TREAT, treatedType.getHibernateEntityName(), true ); - // Register that this treat was used somewhere - ((SqlAstQueryPartProcessingState) getCurrentProcessingState()).registerTreatUsage( tableGroup, treatedType ); + if ( projectedPath instanceof SqmFrom ) { + // Register that the TREAT uses for the SqmFrom node may not be downgraded + ( (SqlAstQueryPartProcessingState) getCurrentProcessingState() ).registerFromUsage( + (SqmFrom) ( (SqmTreatedPath) projectedPath ).getWrappedPath(), + false + ); + } } else if ( projectedPath.getNodeType().getSqmPathType() instanceof EntityDomainType ) { treatedType = (EntityDomainType) projectedPath.getNodeType().getSqmPathType(); - registerEntityNameUsage( tableGroup, EntityNameUse.PROJECTION, treatedType.getHibernateEntityName() ); + registerEntityNameUsage( tableGroup, EntityNameUse.PROJECTION, treatedType.getHibernateEntityName(), true ); + + if ( projectedPath instanceof SqmFrom ) { + // Register that the TREAT uses for the SqmFrom node may not be downgraded + ( (SqlAstQueryPartProcessingState) getCurrentProcessingState() ).registerFromUsage( + (SqmFrom) projectedPath, + true + ); + } } } @@ -2883,59 +2906,76 @@ public abstract class BaseSqmToSqlAstConverter extends Base * it will instead register a {@link EntityNameUse#TREAT} for the treated type. */ private void registerPathAttributeEntityNameUsage(SqmPath sqmPath, TableGroup tableGroup) { + final SqmPath parentPath = sqmPath.getLhs(); + final SqlAstProcessingState processingState = getCurrentProcessingState(); + if ( processingState instanceof SqlAstQueryPartProcessingState ) { + if ( parentPath instanceof SqmFrom ) { + ( (SqlAstQueryPartProcessingState) processingState ).registerFromUsage( + (SqmFrom) parentPath, + true + ); + } + if ( sqmPath instanceof SqmFrom ) { + ( (SqlAstQueryPartProcessingState) processingState ).registerFromUsage( + (SqmFrom) sqmPath, + true + ); + } + } final SqmPathSource resolvedModel; if ( !( sqmPath instanceof SqmTreatedPath ) && tableGroup.getModelPart().getPartMappingType() instanceof EntityMappingType && ( resolvedModel = sqmPath.getResolvedModel() ) instanceof PersistentAttribute ) { - final SqmPath parentPath = sqmPath.getLhs(); final String attributeName = resolvedModel.getPathName(); final EntityMappingType entityType = (EntityMappingType) tableGroup.getModelPart().getPartMappingType(); final EntityMappingType parentType; - final EntityNameUse entityNameUse; - final String treatedEntityName; if ( parentPath instanceof SqmTreatedPath ) { // A treated attribute usage i.e. `treat(alias as Subtype).attribute = 1` final EntityDomainType treatTarget = ( (SqmTreatedPath) parentPath ).getTreatTarget(); - final AbstractEntityPersister persister = (AbstractEntityPersister) creationContext.getMappingMetamodel() - .getEntityDescriptor( treatTarget.getHibernateEntityName() ); - ( (SqlAstQueryPartProcessingState) getCurrentProcessingState() ).registerTreatUsage( - tableGroup, - treatTarget - ); - treatedEntityName = treatTarget.getHibernateEntityName(); - parentType = persister; + parentType = creationContext.getMappingMetamodel() + .getEntityDescriptor( treatTarget.getHibernateEntityName() ); // The following is an optimization to avoid rendering treat conditions into predicates. // Imagine an HQL predicate like `treat(alias as Subtype).attribute is null or alias.name = '...'`. - // If the column for `attribute` is not "shared", meaning that the column is valid only for one subtype, - // then we can safely skip adding the `type(alias) = Subtype and ...` condition to the SQL. + // If the `attribute` is basic, we will render a case wrapper around the column expression + // and hence we can safely skip adding the `type(alias) = Subtype and ...` condition to the SQL. final ModelPart subPart = parentType.findSubPart( attributeName ); + final EntityNameUse entityNameUse; // We only apply this optimization for basic valued model parts for now - if ( subPart instanceof BasicValuedModelPart - && !persister.isSharedColumn( ( (BasicValuedModelPart) subPart ).getSelectionExpression() ) ) { + if ( subPart instanceof BasicValuedModelPart ) { entityNameUse = EntityNameUse.OPTIONAL_TREAT; } else { entityNameUse = EntityNameUse.TREAT; } + registerEntityNameUsage( + tableGroup, + entityNameUse, + treatTarget.getHibernateEntityName() + ); } else { // A simple attribute usage e.g. `alias.attribute = 1` - treatedEntityName = null; parentType = entityType; - entityNameUse = EntityNameUse.EXPRESSION; } final AttributeMapping attributeMapping = parentType.findAttributeMapping( attributeName ); if ( attributeMapping == null ) { - if ( attributeName.equals( parentType.getIdentifierMapping().getPartName() ) ) { - // Until HHH-16571 is fixed, we must register an entity name use for the root entity descriptor name + if ( attributeName.equals( parentType.getIdentifierMapping().getAttributeName() ) ) { + if ( parentType.getIdentifierMapping() instanceof EmbeddableValuedModelPart ) { + // Until HHH-16571 is fixed, we must also register an entity name use for the root entity descriptor name + registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + parentType.getRootEntityDescriptor().getEntityName() + ); + } registerEntityNameUsage( tableGroup, - entityNameUse, - parentType.getRootEntityDescriptor().getEntityName() + EntityNameUse.EXPRESSION, + parentType.getEntityName() ); } else { @@ -2944,7 +2984,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base // Register entity name usages for all subtypes that declare the attribute with the same name then for ( EntityMappingType subMappingType : parentType.getSubMappingTypes() ) { if ( subMappingType.findDeclaredAttributeMapping( attributeName ) != null ) { - registerEntityNameUsage( tableGroup, entityNameUse, subMappingType.getEntityName() ); + registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, subMappingType.getEntityName() ); } } } @@ -2952,10 +2992,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base else { registerEntityNameUsage( tableGroup, - entityNameUse, - treatedEntityName == null - ? attributeMapping.findContainingEntityMapping().getEntityName() - : treatedEntityName + EntityNameUse.EXPRESSION, + attributeMapping.findContainingEntityMapping().getEntityName() ); } } @@ -2966,6 +3004,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base TableGroup tableGroup, EntityNameUse entityNameUse, String hibernateEntityName) { + registerEntityNameUsage( tableGroup, entityNameUse, hibernateEntityName, false ); + } + + private void registerEntityNameUsage( + TableGroup tableGroup, + EntityNameUse entityNameUse, + String hibernateEntityName, + boolean projection) { final AbstractEntityPersister persister = (AbstractEntityPersister) creationContext.getSessionFactory() .getRuntimeMetamodels() .getMappingMetamodel() @@ -2975,19 +3021,24 @@ public abstract class BaseSqmToSqlAstConverter extends Base } final TableGroup actualTableGroup; final EntityNameUse finalEntityNameUse; - if ( tableGroup instanceof PluralTableGroup ) { - actualTableGroup = ( (PluralTableGroup) tableGroup ).getElementTableGroup(); - finalEntityNameUse = entityNameUse; - } - else if ( tableGroup instanceof CorrelatedTableGroup ) { + if ( tableGroup instanceof CorrelatedTableGroup ) { actualTableGroup = ( (CorrelatedTableGroup) tableGroup ).getCorrelatedTableGroup(); // For correlated table groups we can't apply filters, // as the context is in which the use happens may only affect the result of the subquery finalEntityNameUse = entityNameUse == EntityNameUse.EXPRESSION ? entityNameUse : EntityNameUse.PROJECTION; } else { - actualTableGroup = tableGroup; - finalEntityNameUse = entityNameUse; + if ( tableGroup instanceof PluralTableGroup ) { + actualTableGroup = ( (PluralTableGroup) tableGroup ).getElementTableGroup(); + } + else { + actualTableGroup = tableGroup; + } + finalEntityNameUse = entityNameUse == EntityNameUse.EXPRESSION + || entityNameUse == EntityNameUse.PROJECTION + || contextAllowsTreatOrFilterEntityNameUse() + ? entityNameUse + : EntityNameUse.EXPRESSION; } final Map entityNameUses = tableGroupEntityNameUses.computeIfAbsent( actualTableGroup, @@ -2999,11 +3050,12 @@ public abstract class BaseSqmToSqlAstConverter extends Base ); // Resolve the table reference for all types which we register an entity name use for - actualTableGroup.resolveTableReference( null, persister.getTableName() ); + if ( actualTableGroup.isInitialized() ) { + actualTableGroup.resolveTableReference( null, persister.getTableName() ); + } - if ( finalEntityNameUse == EntityNameUse.PROJECTION ) { - // For projections also register uses of all super and subtypes, - // as well as resolve the respective table references + final EntityNameUse.UseKind useKind = finalEntityNameUse.getKind(); + if ( projection ) { EntityMappingType superMappingType = persister; while ( ( superMappingType = superMappingType.getSuperMappingType() ) != null ) { entityNameUses.putIfAbsent( superMappingType.getEntityName(), EntityNameUse.PROJECTION ); @@ -3012,22 +3064,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base ( (AbstractEntityPersister) superMappingType.getEntityPersister() ).getTableName() ); } - - for ( String subclassEntityName : persister.getSubclassEntityNames() ) { - entityNameUses.putIfAbsent( subclassEntityName, EntityNameUse.PROJECTION ); - } - final int subclassTableSpan = persister.getSubclassTableSpan(); - for ( int i = 0; i < subclassTableSpan; i++ ) { - actualTableGroup.resolveTableReference( null, persister.getSubclassTableName( i ) ); - } } - else if ( finalEntityNameUse == EntityNameUse.TREAT ) { + if ( useKind == EntityNameUse.UseKind.TREAT || useKind == EntityNameUse.UseKind.PROJECTION ) { // If we encounter a treat use, we also want register the use for all subtypes. // We do this here to not have to expand entity name uses during pruning later on for ( EntityMappingType subType : persister.getSubMappingTypes() ) { entityNameUses.compute( subType.getEntityName(), - (s, existingUse) -> entityNameUse.stronger( existingUse ) + (s, existingUse) -> finalEntityNameUse.stronger( existingUse ) ); actualTableGroup.resolveTableReference( null, @@ -3037,6 +3081,21 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } + private boolean contextAllowsTreatOrFilterEntityNameUse() { + final Clause currentClause = getCurrentClauseStack().getCurrent(); + switch ( currentClause ) { + case SET: + case FROM: + case GROUP: + case HAVING: + case WHERE: + // A TREAT or FILTER EntityNameUse is only allowed in these clauses, + // but only if it's not in a nested context + return !inNestedContext; + } + return false; + } + protected void registerTypeUsage(EntityDiscriminatorSqmPath path) { registerTypeUsage( getFromClauseAccess().getTableGroup( path.getNavigablePath().getParent() ) ); } @@ -3100,10 +3159,18 @@ public abstract class BaseSqmToSqlAstConverter extends Base final List> sqmTreats = sqmFrom.getSqmTreats(); if ( !sqmTreats.isEmpty() ) { final SqlAstQueryPartProcessingState queryPartProcessingState = (SqlAstQueryPartProcessingState) getCurrentProcessingState(); + queryPartProcessingState.registerTreatedFrom( sqmFrom ); + // If a SqmFrom is used anywhere even though treats exists, + // the treats are context dependent and hence we need to downgrade TREAT entity uses to EXPRESSION. + // Treat expressions will be protected via predicates or case when expressions, + // but we may not filter rows based on the TREAT entity uses. + if ( lhsTableGroup.hasRealJoins() ) {//|| sqmFrom instanceof SqmRoot ) { + queryPartProcessingState.registerFromUsage( sqmFrom, true ); + } for ( SqmFrom sqmTreat : sqmTreats ) { final TableGroup actualTableGroup = getActualTableGroup( lhsTableGroup, sqmTreat ); - // We don't know the context yet in which a treat is used, so we have to register them first and track the usage - queryPartProcessingState.registerTreat( actualTableGroup, ( (SqmTreatedPath) sqmTreat ).getTreatTarget() ); + // We don't know the context yet in which a treat is used, so we have to register base treats and track the usage + registerEntityNameUsage( actualTableGroup, EntityNameUse.BASE_TREAT, ( (SqmTreatedPath) sqmTreat ).getTreatTarget().getHibernateEntityName() ); consumeExplicitJoins( sqmTreat, actualTableGroup ); } } @@ -3266,6 +3333,16 @@ public abstract class BaseSqmToSqlAstConverter extends Base joinedTableGroupJoin.applyPredicate( visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ) ); currentlyProcessingJoin = oldJoin; } + // Since joins on treated paths will never cause table pruning, we need to add a join condition for the treat + if ( sqmJoin.getLhs() instanceof SqmTreatedPath ) { + final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmJoin.getLhs(); + joinedTableGroupJoin.applyPredicate( + createTreatTypeRestriction( + treatedPath.getWrappedPath(), + treatedPath.getTreatTarget() + ) + ); + } if ( transitive ) { consumeExplicitJoins( sqmJoin, joinedTableGroup ); @@ -3593,24 +3670,15 @@ public abstract class BaseSqmToSqlAstConverter extends Base if ( !( path instanceof SqmEntityValuedSimplePath || path instanceof SqmEmbeddedValuedSimplePath - || path instanceof SqmAnyValuedSimplePath ) ) { + || path instanceof SqmAnyValuedSimplePath + || path instanceof SqmTreatedPath ) ) { // Since this is a selection, we must create a table group for the path as a DomainResult will be created // But only create it for paths that are not handled by #prepareReusablePath anyway - final NavigablePath navigablePath; - if ( path instanceof SqmTreatedRoot ) { - navigablePath = ( (SqmTreatedRoot) path ).getWrappedPath().getNavigablePath(); - } - else { - navigablePath = path.getLhs().getNavigablePath(); - } final TableGroup createdTableGroup = createTableGroup( - getActualTableGroup( fromClauseIndex.getTableGroup( navigablePath ), path ), + getActualTableGroup( fromClauseIndex.getTableGroup( path.getLhs().getNavigablePath() ), path ), path ); if ( createdTableGroup != null ) { - if ( path instanceof SqmTreatedPath ) { - fromClauseIndex.register( path, createdTableGroup ); - } registerEntityNameProjectionUsage( path, createdTableGroup ); } } @@ -4915,30 +4983,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base // so we instead add the type restriction predicate as conjunct // by registering the treat into tableGroupEntityNameUses final String treatedName = treatedPath.getTreatTarget().getHibernateEntityName(); - final EntityPersister entityDescriptor = domainModel.findEntityDescriptor( treatedName ); final TableGroup tableGroup = getFromClauseIndex().findTableGroup( wrappedPath.getNavigablePath() ); - final TableGroup actualTableGroup; - if ( tableGroup instanceof PluralTableGroup ) { - final CollectionPart.Nature nature = CollectionPart.Nature.fromName( path.getNavigablePath().getLocalName() ); - actualTableGroup = ( (PluralTableGroup) tableGroup ).getTableGroup( - nature == null - ? CollectionPart.Nature.ELEMENT - : nature - ); - } - else { - actualTableGroup = tableGroup; - } - final Map entityNameUses = tableGroupEntityNameUses.computeIfAbsent( - actualTableGroup, - p -> new HashMap<>( 1 ) - ); - for ( String subclassEntityName : entityDescriptor.getSubclassEntityNames() ) { - entityNameUses.compute( - subclassEntityName, - (s, existingUse) -> EntityNameUse.TREAT.stronger( existingUse ) - ); - } + registerEntityNameUsage( tableGroup, EntityNameUse.TREAT, treatedName ); return expression; } final BasicValuedPathInterpretation basicPath = (BasicValuedPathInterpretation) expression; @@ -5041,7 +5087,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base private Set determineEntityNamesForTreatTypeRestriction( EntityMappingType partMappingType, Map entityNameUses) { - final Set entityNameUsesSet = entityNameUses.keySet(); + final Set entityNameUsesSet = new HashSet<>( entityNameUses.size() ); + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue() == EntityNameUse.PROJECTION ) { + continue; + } + entityNameUsesSet.add( entry.getKey() ); + } + if ( entityNameUsesSet.containsAll( partMappingType.getSubclassEntityNames() ) ) { // No need to create a restriction if all subclasses are used return Collections.emptySet(); @@ -5051,7 +5104,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base return Collections.emptySet(); } final String baseEntityNameToAdd; - if ( entityNameUses.containsKey( partMappingType.getEntityName() ) ) { + if ( entityNameUsesSet.contains( partMappingType.getEntityName() ) ) { if ( !partMappingType.isAbstract() ) { baseEntityNameToAdd = partMappingType.getEntityName(); } @@ -5206,7 +5259,39 @@ public abstract class BaseSqmToSqlAstConverter extends Base final MappingModelExpressible inferableExpressible = getInferredValueMapping(); - if ( inferableExpressible instanceof BasicValuedMapping ) { + if ( inferableExpressible instanceof DiscriminatorMapping ) { + final MappingMetamodelImplementor mappingMetamodel = creationContext.getSessionFactory().getMappingMetamodel(); + final Object literalValue = literal.getLiteralValue(); + final EntityPersister entityDescriptor; + if ( literalValue instanceof Class ) { + entityDescriptor = mappingMetamodel.findEntityDescriptor( (Class) literalValue ); + } + else { + final DiscriminatorMapping discriminatorMapping = (DiscriminatorMapping) inferableExpressible; + //noinspection unchecked + final DiscriminatorConverter valueConverter = (DiscriminatorConverter) discriminatorMapping.getValueConverter(); + final DiscriminatorValueDetails discriminatorDetails; + if ( valueConverter.getDomainJavaType().isInstance( literalValue ) ) { + discriminatorDetails = valueConverter.getDetailsForDiscriminatorValue( literalValue ); + } + else if ( valueConverter.getRelationalJavaType().isInstance( literalValue ) ) { + discriminatorDetails = valueConverter.getDetailsForRelationalForm( literalValue ); + } + else { + // Special case when passing the discriminator value as e.g. string literal, + // but the expected relational type is Character. + // In this case, we use wrap to transform the value to the correct type + final Object relationalForm = valueConverter.getRelationalJavaType().wrap( + literalValue, + creationContext.getSessionFactory().getWrapperOptions() + ); + discriminatorDetails = valueConverter.getDetailsForRelationalForm( relationalForm ); + } + entityDescriptor = discriminatorDetails.getIndicatedEntity().getEntityPersister(); + } + return new EntityTypeLiteral( entityDescriptor ); + } + else if ( inferableExpressible instanceof BasicValuedMapping ) { final BasicValuedMapping basicValuedMapping = (BasicValuedMapping) inferableExpressible; final BasicValueConverter valueConverter = basicValuedMapping.getJdbcMapping().getValueConverter(); if ( valueConverter != null ) { @@ -5219,15 +5304,6 @@ public abstract class BaseSqmToSqlAstConverter extends Base else if ( valueConverter.getRelationalJavaType().isInstance( value ) ) { sqlLiteralValue = value; } - else if ( basicValuedMapping instanceof EntityDiscriminatorMapping ) { - // Special case when passing the discriminator value as e.g. string literal, - // but the expected relational type is Character. - // In this case, we use wrap to transform the value to the correct type - sqlLiteralValue = valueConverter.getRelationalJavaType().wrap( - value, - creationContext.getSessionFactory().getWrapperOptions() - ); - } // In HQL, number literals might not match the relational java type exactly, // so we allow coercion between the number types else if ( Number.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) @@ -5927,6 +6003,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public Expression visitFunction(SqmFunction sqmFunction) { + final boolean oldInNestedContext = inNestedContext; + inNestedContext = true; final Supplier> oldFunctionImpliedResultTypeAccess = functionImpliedResultTypeAccess; functionImpliedResultTypeAccess = inferrableTypeAccessStack.getCurrent(); inferrableTypeAccessStack.push( () -> null ); @@ -5936,6 +6014,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base finally { inferrableTypeAccessStack.pop(); functionImpliedResultTypeAccess = oldFunctionImpliedResultTypeAccess; + inNestedContext = oldInNestedContext; } } @@ -6577,6 +6656,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base public CaseSimpleExpression visitSimpleCaseExpression(SqmCaseSimple expression) { final List whenFragments = new ArrayList<>( expression.getWhenFragments().size() ); final Supplier> inferenceSupplier = inferrableTypeAccessStack.getCurrent(); + final boolean oldInNestedContext = inNestedContext; + + inNestedContext = true; inferrableTypeAccessStack.push( () -> { for ( SqmCaseSimple.WhenFragment whenFragment : expression.getWhenFragments() ) { @@ -6623,6 +6705,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base resolved = (MappingModelExpressible) highestPrecedence( resolved, otherwise.getExpressionType() ); } + inNestedContext = oldInNestedContext; return new CaseSimpleExpression( resolved, fixture, @@ -6635,6 +6718,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base public CaseSearchedExpression visitSearchedCaseExpression(SqmCaseSearched expression) { final List whenFragments = new ArrayList<>( expression.getWhenFragments().size() ); final Supplier> inferenceSupplier = inferrableTypeAccessStack.getCurrent(); + final boolean oldInNestedContext = inNestedContext; + + inNestedContext = true; MappingModelExpressible resolved = determineCurrentExpressible( expression ); Expression otherwise = null; @@ -6663,6 +6749,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base resolved = (MappingModelExpressible) highestPrecedence( resolved, otherwise.getExpressionType() ); } + inNestedContext = oldInNestedContext; return new CaseSearchedExpression( resolved, whenFragments, otherwise ); } @@ -6874,37 +6961,164 @@ public abstract class BaseSqmToSqlAstConverter extends Base new ArrayList<>( predicate.getPredicates().size() ), getBooleanType() ); - final Map> originalConjunctTableGroupTreatUsages; + final Map> previousTableGroupEntityNameUses; if ( tableGroupEntityNameUses.isEmpty() ) { - originalConjunctTableGroupTreatUsages = null; + previousTableGroupEntityNameUses = null; } else { - originalConjunctTableGroupTreatUsages = new IdentityHashMap<>( tableGroupEntityNameUses ); + previousTableGroupEntityNameUses = new IdentityHashMap<>( tableGroupEntityNameUses ); } - Map>[] conjunctTreatUsagesArray = null; - Map> conjunctTreatUsagesUnion = null; + Map>[] disjunctEntityNameUsesArray = null; + Map> entityNameUsesToPropagate = null; + List treatedTableGroups = null; + List filteredTableGroups = null; List predicates = predicate.getPredicates(); for ( int i = 0; i < predicates.size(); i++ ) { tableGroupEntityNameUses.clear(); disjunction.add( (Predicate) predicates.get( i ).accept( this ) ); if ( !tableGroupEntityNameUses.isEmpty() ) { - if ( conjunctTreatUsagesArray == null ) { - conjunctTreatUsagesArray = new Map[predicate.getPredicates().size()]; - conjunctTreatUsagesUnion = new IdentityHashMap<>(); + if ( disjunctEntityNameUsesArray == null ) { + disjunctEntityNameUsesArray = new Map[predicate.getPredicates().size()]; + entityNameUsesToPropagate = previousTableGroupEntityNameUses == null + ? new IdentityHashMap<>() + : previousTableGroupEntityNameUses; } + if ( i == 0 ) { + // Collect the table groups for which filters are registered + for ( Map.Entry> entry : tableGroupEntityNameUses.entrySet() ) { + if ( entry.getValue().containsValue( EntityNameUse.TREAT ) || entry.getValue().containsValue( EntityNameUse.OPTIONAL_TREAT ) ) { + if ( treatedTableGroups == null ) { + treatedTableGroups = new ArrayList<>( 1 ); + } + treatedTableGroups.add( entry.getKey() ); + } + if ( entry.getValue().containsValue( EntityNameUse.FILTER ) ) { + if ( filteredTableGroups == null ) { + filteredTableGroups = new ArrayList<>( 1 ); + } + filteredTableGroups.add( entry.getKey() ); + } + } + } + // Create a copy of the filtered table groups from which we remove + final List missingTableGroupFilters; + if ( filteredTableGroups == null || i == 0 ) { + missingTableGroupFilters = Collections.emptyList(); + } + else { + missingTableGroupFilters = new ArrayList<>( filteredTableGroups ); + } + final List missingTableGroupTreats; + if ( treatedTableGroups == null || i == 0 ) { + missingTableGroupTreats = Collections.emptyList(); + } + else { + missingTableGroupTreats = new ArrayList<>( treatedTableGroups ); + } + // Compute the entity name uses to propagate to the parent context + // If every disjunct contains a FILTER, we can merge the filters + // If every disjunct contains a TREAT, we can merge the treats + // Otherwise, we downgrade the entity name uses to expression uses for ( Map.Entry> entry : tableGroupEntityNameUses.entrySet() ) { - final Map entityNameUses = conjunctTreatUsagesUnion.computeIfAbsent( - entry.getKey(), + final TableGroup tableGroup = entry.getKey(); + final Map entityNameUses = entityNameUsesToPropagate.computeIfAbsent( + tableGroup, k -> new HashMap<>() ); - entityNameUses.putAll( entry.getValue() ); + final boolean downgradeTreatUses; + final boolean downgradeFilterUses; + if ( i == 0 ) { + // Never downgrade the treat uses of the first disjunct + downgradeTreatUses = false; + // Never downgrade the filter uses of the first disjunct + downgradeFilterUses = false; + } + else { + // If the table group is not part of the missingTableGroupTreats, we must downgrade treat uses + downgradeTreatUses = !missingTableGroupTreats.contains( tableGroup ); + // If the table group is not part of the missingTableGroupFilters, we must downgrade filter uses + downgradeFilterUses = !missingTableGroupFilters.contains( tableGroup ); + } + for ( Map.Entry useEntry : entry.getValue().entrySet() ) { + final EntityNameUse.UseKind useKind = useEntry.getValue().getKind(); + final EntityNameUse currentUseKind = entityNameUses.get( useEntry.getKey() ); + final EntityNameUse unionEntityNameUse; + if ( useKind == EntityNameUse.UseKind.TREAT ) { + if ( downgradeTreatUses ) { + unionEntityNameUse = EntityNameUse.EXPRESSION; + } + else { + unionEntityNameUse = useEntry.getValue(); + missingTableGroupTreats.remove( tableGroup ); + } + } + else if ( useKind == EntityNameUse.UseKind.FILTER ) { + if ( downgradeFilterUses ) { + unionEntityNameUse = EntityNameUse.EXPRESSION; + } + else { + unionEntityNameUse = useEntry.getValue(); + missingTableGroupFilters.remove( tableGroup ); + } + } + else { + unionEntityNameUse = useEntry.getValue(); + } + if ( currentUseKind == null ) { + entityNameUses.put( useEntry.getKey(), unionEntityNameUse ); + } + else { + entityNameUses.put( useEntry.getKey(), unionEntityNameUse.stronger( currentUseKind ) ); + } + } + } + // Downgrade entity name uses for table groups that haven't been filtered in this disjunct + for ( TableGroup missingTableGroupTreat : missingTableGroupTreats ) { + treatedTableGroups.remove( missingTableGroupTreat ); + final Map entityNameUses = entityNameUsesToPropagate.get( missingTableGroupTreat ); + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue().getKind() == EntityNameUse.UseKind.TREAT ) { + entry.setValue( EntityNameUse.EXPRESSION ); + } + } + } + for ( TableGroup missingTableGroupFilter : missingTableGroupFilters ) { + filteredTableGroups.remove( missingTableGroupFilter ); + final Map entityNameUses = entityNameUsesToPropagate.get( missingTableGroupFilter ); + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue() == EntityNameUse.FILTER ) { + entry.setValue( EntityNameUse.EXPRESSION ); + } + } + } + disjunctEntityNameUsesArray[i] = new IdentityHashMap<>( tableGroupEntityNameUses ); + } + else { + if ( treatedTableGroups != null ) { + treatedTableGroups = null; + for ( Map entityNameUses : entityNameUsesToPropagate.values() ) { + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue().getKind() == EntityNameUse.UseKind.TREAT ) { + entry.setValue( EntityNameUse.EXPRESSION ); + } + } + } + } + if ( filteredTableGroups != null ) { + filteredTableGroups = null; + for ( Map entityNameUses : entityNameUsesToPropagate.values() ) { + for ( Map.Entry entry : entityNameUses.entrySet() ) { + if ( entry.getValue() == EntityNameUse.FILTER ) { + entry.setValue( EntityNameUse.EXPRESSION ); + } + } + } } - conjunctTreatUsagesArray[i] = new IdentityHashMap<>( tableGroupEntityNameUses ); } } - if ( conjunctTreatUsagesArray == null ) { - if ( originalConjunctTableGroupTreatUsages != null ) { - tableGroupEntityNameUses.putAll( originalConjunctTableGroupTreatUsages ); + if ( disjunctEntityNameUsesArray == null ) { + if ( previousTableGroupEntityNameUses != null ) { + tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); } return disjunction; } @@ -6917,7 +7131,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base final Map intersected = new HashMap<>( entry.getValue() ); entry.setValue( intersected ); boolean remove = false; - for ( Map> conjunctTreatUsages : conjunctTreatUsagesArray ) { + for ( Map> conjunctTreatUsages : disjunctEntityNameUsesArray ) { final Map entityNames; if ( conjunctTreatUsages == null || ( entityNames = conjunctTreatUsages.get( entry.getKey() ) ) == null ) { remove = true; @@ -6953,8 +7167,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base } // Prepend the treat type usages to the respective conjuncts - for ( int i = 0; i < conjunctTreatUsagesArray.length; i++ ) { - final Map> conjunctTreatUsages = conjunctTreatUsagesArray[i]; + for ( int i = 0; i < disjunctEntityNameUsesArray.length; i++ ) { + final Map> conjunctTreatUsages = disjunctEntityNameUsesArray[i]; if ( conjunctTreatUsages != null && !conjunctTreatUsages.isEmpty() ) { disjunction.getPredicates().set( i, @@ -6965,21 +7179,20 @@ public abstract class BaseSqmToSqlAstConverter extends Base ); } } - if ( originalConjunctTableGroupTreatUsages != null ) { - for ( Map.Entry> entry : originalConjunctTableGroupTreatUsages.entrySet() ) { - final Map entityNameUses = tableGroupEntityNameUses.putIfAbsent( - entry.getKey(), - entry.getValue() - ); - if ( entityNameUses != null && entityNameUses != entry.getValue() ) { - for ( Map.Entry useEntry : entry.getValue().entrySet() ) { - final EntityNameUse currentUseKind = entityNameUses.get( useEntry.getKey() ); - if ( currentUseKind == null ) { - entityNameUses.put( useEntry.getKey(), useEntry.getValue() ); - } - else { - entityNameUses.put( useEntry.getKey(), useEntry.getValue().stronger( currentUseKind ) ); - } + // Propagate the union of the entity name uses upwards + for ( Map.Entry> entry : entityNameUsesToPropagate.entrySet() ) { + final Map entityNameUses = tableGroupEntityNameUses.putIfAbsent( + entry.getKey(), + entry.getValue() + ); + if ( entityNameUses != null && entityNameUses != entry.getValue() ) { + for ( Map.Entry useEntry : entry.getValue().entrySet() ) { + final EntityNameUse currentEntityNameUse = entityNameUses.get( useEntry.getKey() ); + if ( currentEntityNameUse == null ) { + entityNameUses.put( useEntry.getKey(), useEntry.getValue() ); + } + else { + entityNameUses.put( useEntry.getKey(), useEntry.getValue().stronger( currentEntityNameUse ) ); } } } @@ -7103,14 +7316,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base } private void handleTypeComparison(Expression lhs, Expression rhs, boolean inclusive) { - final DiscriminatorPathInterpretation typeExpression; + final DiscriminatorPathInterpretation typeExpression; final EntityTypeLiteral literalExpression; if ( lhs instanceof DiscriminatorPathInterpretation ) { - typeExpression = (DiscriminatorPathInterpretation) lhs; + typeExpression = (DiscriminatorPathInterpretation) lhs; literalExpression = rhs instanceof EntityTypeLiteral ? (EntityTypeLiteral) rhs : null; } else if ( rhs instanceof DiscriminatorPathInterpretation ) { - typeExpression = (DiscriminatorPathInterpretation) rhs; + typeExpression = (DiscriminatorPathInterpretation) rhs; literalExpression = lhs instanceof EntityTypeLiteral ? (EntityTypeLiteral) lhs : null; } else { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java index a8259f828b..6968c12478 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstQueryPartProcessingStateImpl.java @@ -11,7 +11,8 @@ import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; -import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; +import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlAstProcessingState; @@ -20,7 +21,6 @@ import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; @@ -36,7 +36,7 @@ public class SqlAstQueryPartProcessingStateImpl implements SqlAstQueryPartProcessingState { private final QueryPart queryPart; - private final Map, Boolean>> treatRegistrations = new HashMap<>(); + private final Map, Boolean> sqmFromRegistrations = new HashMap<>(); private final boolean deduplicateSelectionItems; private FetchParent nestingFetchParent; @@ -77,27 +77,32 @@ public class SqlAstQueryPartProcessingStateImpl } @Override - public void registerTreat(TableGroup tableGroup, EntityDomainType treatType) { - treatRegistrations.computeIfAbsent( tableGroup, tg -> new HashMap<>() ).put( treatType, Boolean.FALSE ); + public void registerTreatedFrom(SqmFrom sqmFrom) { + sqmFromRegistrations.put( sqmFrom, null ); } @Override - public void registerTreatUsage(TableGroup tableGroup, EntityDomainType treatType) { - final Map, Boolean> treatUses = treatRegistrations.get( tableGroup ); - if ( treatUses == null ) { - final SqlAstProcessingState parentState = getParentState(); - if ( parentState instanceof SqlAstQueryPartProcessingState ) { - ( (SqlAstQueryPartProcessingState) parentState ).registerTreatUsage( tableGroup, treatType ); + public void registerFromUsage(SqmFrom sqmFrom, boolean downgradeTreatUses) { + if ( !( sqmFrom instanceof SqmTreatedPath ) ) { + if ( !sqmFromRegistrations.containsKey( sqmFrom ) ) { + final SqlAstProcessingState parentState = getParentState(); + if ( parentState instanceof SqlAstQueryPartProcessingState ) { + ( (SqlAstQueryPartProcessingState) parentState ).registerFromUsage( sqmFrom, downgradeTreatUses ); + } + } + else { + // If downgrading was once forcibly disabled, don't overwrite that anymore + final Boolean currentValue = sqmFromRegistrations.get( sqmFrom ); + if ( currentValue != Boolean.FALSE ) { + sqmFromRegistrations.put( sqmFrom, downgradeTreatUses ); + } } } - else { - treatUses.put( treatType, Boolean.TRUE ); - } } @Override - public Map, Boolean>> getTreatRegistrations() { - return treatRegistrations; + public Map, Boolean> getFromRegistrations() { + return sqmFromRegistrations; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java index 3cfb1fade8..b652b1eec4 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstQueryPartProcessingState.java @@ -9,6 +9,7 @@ package org.hibernate.sql.ast.spi; import java.util.Map; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.select.QueryPart; @@ -24,12 +25,19 @@ public interface SqlAstQueryPartProcessingState extends SqlAstProcessingState { */ QueryPart getInflightQueryPart(); - void registerTreat(TableGroup tableGroup, EntityDomainType treatType); - - void registerTreatUsage(TableGroup tableGroup, EntityDomainType treatType); + /** + * Registers that the given SqmFrom is treated. + */ + void registerTreatedFrom(SqmFrom sqmFrom); /** - * The treat registrations. The boolean indicates whether the treat is used in the query part. + * Registers that the given SqmFrom was used in an expression and whether to downgrade {@link org.hibernate.persister.entity.EntityNameUse#TREAT} of it. */ - Map, Boolean>> getTreatRegistrations(); + void registerFromUsage(SqmFrom sqmFrom, boolean downgradeTreatUses); + + /** + * Returns the treated SqmFroms and whether their {@link org.hibernate.persister.entity.EntityNameUse#TREAT} + * should be downgraded to {@link org.hibernate.persister.entity.EntityNameUse#EXPRESSION}. + */ + Map, Boolean> getFromRegistrations(); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java index cb39a614a2..3a68a05dde 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java @@ -193,4 +193,20 @@ public interface TableGroup extends SqlAstNode, ColumnReferenceQualifier, SqmPat } return null; } + + default boolean hasRealJoins() { + for ( TableGroupJoin join : getTableGroupJoins() ) { + final TableGroup joinedGroup = join.getJoinedGroup(); + if ( !( joinedGroup instanceof VirtualTableGroup ) || joinedGroup.hasRealJoins() ) { + return true; + } + } + for ( TableGroupJoin join : getNestedTableGroupJoins() ) { + final TableGroup joinedGroup = join.getJoinedGroup(); + if ( !( joinedGroup instanceof VirtualTableGroup ) || joinedGroup.hasRealJoins() ) { + return true; + } + } + return false; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/embeddables/EmbeddableWithGenericAndMappedSuperClassTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/embeddables/EmbeddableWithGenericAndMappedSuperClassTest.java index 2785f69507..c4e8fc5c6d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/embeddables/EmbeddableWithGenericAndMappedSuperClassTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/embeddables/EmbeddableWithGenericAndMappedSuperClassTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import jakarta.persistence.AttributeOverride; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Embeddable; @@ -273,6 +274,7 @@ public class EmbeddableWithGenericAndMappedSuperClassTest { @Entity(name = "PopularBook") + @AttributeOverride( name = "edition.code", column = @Column(name = "code_str")) public static class PopularBook extends Book { @@ -285,6 +287,7 @@ public class EmbeddableWithGenericAndMappedSuperClassTest { } @Entity(name = "RareBook") + @AttributeOverride( name = "edition.code", column = @Column(name = "code_nr")) public static class RareBook extends Book { public RareBook() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java index 163cc51c27..aa4ffe4c3e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseJoinedSubclassOptimizationTest.java @@ -55,35 +55,35 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + + "t1_1.seats," + + "t1_2.nr," + "t1_3.doors," + - "t1_4.name " + + "t1_4.familyName," + + "t1_5.architectName," + + "t1_5.doors," + + "t1_6.name " + "from Thing t1_0 " + - "join Building t1_1 on t1_0.id=t1_1.id " + - "join House t1_2 on t1_0.id=t1_2.id " + - "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join Airplane t1_1 on t1_0.id=t1_1.id " + + "join Building t1_2 on t1_0.id=t1_2.id " + + "left join Car t1_3 on t1_0.id=t1_3.id " + + "join House t1_4 on t1_0.id=t1_4.id " + + "left join Skyscraper t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + "where " + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end=2", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); @@ -160,35 +160,35 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + + "t1_1.seats," + + "t1_2.nr," + "t1_3.doors," + - "t1_4.name " + + "t1_4.familyName," + + "t1_5.architectName," + + "t1_5.doors," + + "t1_6.name " + "from Thing t1_0 " + - "left join Building t1_1 on t1_0.id=t1_1.id " + - "left join House t1_2 on t1_0.id=t1_2.id " + - "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join Airplane t1_1 on t1_0.id=t1_1.id " + + "left join Building t1_2 on t1_0.id=t1_2.id " + + "left join Car t1_3 on t1_0.id=t1_3.id " + + "left join House t1_4 on t1_0.id=t1_4.id " + + "left join Skyscraper t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + "where " + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end!=2", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); @@ -209,35 +209,35 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + + "t1_1.seats," + + "t1_2.nr," + "t1_3.doors," + - "t1_4.name " + + "t1_4.familyName," + + "t1_5.architectName," + + "t1_5.doors," + + "t1_6.name " + "from Thing t1_0 " + - "left join Building t1_1 on t1_0.id=t1_1.id " + - "left join House t1_2 on t1_0.id=t1_2.id " + - "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join Airplane t1_1 on t1_0.id=t1_1.id " + + "left join Building t1_2 on t1_0.id=t1_2.id " + + "left join Car t1_3 on t1_0.id=t1_3.id " + + "left join House t1_4 on t1_0.id=t1_4.id " + + "left join Skyscraper t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + "where " + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end in (2,5)", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); @@ -258,35 +258,35 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + + "t1_1.seats," + + "t1_2.nr," + "t1_3.doors," + - "t1_4.name " + + "t1_4.familyName," + + "t1_5.architectName," + + "t1_5.doors," + + "t1_6.name " + "from Thing t1_0 " + - "join Building t1_1 on t1_0.id=t1_1.id " + - "left join House t1_2 on t1_0.id=t1_2.id " + - "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join Airplane t1_1 on t1_0.id=t1_1.id " + + "join Building t1_2 on t1_0.id=t1_2.id " + + "left join Car t1_3 on t1_0.id=t1_3.id " + + "left join House t1_4 on t1_0.id=t1_4.id " + + "left join Skyscraper t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + "where " + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end in (2,3)", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); @@ -307,35 +307,35 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + + "t1_1.seats," + + "t1_2.nr," + "t1_3.doors," + - "t1_4.name " + + "t1_4.familyName," + + "t1_5.architectName," + + "t1_5.doors," + + "t1_6.name " + "from Thing t1_0 " + - "left join Building t1_1 on t1_0.id=t1_1.id " + - "left join House t1_2 on t1_0.id=t1_2.id " + - "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join Airplane t1_1 on t1_0.id=t1_1.id " + + "left join Building t1_2 on t1_0.id=t1_2.id " + + "left join Car t1_3 on t1_0.id=t1_3.id " + + "left join House t1_4 on t1_0.id=t1_4.id " + + "left join Skyscraper t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + "where " + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_4.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_3.id is not null then 5 " + + "when t1_1.id is not null then 6 " + + "when t1_2.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end not in (2,5)", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); @@ -358,29 +358,54 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_1.id is not null then 2 " + + "when t1_5.id is not null then 3 " + + "when t1_4.id is not null then 5 " + + "when t1_2.id is not null then 6 " + + "when t1_3.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + - "t1_3.doors," + - "t1_4.name " + + "t1_2.seats," + + "t1_3.nr," + + "t1_4.doors," + + "t1_1.familyName," + + "t1_5.architectName," + + "t1_5.doors,t1_6.name " + "from Thing t1_0 " + - "join Building t1_1 on t1_0.id=t1_1.id " + - "join House t1_2 on t1_0.id=t1_2.id " + - "left join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join House t1_1 on t1_0.id=t1_1.id " + + "left join Airplane t1_2 on t1_0.id=t1_2.id " + + "left join Building t1_3 on t1_0.id=t1_3.id " + + "left join Car t1_4 on t1_0.id=t1_4.id " + + "left join Skyscraper t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + + "where t1_1.familyName is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathEverywhere(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select treat(t as House) from Thing t where treat(t as House).familyName is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + // We need to join all tables because the EntityResult will create fetches for all subtypes. + // See #testEqTypeRestriction() for further explanation + assertEquals( + "select " + + "t1_1.id," + + "t1_2.nr," + + "t1_1.familyName " + + "from Thing t1_0 " + + "join House t1_1 on t1_0.id=t1_1.id " + + "join Building t1_2 on t1_0.id=t1_2.id " + "where " + - "t1_2.familyName is not null", + "t1_1.familyName is not null", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); } @@ -400,36 +425,65 @@ public class EntityUseJoinedSubclassOptimizationTest { "select " + "t1_0.id," + "case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + + "when t1_5.id is not null then 2 " + + "when t1_1.id is not null then 3 " + + "when t1_4.id is not null then 5 " + + "when t1_2.id is not null then 6 " + + "when t1_3.id is not null then 1 " + + "when t1_6.id is not null then 4 " + "end," + - "t1_6.seats," + - "t1_1.nr," + - "t1_5.doors," + - "t1_2.familyName," + - "t1_3.architectName," + - "t1_3.doors," + - "t1_4.name " + + "t1_2.seats," + + "t1_3.nr," + + "t1_4.doors," + + "t1_5.familyName," + + "t1_1.architectName," + + "t1_1.doors," + + "t1_6.name " + "from Thing t1_0 " + - "join Building t1_1 on t1_0.id=t1_1.id " + - "left join House t1_2 on t1_0.id=t1_2.id " + - "join Skyscraper t1_3 on t1_0.id=t1_3.id " + - "left join Vehicle t1_4 on t1_0.id=t1_4.id " + - "left join Car t1_5 on t1_0.id=t1_5.id " + - "left join Airplane t1_6 on t1_0.id=t1_6.id " + + "left join Skyscraper t1_1 on t1_0.id=t1_1.id " + + "left join Airplane t1_2 on t1_0.id=t1_2.id " + + "left join Building t1_3 on t1_0.id=t1_3.id " + + "left join Car t1_4 on t1_0.id=t1_4.id " + + "left join House t1_5 on t1_0.id=t1_5.id " + + "left join Vehicle t1_6 on t1_0.id=t1_6.id " + "where " + "case when case " + - "when t1_2.id is not null then 2 " + - "when t1_3.id is not null then 3 " + - "when t1_5.id is not null then 5 " + - "when t1_6.id is not null then 6 " + - "when t1_1.id is not null then 1 " + - "when t1_4.id is not null then 4 " + - "end=3 then t1_3.doors end is not null", + "when t1_5.id is not null then 2 " + + "when t1_1.id is not null then 3 " + + "when t1_4.id is not null then 5 " + + "when t1_2.id is not null then 6 " + + "when t1_3.id is not null then 1 " + + "when t1_6.id is not null then 4 " + + "end=3 then t1_1.doors end is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathEverywhereSharedColumn(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select treat(t as Skyscraper) from Thing t where treat(t as Skyscraper).doors is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_1.id," + + "t1_2.nr," + + "t1_1.architectName," + + "t1_1.doors " + + "from Thing t1_0 " + + "join Skyscraper t1_1 on t1_0.id=t1_1.id " + + "join Building t1_2 on t1_0.id=t1_2.id " + + "where " + + "case when case " + + "when t1_1.id is not null then 3 " + + "when t1_2.id is not null then 1 " + + "end=3 then t1_1.doors end is not null", sqlStatementInterceptor.getSqlQueries().get( 0 ) ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java index a9cabb2557..8c7f7c7077 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseSingleTableOptimizationTest.java @@ -239,7 +239,7 @@ public class EntityUseSingleTableOptimizationTest { "t1_0.familyName," + "t1_0.architectName," + "t1_0.name " + - "from (select * from Thing t where t.DTYPE='House') t1_0 " + + "from Thing t1_0 " + "where " + "t1_0.familyName is not null", sqlStatementInterceptor.getSqlQueries().get( 0 ) @@ -267,7 +267,7 @@ public class EntityUseSingleTableOptimizationTest { "t1_0.familyName," + "t1_0.architectName," + "t1_0.name " + - "from (select * from Thing t where t.DTYPE='Skyscraper') t1_0 " + + "from Thing t1_0 " + "where " + "case when t1_0.DTYPE='Skyscraper' then t1_0.doors end is not null", sqlStatementInterceptor.getSqlQueries().get( 0 ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java index 10b409b4ba..3784f1251f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/EntityUseUnionSubclassOptimizationTest.java @@ -58,7 +58,7 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House" + + "select id, nr, null as name, null as seats, null as architectName, null as doors, familyName, 2 as clazz_ from House" + ") t1_0 " + "where " + "t1_0.clazz_=2", @@ -81,7 +81,7 @@ public class EntityUseUnionSubclassOptimizationTest { "select " + "1 " + "from (" + - "select id, nr, null as familyName, null as architectName, null as doors, 1 as clazz_ from Building" + + "select id, nr, 1 as clazz_ from Building" + ") t1_0 " + "where " + "t1_0.clazz_=1", @@ -134,15 +134,15 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, null as nr, null as familyName, null as architectName, null as doors, name, seats, 6 as clazz_ from Airplane " + + "select id, null as nr, name, seats, null as architectName, null as doors, null as familyName, 6 as clazz_ from Airplane " + "union all " + - "select id, nr, null as familyName, null as architectName, null as doors, null as name, null as seats, 1 as clazz_ from Building " + + "select id, nr, null as name, null as seats, null as architectName, null as doors, null as familyName, 1 as clazz_ from Building " + "union all " + - "select id, null as nr, null as familyName, null as architectName, doors, name, null as seats, 5 as clazz_ from Car " + + "select id, null as nr, name, null as seats, null as architectName, doors, null as familyName, 5 as clazz_ from Car " + "union all " + - "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper " + + "select id, nr, null as name, null as seats, architectName, doors, null as familyName, 3 as clazz_ from Skyscraper " + "union all " + - "select id, null as nr, null as familyName, null as architectName, null as doors, name, null as seats, 4 as clazz_ from Vehicle" + + "select id, null as nr, name, null as seats, null as architectName, null as doors, null as familyName, 4 as clazz_ from Vehicle" + ") t1_0 " + "where " + "t1_0.clazz_!=2", @@ -172,9 +172,9 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, null as nr, null as familyName, null as architectName, doors, name, null as seats, 5 as clazz_ from Car " + + "select id, null as nr, name, null as seats, null as architectName, doors, null as familyName, 5 as clazz_ from Car " + "union all " + - "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House" + + "select id, nr, null as name, null as seats, null as architectName, null as doors, familyName, 2 as clazz_ from House" + ") t1_0 " + "where " + "t1_0.clazz_ in (2,5)", @@ -204,9 +204,9 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House " + + "select id, nr, null as name, null as seats, null as architectName, null as doors, familyName, 2 as clazz_ from House " + "union all " + - "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper" + + "select id, nr, null as name, null as seats, architectName, doors, null as familyName, 3 as clazz_ from Skyscraper" + ") t1_0 " + "where " + "t1_0.clazz_ in (2,3)", @@ -236,13 +236,13 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, null as nr, null as familyName, null as architectName, null as doors, name, seats, 6 as clazz_ from Airplane " + + "select id, null as nr, name, seats, null as architectName, null as doors, null as familyName, 6 as clazz_ from Airplane " + "union all " + - "select id, nr, null as familyName, null as architectName, null as doors, null as name, null as seats, 1 as clazz_ from Building " + + "select id, nr, null as name, null as seats, null as architectName, null as doors, null as familyName, 1 as clazz_ from Building " + "union all " + - "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper " + + "select id, nr, null as name, null as seats, architectName, doors, null as familyName, 3 as clazz_ from Skyscraper " + "union all " + - "select id, null as nr, null as familyName, null as architectName, null as doors, name, null as seats, 4 as clazz_ from Vehicle" + + "select id, null as nr, name, null as seats, null as architectName, null as doors, null as familyName, 4 as clazz_ from Vehicle" + ") t1_0 " + "where " + "t1_0.clazz_ not in (2,5)", @@ -272,7 +272,42 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House" + + "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House " + + "union all " + + "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, doors, name, null as seats, 5 as clazz_ from Car " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, seats, 6 as clazz_ from Airplane " + + "union all " + + "select id, nr, null as familyName, null as architectName, null as doors, null as name, null as seats, 1 as clazz_ from Building " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, null as seats, 4 as clazz_ from Vehicle" + + ") t1_0 " + + "where " + + "t1_0.familyName is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathEverywhere(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select treat(t as House) from Thing t where treat(t as House).familyName is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.nr," + + "t1_0.familyName " + + "from (" + + "select id, nr, familyName, 2 as clazz_ from House" + ") t1_0 " + "where " + "t1_0.familyName is not null", @@ -302,7 +337,43 @@ public class EntityUseUnionSubclassOptimizationTest { "t1_0.architectName," + "t1_0.name " + "from (" + - "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper" + + "select id, nr, familyName, null as architectName, null as doors, null as name, null as seats, 2 as clazz_ from House " + + "union all " + + "select id, nr, null as familyName, architectName, doors, null as name, null as seats, 3 as clazz_ from Skyscraper " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, doors, name, null as seats, 5 as clazz_ from Car " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, seats, 6 as clazz_ from Airplane " + + "union all " + + "select id, nr, null as familyName, null as architectName, null as doors, null as name, null as seats, 1 as clazz_ from Building " + + "union all " + + "select id, null as nr, null as familyName, null as architectName, null as doors, name, null as seats, 4 as clazz_ from Vehicle" + + ") t1_0 " + + "where " + + "case when t1_0.clazz_=3 then t1_0.doors end is not null", + sqlStatementInterceptor.getSqlQueries().get( 0 ) + ); + } + ); + } + + @Test + public void testTreatPathEverywhereSharedColumn(SessionFactoryScope scope) { + SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + scope.inTransaction( + entityManager -> { + sqlStatementInterceptor.clear(); + entityManager.createSelectionQuery( "select treat(t as Skyscraper) from Thing t where treat(t as Skyscraper).doors is not null" ) + .getResultList(); + sqlStatementInterceptor.assertExecutedCount( 1 ); + assertEquals( + "select " + + "t1_0.id," + + "t1_0.nr," + + "t1_0.architectName," + + "t1_0.doors " + + "from (" + + "select id, nr, architectName, doors, 3 as clazz_ from Skyscraper" + ") t1_0 " + "where " + "case when t1_0.clazz_=3 then t1_0.doors end is not null", diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/TreatKeywordTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/TreatKeywordTest.java index bff76991e1..6530a8b0be 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/TreatKeywordTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/TreatKeywordTest.java @@ -16,9 +16,11 @@ import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.hibernate.Session; import org.hibernate.Transaction; @@ -193,48 +195,128 @@ public class TreatKeywordTest extends BaseCoreFunctionalTestCase { @Test @TestForIssue(jiraKey = "HHH-9411") public void testTreatWithRestrictionOnAbstractClass() { - Session s = openSession(); - Transaction tx = s.beginTransaction(); + inTransaction( + s -> { + Greyhound greyhound = new Greyhound(); + Dachshund dachshund = new Dachshund(); + s.persist( greyhound ); + s.persist( dachshund ); - Greyhound greyhound = new Greyhound(); - Dachshund dachshund = new Dachshund(); - s.save( greyhound ); - s.save( dachshund ); + List results = s.createQuery( "select treat (a as Dog) from Animal a where a.fast = TRUE" ).list(); - List results = s.createQuery( "select treat (a as Dog) from Animal a where a.fast = TRUE" ).list(); - - assertEquals( Arrays.asList( greyhound ), results ); - - tx.commit(); - s.close(); + assertEquals( Arrays.asList( greyhound ), results ); + s.remove( greyhound ); + s.remove( dachshund ); + } + ); } @Test @TestForIssue(jiraKey = "HHH-16657") public void testTypeFilterInSubquery() { - Session s = openSession(); - Transaction tx = s.beginTransaction(); + inTransaction( + s -> { + JoinedEntitySubclass2 child1 = new JoinedEntitySubclass2(3, "child1"); + JoinedEntitySubSubclass2 child2 = new JoinedEntitySubSubclass2(4, "child2"); + JoinedEntitySubclass root1 = new JoinedEntitySubclass(1, "root1", child1); + JoinedEntitySubSubclass root2 = new JoinedEntitySubSubclass(2, "root2", child2); + s.persist( child1 ); + s.persist( child2 ); + s.persist( root1 ); + s.persist( root2 ); + } + ); + inSession( + s -> { + List results = s.createSelectionQuery( + "select (select o.name from j.other o where type(j) = JoinedEntitySubSubclass) from JoinedEntitySubclass j order by j.id", + String.class + ).list(); - JoinedEntitySubclass2 child1 = new JoinedEntitySubclass2(3, "child1"); - JoinedEntitySubSubclass2 child2 = new JoinedEntitySubSubclass2(4, "child2"); - JoinedEntitySubclass root1 = new JoinedEntitySubclass(1, "root1", child1); - JoinedEntitySubSubclass root2 = new JoinedEntitySubSubclass(2, "root2", child2); - s.persist( child1 ); - s.persist( child2 ); - s.persist( root1 ); - s.persist( root2 ); + assertEquals( 2, results.size() ); + assertNull( results.get( 0 ) ); + assertEquals( "child2", results.get( 1 ) ); + } + ); + inTransaction( + s -> { + s.createMutationQuery( "update JoinedEntity j set j.other = null" ).executeUpdate(); + s.createMutationQuery( "delete from JoinedEntity" ).executeUpdate(); + } + ); + } - List results = s.createSelectionQuery( - "select (select o.name from j.other o where type(j) = JoinedEntitySubSubclass) from JoinedEntitySubclass j order by j.id", - String.class - ).list(); + @Test + @TestForIssue(jiraKey = "HHH-16658") + public void testPropagateEntityNameUsesFromDisjunction() { + inSession( + s -> { + s.createSelectionQuery( + "select 1 from Animal a where (type(a) <> Dachshund or treat(a as Dachshund).fast) and (type(a) <> Greyhound or treat(a as Greyhound).fast)", + Integer.class + ).list(); + } + ); + } - assertEquals( 2, results.size() ); - assertNull( results.get( 0 ) ); - assertEquals( "child2", results.get( 1 ) ); + @Test + @TestForIssue(jiraKey = "HHH-16658") + public void testPropagateEntityNameUsesFromDisjunction2() { + inSession( + s -> { + s.createSelectionQuery( + "select 1 from JoinedEntity j where type(j) <> JoinedEntitySubclass or length(coalesce(treat(j as JoinedEntitySubclass).name,'')) > 1", + Integer.class + ).list(); + } + ); + } - tx.commit(); - s.close(); + @Test + @TestForIssue(jiraKey = "HHH-16657") + public void testTreatInSelect() { + inTransaction( + s -> { + JoinedEntitySubclass root1 = new JoinedEntitySubclass(1, "root1"); + JoinedEntitySubSubclass root2 = new JoinedEntitySubSubclass(2, "root2"); + s.persist( root1 ); + s.persist( root2 ); + } + ); + inSession( + s -> { + List results = s.createSelectionQuery( + "select treat(j as JoinedEntitySubSubclass).name from JoinedEntitySubclass j order by j.id", + String.class + ).list(); + + assertEquals( 2, results.size() ); + assertNull( results.get( 0 ) ); + assertEquals( "root2", results.get( 1 ) ); + } + ); + inTransaction( + s -> { + s.createMutationQuery( "delete from JoinedEntity" ).executeUpdate(); + } + ); + } + + @Test + @TestForIssue(jiraKey = "HHH-16571") // Sort of related to that issue + public void testJoinSubclassOneToMany() { + // Originally, the FK for "others" used the primary key of the root table JoinedEntity + // Since we didn't register an entity use, we wrongly pruned that table before. + // This was fixed by letting the FK descriptor point to the primary key of JoinedEntitySubclass2, + // i.e. the plural attribute declaring type, which has the nice benefit of saving us a join + inSession( + s -> { + s.createSelectionQuery( + "select 1 from JoinedEntitySubclass2 s left join s.others o", + Integer.class + ).list(); + } + ); } @Entity( name = "JoinedEntity" ) @@ -295,6 +377,8 @@ public class TreatKeywordTest extends BaseCoreFunctionalTestCase { @Entity( name = "JoinedEntitySubclass2" ) @Table( name = "JoinedEntitySubclass2" ) public static class JoinedEntitySubclass2 extends JoinedEntity { + @OneToMany(mappedBy = "other") + Set others; public JoinedEntitySubclass2() { }