From fd3f62f3f7db68f30c23f96d5403c0cc667040b0 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 22 Oct 2025 20:14:35 +0200 Subject: [PATCH] HHH-19883 Respect predicates defined on treated join nodes --- .../internal/QualifiedJoinPathConsumer.java | 21 ++- .../hql/internal/SemanticQueryBuilder.java | 8 +- .../query/sqm/spi/SqmCreationHelper.java | 89 ++++++++++++ .../sqm/sql/BaseSqmToSqlAstConverter.java | 65 ++++++++- .../sqm/tree/domain/AbstractSqmFrom.java | 2 +- .../query/sqm/tree/from/SqmFromClause.java | 128 ++++++++++++------ .../tree/predicate/SqmJunctionPredicate.java | 10 ++ .../JoinedInheritanceTreatQueryTest.java | 91 +++++++++++++ .../inheritance/ManyToManyTreatJoinTest.java | 46 +++++++ ...ttributeJoinWithJoinedInheritanceTest.java | 32 +++++ ...eJoinWithNaturalJoinedInheritanceTest.java | 32 +++++ ...inWithRestrictedJoinedInheritanceTest.java | 41 +++++- ...uteJoinWithSingleTableInheritanceTest.java | 32 +++++ ...eJoinWithTablePerClassInheritanceTest.java | 32 +++++ 14 files changed, 574 insertions(+), 55 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index e6c872e13501..4bccded0e5ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java @@ -27,6 +27,7 @@ import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.jboss.logging.Logger; @@ -47,6 +48,7 @@ public class QualifiedJoinPathConsumer implements DotIdentifierConsumer { private final SqmJoinType joinType; private final boolean fetch; private final String alias; + private final boolean allowReuse; private ConsumerDelegate delegate; private boolean nested; @@ -56,11 +58,13 @@ public QualifiedJoinPathConsumer( SqmJoinType joinType, boolean fetch, String alias, + boolean allowReuse, SqmCreationState creationState) { this.sqmRoot = sqmRoot; this.joinType = joinType; this.fetch = fetch; this.alias = alias; + this.allowReuse = allowReuse; this.creationState = creationState; } @@ -74,6 +78,8 @@ public QualifiedJoinPathConsumer( this.joinType = joinType; this.fetch = fetch; this.alias = alias; + // This constructor is only used for entity names, so no need for join reuse + this.allowReuse = false; this.creationState = creationState; this.delegate = new AttributeJoinDelegate( sqmFrom, @@ -104,7 +110,13 @@ public void consumeIdentifier(String identifier, boolean isBase, boolean isTermi } else { assert delegate != null; - delegate.consumeIdentifier( identifier, !nested && isTerminal, !( nested && isTerminal ) ); + delegate.consumeIdentifier( + identifier, + !nested && isTerminal, + // Non-nested joins shall allow reuse, but nested ones (i.e. in treat) + // only allow join reuse for non-terminal parts + allowReuse && (!nested || !isTerminal) + ); } } @@ -197,7 +209,12 @@ private AttributeJoinDelegate resolveAlias(String identifier, boolean isTerminal if ( allowReuse ) { if ( !isTerminal ) { for ( SqmJoin sqmJoin : lhs.getSqmJoins() ) { - if ( sqmJoin.getAlias() == null && sqmJoin.getModel() == subPathSource ) { + // In order for an HQL join to be reusable, is must have the same path source, + if ( sqmJoin.getModel() == subPathSource + // must not have a join condition + && ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null + // and the same join type + && sqmJoin.getSqmJoinType() == joinType ) { return sqmJoin; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 17b152e5642f..fd7b093406bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -2219,10 +2219,12 @@ protected void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot sqmR throw new SemanticException( "The 'from' clause of a subquery has a 'fetch'", query ); } - dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, this ) ); + final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction(); + // Joins are allowed to be reused if they don't have a join condition + final boolean allowReuse = joinRestrictionContext == null; + dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, allowReuse, this ) ); try { final SqmQualifiedJoin join = getJoin( sqmRoot, joinType, qualifiedJoinTargetContext, alias, fetch ); - final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction(); if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin || join instanceof SqmCteJoin ) { sqmRoot.addSqmJoin( join ); } @@ -2338,7 +2340,7 @@ protected void consumeJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx, final String alias = extractAlias( ctx.variable() ); dotIdentifierConsumerStack.push( // According to JPA spec 4.4.6 this is an inner join - new QualifiedJoinPathConsumer( sqmRoot, SqmJoinType.INNER, false, alias, this ) + new QualifiedJoinPathConsumer( sqmRoot, SqmJoinType.INNER, false, alias, true, this ) ); try { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java index 1c43c1ae9b34..858ae81d2fce 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java @@ -8,11 +8,19 @@ import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmJunctionPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.spi.NavigablePath; import org.hibernate.query.sqm.tree.domain.SqmPath; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; +import jakarta.persistence.criteria.Predicate; + +import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; + /** * @author Steve Ebersole */ @@ -68,6 +76,87 @@ public static NavigablePath buildSubNavigablePath(SqmPath lhs, String subNavi return buildSubNavigablePath( navigablePath, subNavigable, alias ); } + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, List incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.size(); i++ ) { + combined = combinePredicates( combined, (SqmPredicate) incomingRestrictions.get(i) ); + } + return combined; + } + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, JpaPredicate... incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.length; i++ ) { + combined = combinePredicates( combined, incomingRestrictions[i] ); + } + return combined; + } + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, Predicate... incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.length; i++ ) { + combined = combinePredicates( combined, incomingRestrictions[i] ); + } + return combined; + } + + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, SqmPredicate incomingRestriction) { + if ( baseRestriction == null ) { + return incomingRestriction; + } + + if ( incomingRestriction == null ) { + return baseRestriction; + } + + final SqmJunctionPredicate combinedPredicate; + + if ( baseRestriction instanceof SqmJunctionPredicate ) { + final SqmJunctionPredicate junction = (SqmJunctionPredicate) baseRestriction; + // we already had multiple before + if ( junction.getPredicates().isEmpty() ) { + return incomingRestriction; + } + + if ( junction.getOperator() == Predicate.BooleanOperator.AND ) { + combinedPredicate = junction; + } + else { + combinedPredicate = new SqmJunctionPredicate( + Predicate.BooleanOperator.AND, + baseRestriction.getExpressible(), + baseRestriction.nodeBuilder() + ); + combinedPredicate.getPredicates().add( baseRestriction ); + } + } + else { + combinedPredicate = new SqmJunctionPredicate( + Predicate.BooleanOperator.AND, + baseRestriction.getExpressible(), + baseRestriction.nodeBuilder() + ); + combinedPredicate.getPredicates().add( baseRestriction ); + } + + combinedPredicate.getPredicates().add( incomingRestriction ); + + return combinedPredicate; + } + private SqmCreationHelper() { } 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 7d93f94cd67f..5d8f346977a6 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 @@ -128,6 +128,7 @@ import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; import org.hibernate.query.sqm.produce.function.internal.PatternRenderer; import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.sql.internal.AnyDiscriminatorPathInterpretation; import org.hibernate.query.sqm.sql.internal.AsWrappedExpression; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; @@ -227,6 +228,7 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction; @@ -385,7 +387,6 @@ import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.FetchableContainer; -import org.hibernate.sql.results.graph.collection.internal.EagerCollectionFetch; import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; import org.hibernate.sql.results.graph.instantiation.internal.DynamicInstantiation; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; @@ -2684,7 +2685,12 @@ protected void consumeFromClauseCorrelatedRoot(SqmRoot sqmRoot) { // as roots anyway, so nothing to worry about log.tracef( "Resolved SqmRoot [%s] to correlated TableGroup [%s]", sqmRoot, tableGroup ); - consumeExplicitJoins( from, tableGroup ); + if ( from instanceof SqmRoot ) { + consumeJoins( (SqmRoot) from, fromClauseIndex, tableGroup ); + } + else { + consumeExplicitJoins( from, tableGroup ); + } return; } else { @@ -3347,6 +3353,39 @@ private TableGroup consumeAttributeJoin( SqmMappingModelHelper.resolveExplicitTreatTarget( sqmJoin, this ) ); + final List> sqmTreats = sqmJoin.getSqmTreats(); + final SqmPredicate joinPredicate; + final SqmPredicate[] treatPredicates; + final boolean hasPredicate; + if ( !sqmTreats.isEmpty() ) { + if ( sqmTreats.size() == 1 ) { + // If there is only a single treat, combine the predicates just as they are + joinPredicate = SqmCreationHelper.combinePredicates( + sqmJoin.getJoinPredicate(), + ( (SqmQualifiedJoin) sqmTreats.get( 0 ) ).getJoinPredicate() + ); + treatPredicates = null; + hasPredicate = joinPredicate != null; + } + else { + // When there are multiple predicates, we have to apply type filters + joinPredicate = sqmJoin.getJoinPredicate(); + treatPredicates = new SqmPredicate[sqmTreats.size()]; + boolean hasTreatPredicate = false; + for ( int i = 0; i < sqmTreats.size(); i++ ) { + final var p = ( (SqmQualifiedJoin) sqmTreats.get( i ) ).getJoinPredicate(); + treatPredicates[i] = p; + hasTreatPredicate = hasTreatPredicate || p != null; + } + hasPredicate = joinPredicate != null || hasTreatPredicate; + } + } + else { + joinPredicate = sqmJoin.getJoinPredicate(); + treatPredicates = null; + hasPredicate = joinPredicate != null; + } + if ( pathSource instanceof PluralPersistentAttribute ) { assert modelPart instanceof PluralAttributeMapping; @@ -3363,7 +3402,7 @@ private TableGroup consumeAttributeJoin( null, sqmJoinType.getCorrespondingSqlJoinType(), sqmJoin.isFetched(), - sqmJoin.getJoinPredicate() != null, + hasPredicate, this ); @@ -3379,7 +3418,7 @@ private TableGroup consumeAttributeJoin( null, sqmJoinType.getCorrespondingSqlJoinType(), sqmJoin.isFetched(), - sqmJoin.getJoinPredicate() != null, + hasPredicate, this ); @@ -3388,7 +3427,7 @@ private TableGroup consumeAttributeJoin( // Since this is an explicit join, we force the initialization of a possible lazy table group // to retain the cardinality, but only if this is a non-trivial attribute join. // Left or inner singular attribute joins without a predicate can be safely optimized away - if ( sqmJoin.getJoinPredicate() != null || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) { + if ( hasPredicate || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) { joinedTableGroup.getPrimaryTableReference(); } } @@ -3425,14 +3464,26 @@ private TableGroup consumeAttributeJoin( final TableGroupJoin joinForPredicate; // add any additional join restrictions - if ( sqmJoin.getJoinPredicate() != null ) { + if ( hasPredicate ) { if ( sqmJoin.isFetched() ) { QueryLogging.QUERY_MESSAGE_LOGGER.debugf( "Join fetch [%s] is restricted", sqmJoinNavigablePath ); } final SqmJoin oldJoin = currentlyProcessingJoin; currentlyProcessingJoin = sqmJoin; - final Predicate predicate = visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ); + Predicate predicate = joinPredicate == null ? null : visitNestedTopLevelPredicate( joinPredicate ); + if ( treatPredicates != null ) { + final Junction orPredicate = new Junction( Junction.Nature.DISJUNCTION ); + for ( int i = 0; i < treatPredicates.length; i++ ) { + final EntityDomainType treatType = + (EntityDomainType) ( (SqmTreatedPath) sqmTreats.get( i ) ).getTreatTarget(); + orPredicate.add( combinePredicates( + createTreatTypeRestriction( sqmJoin, treatType ), + treatPredicates[i] == null ? null : visitNestedTopLevelPredicate( treatPredicates[i] ) + ) ); + } + predicate = predicate != null ? combinePredicates( predicate, orPredicate ) : orPredicate; + } joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( joinedTableGroupJoin ); // If translating the join predicate didn't initialize the table group, // we can safely apply it on the collection table group instead diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 235864080672..3db35b70fef7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -166,7 +166,7 @@ public SqmPath resolvePathPart( if ( sqmJoin instanceof SqmSingularJoin && name.equals( sqmJoin.getReferencedPathSource().getPathName() ) ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; - if ( attributeJoin.getOn() == null ) { + if ( attributeJoin.getJoinPredicate() == null ) { // todo (6.0): to match the expectation of the JPA spec I think we also have to check // that the join type is INNER or the default join type for the attribute, // but as far as I understand, in 5.x we expect to ignore this behavior diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java index 24ffbed1143f..049e646136f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java @@ -14,6 +14,7 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; /** @@ -117,57 +118,62 @@ public void appendHqlString(StringBuilder sb) { } public static void appendJoins(SqmFrom sqmFrom, StringBuilder sb) { - for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { - switch ( sqmJoin.getSqmJoinType() ) { - case LEFT: - sb.append( " left join " ); - break; - case RIGHT: - sb.append( " right join " ); - break; - case INNER: - sb.append( " join " ); - break; - case FULL: - sb.append( " full join " ); - break; - case CROSS: - sb.append( " cross join " ); - break; - } + if ( sqmFrom instanceof SqmRoot && ( (SqmRoot) sqmFrom ).getOrderedJoins() != null ) { + appendJoins( sqmFrom, ( (SqmRoot) sqmFrom ).getOrderedJoins(), sb, false ); + } + else { + appendJoins( sqmFrom, sqmFrom.getSqmJoins(), sb, true ); + } + } + + private static void appendJoins(SqmFrom sqmFrom, List> joins, StringBuilder sb, boolean transitive) { + for ( SqmJoin sqmJoin : joins ) { + appendJoinType( sb, sqmJoin.getSqmJoinType() ); if ( sqmJoin instanceof SqmAttributeJoin ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; - if ( sqmFrom instanceof SqmTreatedPath ) { - final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmFrom; - sb.append( "treat(" ); - sb.append( treatedPath.getWrappedPath().resolveAlias() ); - sb.append( " as " ).append( treatedPath.getTreatTarget().getTypeName() ).append( ')' ); + final List> sqmTreats = attributeJoin.getSqmTreats(); + if ( attributeJoin.getExplicitAlias() != null && !sqmTreats.isEmpty() ) { + for ( int i = 0; i < sqmTreats.size(); i++ ) { + final var treatJoin = (SqmAttributeJoin) sqmTreats.get( i ); + if ( i != 0 ) { + appendJoinType( sb, sqmJoin.getSqmJoinType() ); + } + sb.append( "treat(" ); + appendAttributeJoin( sqmFrom, sb, attributeJoin ); + sb.append( " as " ); + sb.append( ((SqmTreatedPath) treatJoin).getTreatTarget().getTypeName() ); + sb.append( ')' ); + appendJoinAliasAndOnClause( sb, treatJoin ); + if ( transitive ) { + appendJoins( treatJoin, sb ); + } + } } else { - sb.append( sqmFrom.resolveAlias() ); - } - sb.append( '.' ).append( ( attributeJoin ).getAttribute().getName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - if ( attributeJoin.getJoinPredicate() != null ) { - sb.append( " on " ); - attributeJoin.getJoinPredicate().appendHqlString( sb ); + appendAttributeJoin( sqmFrom, sb, attributeJoin ); + appendJoinAliasAndOnClause( sb, attributeJoin ); + if ( transitive ) { + appendJoins( attributeJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } } - appendJoins( sqmJoin, sb ); } else if ( sqmJoin instanceof SqmCrossJoin ) { sb.append( ( (SqmCrossJoin) sqmJoin ).getEntityName() ); sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - appendJoins( sqmJoin, sb ); + if ( transitive ) { + appendJoins( sqmJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } } else if ( sqmJoin instanceof SqmEntityJoin ) { final SqmEntityJoin sqmEntityJoin = (SqmEntityJoin) sqmJoin; - sb.append( ( sqmEntityJoin ).getEntityName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - if ( sqmEntityJoin.getJoinPredicate() != null ) { - sb.append( " on " ); - sqmEntityJoin.getJoinPredicate().appendHqlString( sb ); + sb.append( sqmEntityJoin.getEntityName() ); + appendJoinAliasAndOnClause( sb, sqmEntityJoin ); + if ( transitive ) { + appendJoins( sqmJoin, sb ); + appendTreatJoins( sqmJoin, sb ); } - appendJoins( sqmJoin, sb ); } else { throw new UnsupportedOperationException( "Unsupported join: " + sqmJoin ); @@ -175,6 +181,52 @@ else if ( sqmJoin instanceof SqmEntityJoin ) { } } + private static void appendJoinAliasAndOnClause(StringBuilder sb, SqmQualifiedJoin join) { + sb.append( ' ' ).append( join.resolveAlias() ); + if ( join.getJoinPredicate() != null ) { + sb.append( " on " ); + join.getJoinPredicate().appendHqlString( sb ); + } + } + + private static void appendAttributeJoin(SqmFrom sqmFrom, StringBuilder sb, SqmAttributeJoin attributeJoin) { + if ( sqmFrom instanceof SqmTreatedPath ) { + final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmFrom; + sb.append( "treat(" ); + treatedPath.getWrappedPath().appendHqlString( sb ); +// sb.append( treatedPath.getWrappedPath().resolveAlias( context ) ); + sb.append( " as " ).append( treatedPath.getTreatTarget().getTypeName() ).append( ')' ); + } + else { + sb.append( sqmFrom.resolveAlias() ); + } + sb.append( '.' ).append( attributeJoin.getAttribute().getName() ); + } + + private static void appendJoinType(StringBuilder sb, SqmJoinType sqmJoinType) { + final String joinText; + switch ( sqmJoinType ) { + case LEFT: + joinText = " left join "; + break; + case RIGHT: + joinText = " right join "; + break; + case INNER: + joinText = " join "; + break; + case FULL: + joinText = " full join "; + break; + case CROSS: + joinText = " cross join "; + break; + default: + throw new UnsupportedOperationException( "Unsupported join type: " + sqmJoinType ); + } + sb.append( joinText ); + } + private void appendJoins(SqmFrom sqmFrom, String correlationPrefix, StringBuilder sb) { String separator = ""; for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java index 343f07a65c75..b36d4fcd2313 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java @@ -11,6 +11,7 @@ import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.SqmCopyContext; import jakarta.persistence.criteria.Expression; @@ -22,6 +23,15 @@ public class SqmJunctionPredicate extends AbstractSqmPredicate { private final BooleanOperator booleanOperator; private final List predicates; + public SqmJunctionPredicate( + BooleanOperator booleanOperator, + SqmExpressible expressible, + NodeBuilder nodeBuilder) { + super( expressible, nodeBuilder ); + this.booleanOperator = booleanOperator; + this.predicates = new ArrayList<>(); + } + public SqmJunctionPredicate( BooleanOperator booleanOperator, SqmPredicate leftHandPredicate, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java index 4f2ef352d1e1..221481f0bc80 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java @@ -24,6 +24,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -80,6 +82,95 @@ public void testTreatedJoin(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner2) as own1 on own1.basicProp = 'unknown value'", + Product.class + ).getResultList(); + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinWithCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner1) as own1 on own1.description is null " + + "join treat(p.owner AS ProductOwner2) as own2 on own2.basicProp = 'unknown value'", + Product.class + ).getResultList(); + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinSameAttribute(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner1) as own1 " + + "join treat(p.owner AS ProductOwner2) as own2", + Product.class + ).getResultList(); + // No rows, because treat joining the same association with disjunct types can't emit results + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinSameAttributeCriteria(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery(Product.class); + final var p = query.from( Product.class ); + p.join( "owner" ).treatAs( ProductOwner1.class ); + p.join( "owner" ).treatAs( ProductOwner2.class ); + final List result = session.createSelectionQuery( query ).getResultList(); + // No rows, because treat joining the same association with disjunct types can't emit results + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinCriteria(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery(Product.class); + final var p = query.from( Product.class ); + final var ownerJoin = p.join( "owner" ); + ownerJoin.treatAs( ProductOwner1.class ); + ownerJoin.treatAs( ProductOwner2.class ); + final List result = session.createSelectionQuery( query ).getResultList(); + // The owner attribute is inner joined, but since there are multiple subtype treats, + // the type restriction for the treat usage does not filter rows + assertThat( result ).hasSize( 2 ); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + @Test public void testImplicitTreatedJoin(SessionFactoryScope scope) { final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java index 5e83f66318b1..cf4e8bef8c99 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java @@ -15,6 +15,7 @@ import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterAll; @@ -96,6 +97,21 @@ public void testSingleTableSelectParent(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testSingleTableTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.singleEntities as SingleSub1) s on s.subProp<>2", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + @Test public void testJoinedSelectChild(SessionFactoryScope scope) { final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); @@ -124,6 +140,21 @@ public void testJoinedSelectParent(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testJoinedTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.joinedEntities as JoinedSub1) s on s.subProp<>3", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + @Test public void testTablePerClassSelectChild(SessionFactoryScope scope) { final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); @@ -152,6 +183,21 @@ public void testTablePerClassSelectParent(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTablePerClassTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.unionEntities as UnionSub1) s on s.subProp<>4", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + @Entity( name = "ParentEntity" ) public static class ParentEntity { @Id diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java index c5ff0c9d3715..db0ffa80b576 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java @@ -138,6 +138,29 @@ public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + @Test public void testRightJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -266,6 +289,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; @Column( name = "disc_col", insertable = false, updatable = false ) private String discCol; @@ -284,6 +308,14 @@ public Integer getId() { public String getDiscCol() { return discCol; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java index 08ef6698e023..c47697a4bfa8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java @@ -75,6 +75,29 @@ public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + } ); + } + private void assertResult( Tuple result, Integer rootId, @@ -120,6 +143,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; @Column( name = "disc_col", insertable = false, updatable = false ) private String discCol; @@ -138,6 +162,14 @@ public Integer getId() { public String getDiscCol() { return discCol; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java index 4e64027d59e4..22e7232f18ab 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java @@ -47,10 +47,11 @@ public class AttributeJoinWithRestrictedJoinedInheritanceTest { @AfterEach public void cleanup(SessionFactoryScope scope) { scope.inTransaction( s -> { - s.createMutationQuery( "delete from RootOne" ).executeUpdate(); - s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); - s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); - s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + s.createNativeQuery( "delete from root_one" ).executeUpdate(); + s.createNativeQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createNativeQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createNativeQuery( "delete from child_entity" ).executeUpdate(); + s.createNativeQuery( "delete from BaseClass" ).executeUpdate(); } ); } @@ -77,6 +78,29 @@ public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + private void assertResult( Tuple result, Integer rootId, @@ -117,6 +141,7 @@ public static class BaseClass { @Id @Column(name = "ident") private Integer id; + private String name; @Column( name = "disc_col", insertable = false, updatable = false ) private String discCol; @@ -135,6 +160,14 @@ public Integer getId() { public String getDiscCol() { return discCol; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java index 522f20baabcb..1ecf1e7b1db4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java @@ -111,6 +111,29 @@ public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + @Test public void testRightJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -233,6 +256,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; @Column( name = "disc_col", insertable = false, updatable = false ) private String discCol; @@ -251,6 +275,14 @@ public Integer getId() { public String getDiscCol() { return discCol; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java index 661be8dae292..dcfd5b757730 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java @@ -109,6 +109,29 @@ public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, SubChildEntityA1.class ); + } ); + } + @Test public void testRightJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -228,6 +251,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; public BaseClass() { } @@ -239,6 +263,14 @@ public BaseClass(Integer id) { public Integer getId() { return id; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" )