From 6e6cc5f06e7560fd396160ee9bf455e9743256ad Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Fri, 4 Mar 2022 11:15:15 +0100 Subject: [PATCH] Fix subquery throwing SqlTreeCreationException( Found un-correlated path usage in sub query) --- .../sqm/sql/BaseSqmToSqlAstConverter.java | 3 +- .../jpa/compliance/CriteriaSubqueryTest.java | 231 ++++++++++++++++++ 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/CriteriaSubqueryTest.java 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 0bf54f174a..d27d3b835f 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 @@ -2851,7 +2851,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base if ( parentTableGroup == null ) { final TableGroup parent = fromClauseIndex.findTableGroupOnParents( parentPath.getNavigablePath() ); if ( parent != null ) { - throw new SqlTreeCreationException( "Found un-correlated path usage in sub query - " + parentPath ); + fromClauseIndex.register( (SqmPath) parentPath, parent ); + return parent; } throw new SqlTreeCreationException( "Could not locate TableGroup - " + parentPath.getNavigablePath() ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/CriteriaSubqueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/CriteriaSubqueryTest.java new file mode 100644 index 0000000000..80a52f55b0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/CriteriaSubqueryTest.java @@ -0,0 +1,231 @@ +package org.hibernate.orm.test.jpa.compliance; + +import java.util.List; + +import org.hibernate.dialect.MySQLDialect; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; +import jakarta.persistence.metamodel.EntityType; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Jpa( + annotatedClasses = { CriteriaSubqueryTest.TestEntity.class } +) +public class CriteriaSubqueryTest { + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + TestEntity testEntity = new TestEntity( 1, "1", 10 ); + TestEntity testEntity2 = new TestEntity( 2, "2", 17 ); + TestEntity testEntity3 = new TestEntity( 3, "3", 22 ); + TestEntity testEntity4 = new TestEntity( 4, "4", 38 ); + entityManager.persist( testEntity ); + entityManager.persist( testEntity2 ); + entityManager.persist( testEntity3 ); + entityManager.persist( testEntity4 ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> + entityManager.createQuery( "delete from TestEntity" ).executeUpdate() + ); + } + + @Test + public void existsInSubqueryTest(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + final Integer expectedId = 2; + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery( TestEntity.class ); + final Root from = criteriaQuery.from( TestEntity.class ); + + final EntityType testEntityType = from.getModel(); + + final Subquery subquery = criteriaQuery.subquery( TestEntity.class ); + final Root subqueryFrom = subquery.from( TestEntity.class ); + + assertThat( subqueryFrom.getModel().getName(), is( TestEntity.class.getSimpleName() ) ); + + subquery.where( criteriaBuilder.equal( + from.get( testEntityType.getSingularAttribute( "id", Integer.class ) ), + expectedId + ) ).select( subqueryFrom ); + + criteriaQuery.where( criteriaBuilder.exists( subquery ) ); + + criteriaQuery.select( from ); + + final List testEntities = entityManager.createQuery( criteriaQuery ).getResultList(); + + assertThat( testEntities.size(), is( 1 ) ); + assertThat( testEntities.get( 0 ).getId(), is( expectedId ) ); + } + ); + } + + @Test + public void subqueryCiteriaSelectTest(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + final Integer expectedId = 2; + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery( TestEntity.class ); + final Root from = criteriaQuery.from( TestEntity.class ); + + final EntityType testEntityType = from.getModel(); + + final Subquery subquery = criteriaQuery.subquery( TestEntity.class ); + final Root subqueryFrom = subquery.from( testEntityType ); + + assertThat( subqueryFrom.getModel().getName(), is( testEntityType.getName() ) ); + + subquery.where( criteriaBuilder.equal( + from.get( testEntityType.getSingularAttribute( "id", Integer.class ) ), + expectedId + ) ).select( subqueryFrom ); + + criteriaQuery.where( criteriaBuilder.exists( subquery ) ); + + criteriaQuery.select( from ); + + final List testEntities = entityManager.createQuery( criteriaQuery ).getResultList(); + assertThat( testEntities.size(), is( 1 ) ); + assertThat( testEntities.get( 0 ).getId(), is( expectedId ) ); + } + ); + } + + @Test + @SkipForDialect( dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "does not support specifying in the subquery from clause the the same table used in the delete/update ") + public void subqueryCriteriaDeleteTest(EntityManagerFactoryScope scope) { + + scope.inTransaction( + entityManager -> { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + final CriteriaDelete criteriaDelete = criteriaBuilder.createCriteriaDelete( TestEntity.class ); + final Root from = criteriaDelete.from( TestEntity.class ); + + final EntityType testEntityType = from.getModel(); + + final Subquery subquery = criteriaDelete.subquery( TestEntity.class ); + final Root subqueryFrom = subquery.from( TestEntity.class ); + + subquery.where( criteriaBuilder.equal( + from.get( testEntityType.getSingularAttribute( "id", Integer.class ) ), 2 ) ) + .select( subqueryFrom ); + + criteriaDelete.where( criteriaBuilder.exists( subquery ) ); + + final int entityDeleted = entityManager.createQuery( criteriaDelete ).executeUpdate(); + assertThat( entityDeleted, is( 1 ) ); + } + ); + + scope.inTransaction( + entityManager -> { + final TestEntity testEntity = entityManager.find( TestEntity.class, 2 ); + assertNull( testEntity ); + } + ); + + } + + @Test + @SkipForDialect( dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "does not support specifying in the subquery from clause the the same table used in the delete/update ") + public void subqueryCriteriaUpdateTest(EntityManagerFactoryScope scope) { + + scope.inTransaction( + entityManager -> { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + final CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate( TestEntity.class ); + final Root from = criteriaUpdate.from( TestEntity.class ); + final EntityType testEntityType = from.getModel(); + + criteriaUpdate.set( + from.get( "age" ), + criteriaBuilder.sum( + from.get( testEntityType.getSingularAttribute( "age", Integer.class ) ), + 13 + ) + ); + + + final Subquery subquery = criteriaUpdate.subquery( TestEntity.class ); + final Root subqueryFrom = subquery.from( TestEntity.class ); + + subquery.where( criteriaBuilder.equal( + from.get( testEntityType.getSingularAttribute( "id", Integer.class ) ), 2 ) ) + .select( subqueryFrom ); + + criteriaUpdate.where( criteriaBuilder.exists( subquery ) ); + + int entityUpdated = entityManager.createQuery( criteriaUpdate ).executeUpdate(); + assertThat( entityUpdated, is( 1 ) ); + } + ); + + scope.inTransaction( + entityManager -> { + TestEntity testEntity = entityManager.find( TestEntity.class, 2 ); + assertThat( testEntity.getAge(), is( 30 ) ); + } + ); + } + + + @Entity(name = "TestEntity") + public static class TestEntity { + + @Id + private Integer id; + + private String name; + + private Integer age; + + public TestEntity() { + } + + public TestEntity(Integer id, String name, Integer age) { + this.id = id; + this.name = name; + this.age = age; + } + + public Integer getId() { + return id; + } + + public Integer getAge() { + return age; + } + } +}