Skip to content

Commit 4a5de9c

Browse files
committed
HHH-19883 Respect predicates defined on treated join nodes
1 parent 408cdd9 commit 4a5de9c

14 files changed

+574
-55
lines changed

hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.hibernate.query.sqm.tree.from.SqmEntityJoin;
2828
import org.hibernate.query.sqm.tree.from.SqmFrom;
2929
import org.hibernate.query.sqm.tree.from.SqmJoin;
30+
import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin;
3031
import org.hibernate.query.sqm.tree.from.SqmRoot;
3132

3233
import org.jboss.logging.Logger;
@@ -47,6 +48,7 @@ public class QualifiedJoinPathConsumer implements DotIdentifierConsumer {
4748
private final SqmJoinType joinType;
4849
private final boolean fetch;
4950
private final String alias;
51+
private final boolean allowReuse;
5052

5153
private ConsumerDelegate delegate;
5254
private boolean nested;
@@ -56,11 +58,13 @@ public QualifiedJoinPathConsumer(
5658
SqmJoinType joinType,
5759
boolean fetch,
5860
String alias,
61+
boolean allowReuse,
5962
SqmCreationState creationState) {
6063
this.sqmRoot = sqmRoot;
6164
this.joinType = joinType;
6265
this.fetch = fetch;
6366
this.alias = alias;
67+
this.allowReuse = allowReuse;
6468
this.creationState = creationState;
6569
}
6670

@@ -74,6 +78,8 @@ public QualifiedJoinPathConsumer(
7478
this.joinType = joinType;
7579
this.fetch = fetch;
7680
this.alias = alias;
81+
// This constructor is only used for entity names, so no need for join reuse
82+
this.allowReuse = false;
7783
this.creationState = creationState;
7884
this.delegate = new AttributeJoinDelegate(
7985
sqmFrom,
@@ -104,7 +110,13 @@ public void consumeIdentifier(String identifier, boolean isBase, boolean isTermi
104110
}
105111
else {
106112
assert delegate != null;
107-
delegate.consumeIdentifier( identifier, !nested && isTerminal, !( nested && isTerminal ) );
113+
delegate.consumeIdentifier(
114+
identifier,
115+
!nested && isTerminal,
116+
// Non-nested joins shall allow reuse, but nested ones (i.e. in treat)
117+
// only allow join reuse for non-terminal parts
118+
allowReuse && (!nested || !isTerminal)
119+
);
108120
}
109121
}
110122

@@ -197,7 +209,12 @@ private AttributeJoinDelegate resolveAlias(String identifier, boolean isTerminal
197209
if ( allowReuse ) {
198210
if ( !isTerminal ) {
199211
for ( SqmJoin<?, ?> sqmJoin : lhs.getSqmJoins() ) {
200-
if ( sqmJoin.getAlias() == null && sqmJoin.getModel() == subPathSource ) {
212+
// In order for an HQL join to be reusable, is must have the same path source,
213+
if ( sqmJoin.getModel() == subPathSource
214+
// must not have a join condition
215+
&& ( (SqmQualifiedJoin<?, ?>) sqmJoin ).getJoinPredicate() == null
216+
// and the same join type
217+
&& sqmJoin.getSqmJoinType() == joinType ) {
201218
return sqmJoin;
202219
}
203220
}

hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,10 +2219,12 @@ protected <X> void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot<X> sqmR
22192219
throw new SemanticException( "The 'from' clause of a subquery has a 'fetch'", query );
22202220
}
22212221

2222-
dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, this ) );
2222+
final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction();
2223+
// Joins are allowed to be reused if they don't have a join condition
2224+
final boolean allowReuse = joinRestrictionContext == null;
2225+
dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, allowReuse, this ) );
22232226
try {
22242227
final SqmQualifiedJoin<X, ?> join = getJoin( sqmRoot, joinType, qualifiedJoinTargetContext, alias, fetch );
2225-
final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction();
22262228
if ( join instanceof SqmEntityJoin<?> || join instanceof SqmDerivedJoin<?> || join instanceof SqmCteJoin<?> ) {
22272229
sqmRoot.addSqmJoin( join );
22282230
}
@@ -2338,7 +2340,7 @@ protected void consumeJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx,
23382340
final String alias = extractAlias( ctx.variable() );
23392341
dotIdentifierConsumerStack.push(
23402342
// According to JPA spec 4.4.6 this is an inner join
2341-
new QualifiedJoinPathConsumer( sqmRoot, SqmJoinType.INNER, false, alias, this )
2343+
new QualifiedJoinPathConsumer( sqmRoot, SqmJoinType.INNER, false, alias, true, this )
23422344
);
23432345

23442346
try {

hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@
88

99
import org.hibernate.metamodel.mapping.CollectionPart;
1010
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
11+
import org.hibernate.query.criteria.JpaPredicate;
12+
import org.hibernate.query.sqm.tree.predicate.SqmJunctionPredicate;
13+
import org.hibernate.query.sqm.tree.predicate.SqmPredicate;
1114
import org.hibernate.spi.NavigablePath;
1215
import org.hibernate.query.sqm.tree.domain.SqmPath;
1316

17+
import java.util.List;
1418
import java.util.concurrent.atomic.AtomicLong;
1519

20+
import jakarta.persistence.criteria.Predicate;
21+
22+
import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty;
23+
1624
/**
1725
* @author Steve Ebersole
1826
*/
@@ -68,6 +76,87 @@ public static NavigablePath buildSubNavigablePath(SqmPath<?> lhs, String subNavi
6876
return buildSubNavigablePath( navigablePath, subNavigable, alias );
6977
}
7078

79+
public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, List<Predicate> incomingRestrictions) {
80+
if ( isEmpty( incomingRestrictions ) ) {
81+
return baseRestriction;
82+
}
83+
84+
SqmPredicate combined = combinePredicates( null, baseRestriction );
85+
for ( int i = 0; i < incomingRestrictions.size(); i++ ) {
86+
combined = combinePredicates( combined, (SqmPredicate) incomingRestrictions.get(i) );
87+
}
88+
return combined;
89+
}
90+
91+
public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, JpaPredicate... incomingRestrictions) {
92+
if ( isEmpty( incomingRestrictions ) ) {
93+
return baseRestriction;
94+
}
95+
96+
SqmPredicate combined = combinePredicates( null, baseRestriction );
97+
for ( int i = 0; i < incomingRestrictions.length; i++ ) {
98+
combined = combinePredicates( combined, incomingRestrictions[i] );
99+
}
100+
return combined;
101+
}
102+
103+
public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, Predicate... incomingRestrictions) {
104+
if ( isEmpty( incomingRestrictions ) ) {
105+
return baseRestriction;
106+
}
107+
108+
SqmPredicate combined = combinePredicates( null, baseRestriction );
109+
for ( int i = 0; i < incomingRestrictions.length; i++ ) {
110+
combined = combinePredicates( combined, incomingRestrictions[i] );
111+
}
112+
return combined;
113+
}
114+
115+
116+
public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, SqmPredicate incomingRestriction) {
117+
if ( baseRestriction == null ) {
118+
return incomingRestriction;
119+
}
120+
121+
if ( incomingRestriction == null ) {
122+
return baseRestriction;
123+
}
124+
125+
final SqmJunctionPredicate combinedPredicate;
126+
127+
if ( baseRestriction instanceof SqmJunctionPredicate ) {
128+
final SqmJunctionPredicate junction = (SqmJunctionPredicate) baseRestriction;
129+
// we already had multiple before
130+
if ( junction.getPredicates().isEmpty() ) {
131+
return incomingRestriction;
132+
}
133+
134+
if ( junction.getOperator() == Predicate.BooleanOperator.AND ) {
135+
combinedPredicate = junction;
136+
}
137+
else {
138+
combinedPredicate = new SqmJunctionPredicate(
139+
Predicate.BooleanOperator.AND,
140+
baseRestriction.getExpressible(),
141+
baseRestriction.nodeBuilder()
142+
);
143+
combinedPredicate.getPredicates().add( baseRestriction );
144+
}
145+
}
146+
else {
147+
combinedPredicate = new SqmJunctionPredicate(
148+
Predicate.BooleanOperator.AND,
149+
baseRestriction.getExpressible(),
150+
baseRestriction.nodeBuilder()
151+
);
152+
combinedPredicate.getPredicates().add( baseRestriction );
153+
}
154+
155+
combinedPredicate.getPredicates().add( incomingRestriction );
156+
157+
return combinedPredicate;
158+
}
159+
71160
private SqmCreationHelper() {
72161
}
73162

hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper;
129129
import org.hibernate.query.sqm.produce.function.internal.PatternRenderer;
130130
import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker;
131+
import org.hibernate.query.sqm.spi.SqmCreationHelper;
131132
import org.hibernate.query.sqm.sql.internal.AnyDiscriminatorPathInterpretation;
132133
import org.hibernate.query.sqm.sql.internal.AsWrappedExpression;
133134
import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation;
@@ -227,6 +228,7 @@
227228
import org.hibernate.query.sqm.tree.from.SqmFrom;
228229
import org.hibernate.query.sqm.tree.from.SqmFromClause;
229230
import org.hibernate.query.sqm.tree.from.SqmJoin;
231+
import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin;
230232
import org.hibernate.query.sqm.tree.from.SqmRoot;
231233
import org.hibernate.query.sqm.tree.insert.SqmConflictClause;
232234
import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction;
@@ -385,7 +387,6 @@
385387
import org.hibernate.sql.results.graph.FetchParent;
386388
import org.hibernate.sql.results.graph.Fetchable;
387389
import org.hibernate.sql.results.graph.FetchableContainer;
388-
import org.hibernate.sql.results.graph.collection.internal.EagerCollectionFetch;
389390
import org.hibernate.sql.results.graph.entity.EntityResultGraphNode;
390391
import org.hibernate.sql.results.graph.instantiation.internal.DynamicInstantiation;
391392
import org.hibernate.sql.results.graph.internal.ImmutableFetchList;
@@ -2684,7 +2685,12 @@ protected void consumeFromClauseCorrelatedRoot(SqmRoot<?> sqmRoot) {
26842685
// as roots anyway, so nothing to worry about
26852686

26862687
log.tracef( "Resolved SqmRoot [%s] to correlated TableGroup [%s]", sqmRoot, tableGroup );
2687-
consumeExplicitJoins( from, tableGroup );
2688+
if ( from instanceof SqmRoot<?> ) {
2689+
consumeJoins( (SqmRoot<?>) from, fromClauseIndex, tableGroup );
2690+
}
2691+
else {
2692+
consumeExplicitJoins( from, tableGroup );
2693+
}
26882694
return;
26892695
}
26902696
else {
@@ -3347,6 +3353,39 @@ private TableGroup consumeAttributeJoin(
33473353
SqmMappingModelHelper.resolveExplicitTreatTarget( sqmJoin, this )
33483354
);
33493355

3356+
final List<SqmFrom<?, ?>> sqmTreats = sqmJoin.getSqmTreats();
3357+
final SqmPredicate joinPredicate;
3358+
final SqmPredicate[] treatPredicates;
3359+
final boolean hasPredicate;
3360+
if ( !sqmTreats.isEmpty() ) {
3361+
if ( sqmTreats.size() == 1 ) {
3362+
// If there is only a single treat, combine the predicates just as they are
3363+
joinPredicate = SqmCreationHelper.combinePredicates(
3364+
sqmJoin.getJoinPredicate(),
3365+
( (SqmQualifiedJoin<?, ?>) sqmTreats.get( 0 ) ).getJoinPredicate()
3366+
);
3367+
treatPredicates = null;
3368+
hasPredicate = joinPredicate != null;
3369+
}
3370+
else {
3371+
// When there are multiple predicates, we have to apply type filters
3372+
joinPredicate = sqmJoin.getJoinPredicate();
3373+
treatPredicates = new SqmPredicate[sqmTreats.size()];
3374+
boolean hasTreatPredicate = false;
3375+
for ( int i = 0; i < sqmTreats.size(); i++ ) {
3376+
final var p = ( (SqmQualifiedJoin<?, ?>) sqmTreats.get( i ) ).getJoinPredicate();
3377+
treatPredicates[i] = p;
3378+
hasTreatPredicate = hasTreatPredicate || p != null;
3379+
}
3380+
hasPredicate = joinPredicate != null || hasTreatPredicate;
3381+
}
3382+
}
3383+
else {
3384+
joinPredicate = sqmJoin.getJoinPredicate();
3385+
treatPredicates = null;
3386+
hasPredicate = joinPredicate != null;
3387+
}
3388+
33503389
if ( pathSource instanceof PluralPersistentAttribute ) {
33513390
assert modelPart instanceof PluralAttributeMapping;
33523391

@@ -3363,7 +3402,7 @@ private TableGroup consumeAttributeJoin(
33633402
null,
33643403
sqmJoinType.getCorrespondingSqlJoinType(),
33653404
sqmJoin.isFetched(),
3366-
sqmJoin.getJoinPredicate() != null,
3405+
hasPredicate,
33673406
this
33683407
);
33693408

@@ -3379,7 +3418,7 @@ private TableGroup consumeAttributeJoin(
33793418
null,
33803419
sqmJoinType.getCorrespondingSqlJoinType(),
33813420
sqmJoin.isFetched(),
3382-
sqmJoin.getJoinPredicate() != null,
3421+
hasPredicate,
33833422
this
33843423
);
33853424

@@ -3388,7 +3427,7 @@ private TableGroup consumeAttributeJoin(
33883427
// Since this is an explicit join, we force the initialization of a possible lazy table group
33893428
// to retain the cardinality, but only if this is a non-trivial attribute join.
33903429
// Left or inner singular attribute joins without a predicate can be safely optimized away
3391-
if ( sqmJoin.getJoinPredicate() != null || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) {
3430+
if ( hasPredicate || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) {
33923431
joinedTableGroup.getPrimaryTableReference();
33933432
}
33943433
}
@@ -3425,14 +3464,26 @@ private TableGroup consumeAttributeJoin(
34253464
final TableGroupJoin joinForPredicate;
34263465

34273466
// add any additional join restrictions
3428-
if ( sqmJoin.getJoinPredicate() != null ) {
3467+
if ( hasPredicate ) {
34293468
if ( sqmJoin.isFetched() ) {
34303469
QueryLogging.QUERY_MESSAGE_LOGGER.debugf( "Join fetch [%s] is restricted", sqmJoinNavigablePath );
34313470
}
34323471

34333472
final SqmJoin<?, ?> oldJoin = currentlyProcessingJoin;
34343473
currentlyProcessingJoin = sqmJoin;
3435-
final Predicate predicate = visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() );
3474+
Predicate predicate = joinPredicate == null ? null : visitNestedTopLevelPredicate( joinPredicate );
3475+
if ( treatPredicates != null ) {
3476+
final Junction orPredicate = new Junction( Junction.Nature.DISJUNCTION );
3477+
for ( int i = 0; i < treatPredicates.length; i++ ) {
3478+
final EntityDomainType<?> treatType =
3479+
(EntityDomainType<?>) ( (SqmTreatedPath<?, ?>) sqmTreats.get( i ) ).getTreatTarget();
3480+
orPredicate.add( combinePredicates(
3481+
createTreatTypeRestriction( sqmJoin, treatType ),
3482+
treatPredicates[i] == null ? null : visitNestedTopLevelPredicate( treatPredicates[i] )
3483+
) );
3484+
}
3485+
predicate = predicate != null ? combinePredicates( predicate, orPredicate ) : orPredicate;
3486+
}
34363487
joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( joinedTableGroupJoin );
34373488
// If translating the join predicate didn't initialize the table group,
34383489
// we can safely apply it on the collection table group instead

hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public SqmPath<?> resolvePathPart(
166166
if ( sqmJoin instanceof SqmSingularJoin<?, ?>
167167
&& name.equals( sqmJoin.getReferencedPathSource().getPathName() ) ) {
168168
final SqmAttributeJoin<?, ?> attributeJoin = (SqmAttributeJoin<?, ?>) sqmJoin;
169-
if ( attributeJoin.getOn() == null ) {
169+
if ( attributeJoin.getJoinPredicate() == null ) {
170170
// todo (6.0): to match the expectation of the JPA spec I think we also have to check
171171
// that the join type is INNER or the default join type for the attribute,
172172
// but as far as I understand, in 5.x we expect to ignore this behavior

0 commit comments

Comments
 (0)