From 943456b99884f07c97590e86394f62434c026e55 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Sun, 31 Aug 2025 16:50:59 +0200 Subject: [PATCH] HHH-19745 Use identity sensitive collections/operations where necessary after introducing equals implementation for SqmPath --- .../QualifiedJoinPredicatePathConsumer.java | 16 ++++- .../query/sqm/tree/select/SqmQuerySpec.java | 3 +- .../query/sqm/tree/select/SqmSubQuery.java | 5 +- .../orm/test/query/SubQueryShadowingTest.java | 62 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java index 784aac84341d..724d8639e332 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPredicatePathConsumer.java @@ -4,6 +4,7 @@ */ package org.hibernate.query.hql.internal; +import java.util.List; import java.util.Locale; import org.hibernate.query.SemanticException; @@ -56,9 +57,9 @@ protected void validateAsRoot(SqmFrom pathRoot) { final SqmFromClause fromClause = querySpec.getFromClause(); // If the current processing query contains the root of the current join, // then the root of the processing path must be a root of one of the parent queries - if ( fromClause != null && fromClause.getRoots().contains( joinRoot ) ) { + if ( fromClause != null && contains( fromClause.getRoots(), joinRoot ) ) { // It is allowed to use correlations from the same query - if ( !( root instanceof SqmCorrelation ) || !fromClause.getRoots().contains( root ) ) { + if ( !( root instanceof SqmCorrelation ) || !contains( fromClause.getRoots(), root ) ) { validateAsRootOnParentQueryClosure( pathRoot, root, processingState.getParentProcessingState() ); } @@ -97,7 +98,7 @@ private void validateAsRootOnParentQueryClosure( // If we are in a subquery, the "foreign" from element could be one of the subquery roots, // which is totally fine. The aim of this check is to prevent uses of different "spaces" // i.e. `from A a, B b join b.id = a.id` would be illegal - if ( fromClause != null && fromClause.getRoots().contains( root ) ) { + if ( fromClause != null && contains( fromClause.getRoots(), root ) ) { super.validateAsRoot( pathRoot ); return; } @@ -113,6 +114,15 @@ private void validateAsRootOnParentQueryClosure( ) ); } + + private boolean contains(List> roots, SqmRoot root) { + for ( SqmRoot sqmRoot : roots ) { + if ( sqmRoot == root ) { + return true; + } + } + return false; + } }; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java index a6f2fbf6dd00..3806609fff1a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQuerySpec.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -516,7 +517,7 @@ public void validateFetchOwners() { } } else { - selectedFromSet = new HashSet<>( selectClause.getSelections().size() ); + selectedFromSet = Collections.newSetFromMap( new IdentityHashMap<>( selectClause.getSelections().size() ) ); for ( SqmSelection selection : selectClause.getSelections() ) { collectSelectedFromSet( selectedFromSet, selection.getSelectableNode() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index c47f330a5870..012adb930a2b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -7,7 +7,8 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.Collection; -import java.util.HashSet; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -580,7 +581,7 @@ public JpaEntityJoin correlate(JpaEntityJoin parentEntityJoin) { @Override public Set> getCorrelatedJoins() { - final Set> correlatedJoins = new HashSet<>(); + final Set> correlatedJoins = Collections.newSetFromMap( new IdentityHashMap<>() ); final SqmFromClause fromClause = getQuerySpec().getFromClause(); if ( fromClause != null ) { for ( SqmRoot root : fromClause.getRoots() ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java new file mode 100644 index 000000000000..d4e266a82b0e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryShadowingTest.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + + +@DomainModel( + annotatedClasses = { + SubQueryShadowingTest.TestEntity.class, + } +) +@SessionFactory +public class SubQueryShadowingTest { + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsOffsetInSubquery.class, comment = "The check is for both, limit and offset in subqueries") + @Jira("https://hibernate.atlassian.net/browse/HHH-19745") + public void testSelectCase(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( + """ + from TestEntity t + left join TestEntity t2 on exists ( + select 1 + from TestEntity t + order by t.id + limit 1 + ) + """, TestEntity.class ) + .list(); + } ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(String name) { + this.name = name; + } + } +}