From 24cedfa6ec63105b423ec87f8dadca716d64190a Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Tue, 29 Oct 2019 15:06:42 -0700 Subject: [PATCH 1/8] HHH-13619 : test cases --- .../test/hql/size/ManyToManySizeTest.java | 342 +++++++++++++++++ .../test/hql/size/OneToManySizeTest.java | 344 ++++++++++++++++++ 2 files changed, 686 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/test/hql/size/OneToManySizeTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java new file mode 100644 index 0000000000..7414e53635 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java @@ -0,0 +1,342 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ + +package org.hibernate.test.hql.size; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; + +@TestForIssue( jiraKey = "HHH-13619" ) +public class ManyToManySizeTest extends BaseNonConfigCoreFunctionalTestCase { + + @Test + public void testSizeAsSelectExpression() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.ManyToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 3 ) ); + final CompanyDto companyDto0 = (CompanyDto) results.get( 0 ); + assertThat( companyDto0.getId(), is( 0 ) ); + assertThat( companyDto0.getName(), is( "Company 0") ); + assertThat( companyDto0.getSizeCustomer(), is( 0 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 1 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 2 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionWithLeftJoin() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.ManyToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c left join c.customers cu" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 3 ) ); + final CompanyDto companyDto0 = (CompanyDto) results.get( 0 ); + assertThat( companyDto0.getId(), is( 0 ) ); + assertThat( companyDto0.getName(), is( "Company 0") ); + assertThat( companyDto0.getSizeCustomer(), is( 0 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 1 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 2 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionWithInnerJoin() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.ManyToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c inner join c.customers cu" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 2 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 0 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 1 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionOfAliasWithInnerJoin() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.ManyToManySizeTest$CompanyDto(" + + " c.id, c.name, size( cu ) )" + + " from Company c inner join c.customers cu" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 2 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 0 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 1 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionExcludeEmptyCollection() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.ManyToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c" + + " where c.id != 0" + + " group by c.id, c.name order by c.id" + ).list(); + assertThat( results.size(), is( 2 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 0 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 1 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsConditionalExpressionExcludeEmptyCollection() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "from Company c" + + " where size( c.customers ) > 0" + + " group by c.id, c.name order by c.id", + Company.class + ).list(); + assertThat( results.size(), is( 2 ) ); + final Company company1 = results.get( 0 ); + assertThat( company1.id, is( 1 ) ); + assertThat( company1.name, is( "Company 1") ); + assertThat( Hibernate.isInitialized( company1.customers ), is( true ) ); + assertThat( company1.customers.size(), is( 1 ) ); + final Company company2 = results.get( 1 ); + assertThat( company2.id, is( 2 ) ); + assertThat( company2.name, is( "Company 2") ); + assertThat( Hibernate.isInitialized( company2.customers ), is( true ) ); + assertThat( company2.customers.size(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsConditionalExpressionIncludeEmptyCollection() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "from Company c" + + " where size( c.customers ) > -1" + + " group by c.id, c.name order by c.id", + Company.class + ).list(); + assertThat( results.size(), is( 3 ) ); + final Company company0 = results.get( 0 ); + assertThat( company0.id, is( 0 ) ); + assertThat( company0.name, is( "Company 0") ); + assertThat( Hibernate.isInitialized(company0.customers), is( true ) ); + assertThat( company0.customers.size(), is( 0 ) ); + final Company company1 = results.get( 1 ); + assertThat( company1.id, is( 1 ) ); + assertThat( company1.name, is( "Company 1") ); + assertThat( Hibernate.isInitialized(company1.customers), is( true ) ); + assertThat( company1.customers.size(), is( 1 ) ); + final Company company2 = results.get( 2 ); + assertThat( company2.id, is( 2 ) ); + assertThat( company2.name, is( "Company 2") ); + assertThat( Hibernate.isInitialized(company2.customers), is( true ) ); + assertThat( company2.customers.size(), is( 2 ) ); + } + ); + } + + @Before + public void initDataBase(){ + + doInHibernate( + this::sessionFactory, + session -> { + // Add a company with no customers + final Company companyWithNoCustomers = new Company( 0 ); + companyWithNoCustomers.name = "Company 0"; + session.persist( companyWithNoCustomers ); + int k = 0; + for ( int i = 1; i <= 2; i++ ) { + final Company company = new Company( i ); + company.name = "Company " + i; + + for ( int j = 1; j <= i; j++ ) { + final Customer customer = new Customer( k ); + customer.name = "Customer " + k; + company.customers.add( customer ); + k++; + } + session.persist( company ); + } + } + ); + } + + @After + public void cleanupDatabase() { + doInHibernate( + this::sessionFactory, + session -> { + for ( Company company : session.createQuery( "from Company", Company.class ).list() ) { + session.delete( company ); + } + } + ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Company.class, Customer.class }; + } + + @Entity(name ="Company") + public static class Company { + + @Id + private int id; + + private String name; + + @ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER) + private List customers = new ArrayList<>(); + + public Company() { + } + + public Company(int id) { + this.id = id; + } + } + + @Entity(name = "Customer") + public static class Customer { + + @Id + private int id; + + private String name; + + public Customer() { + } + + public Customer(int id) { + this.id = id; + } + } + + public static class CompanyDto { + + public int id; + + public String name; + + public int sizeCustomer; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSizeCustomer() { + return sizeCustomer; + } + + public void setSizeCustomer(int sizeCustomer) { + this.sizeCustomer = sizeCustomer; + } + + public CompanyDto(){} + + public CompanyDto(int id, String name, int sizeCustomer){ + this.id = id; + this.name = name; + this.sizeCustomer = sizeCustomer; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/size/OneToManySizeTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/size/OneToManySizeTest.java new file mode 100644 index 0000000000..b355ec9f4c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/size/OneToManySizeTest.java @@ -0,0 +1,344 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ + +package org.hibernate.test.hql.size; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; + +@TestForIssue( jiraKey = "HHH-13619" ) +public class OneToManySizeTest extends BaseNonConfigCoreFunctionalTestCase { + + @Test + public void testSizeAsSelectExpression() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.OneToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 3 ) ); + final CompanyDto companyDto0 = (CompanyDto) results.get( 0 ); + assertThat( companyDto0.getId(), is( 0 ) ); + assertThat( companyDto0.getName(), is( "Company 0") ); + assertThat( companyDto0.getSizeCustomer(), is( 0 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 1 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 2 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionWithLeftJoin() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.OneToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c left join c.customers cu" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 3 ) ); + final CompanyDto companyDto0 = (CompanyDto) results.get( 0 ); + assertThat( companyDto0.getId(), is( 0 ) ); + assertThat( companyDto0.getName(), is( "Company 0") ); + assertThat( companyDto0.getSizeCustomer(), is( 0 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 1 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 2 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionWithInnerJoin() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.OneToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c inner join c.customers cu" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 2 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 0 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 1 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionOfAliasWithInnerJoin() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.OneToManySizeTest$CompanyDto(" + + " c.id, c.name, size( cu ) )" + + " from Company c inner join c.customers cu" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 2 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 0 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 1 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsSelectExpressionExcludeEmptyCollection() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select new org.hibernate.test.hql.size.OneToManySizeTest$CompanyDto(" + + " c.id, c.name, size( c.customers ) )" + + " from Company c" + + " where c.id != 0" + + " group by c.id, c.name order by c.id" + ).list(); + assertThat( results.size(), is( 2 ) ); + final CompanyDto companyDto1 = (CompanyDto) results.get( 0 ); + assertThat( companyDto1.getId(), is( 1 ) ); + assertThat( companyDto1.getName(), is( "Company 1") ); + assertThat( companyDto1.getSizeCustomer(), is( 1 ) ); + final CompanyDto companyDto2 = (CompanyDto) results.get( 1 ); + assertThat( companyDto2.getId(), is( 2 ) ); + assertThat( companyDto2.getName(), is( "Company 2") ); + assertThat( companyDto2.getSizeCustomer(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsConditionalExpressionExcludeEmptyCollection() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "from Company c" + + " where size( c.customers ) > 0" + + " group by c.id, c.name order by c.id", + Company.class + ).list(); + assertThat( results.size(), is( 2 ) ); + final Company company1 = results.get( 0 ); + assertThat( company1.id, is( 1 ) ); + assertThat( company1.name, is( "Company 1") ); + assertThat( Hibernate.isInitialized( company1.customers ), is( true ) ); + assertThat( company1.customers.size(), is( 1 ) ); + final Company company2 = results.get( 1 ); + assertThat( company2.id, is( 2 ) ); + assertThat( company2.name, is( "Company 2") ); + assertThat( Hibernate.isInitialized( company2.customers ), is( true ) ); + assertThat( company2.customers.size(), is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsConditionalExpressionIncludeEmptyCollection() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "from Company c" + + " where size( c.customers ) > -1" + + " group by c.id, c.name order by c.id", + Company.class + ).list(); + assertThat( results.size(), is( 3 ) ); + final Company company0 = results.get( 0 ); + assertThat( company0.id, is( 0 ) ); + assertThat( company0.name, is( "Company 0") ); + assertThat( Hibernate.isInitialized(company0.customers), is( true ) ); + assertThat( company0.customers.size(), is( 0 ) ); + final Company company1 = results.get( 1 ); + assertThat( company1.id, is( 1 ) ); + assertThat( company1.name, is( "Company 1") ); + assertThat( Hibernate.isInitialized(company1.customers), is( true ) ); + assertThat( company1.customers.size(), is( 1 ) ); + final Company company2 = results.get( 2 ); + assertThat( company2.id, is( 2 ) ); + assertThat( company2.name, is( "Company 2") ); + assertThat( Hibernate.isInitialized(company2.customers), is( true ) ); + assertThat( company2.customers.size(), is( 2 ) ); + } + ); + } + + @Before + public void initDataBase(){ + + doInHibernate( + this::sessionFactory, + session -> { + // Add a company with no customers + final Company companyWithNoCustomers = new Company( 0 ); + companyWithNoCustomers.name = "Company 0"; + session.persist( companyWithNoCustomers ); + int k = 0; + for ( int i = 1; i <= 2; i++ ) { + final Company company = new Company( i ); + company.name = "Company " + i; + + for ( int j = 1; j <= i; j++ ) { + final Customer customer = new Customer( k ); + customer.name = "Customer " + k; + company.customers.add( customer ); + k++; + } + session.persist( company ); + } + } + ); + } + + @After + public void cleanupDatabase() { + doInHibernate( + this::sessionFactory, + session -> { + for ( Company company : session.createQuery( "from Company", Company.class ).list() ) { + session.delete( company ); + } + } + ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Company.class, Customer.class }; + } + + @Entity(name ="Company") + public static class Company { + + @Id + private int id; + + private String name; + + @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER) + @JoinColumn + private List customers = new ArrayList<>(); + + public Company() { + } + + public Company(int id) { + this.id = id; + } + } + + @Entity(name = "Customer") + public static class Customer { + + @Id + private int id; + + private String name; + + public Customer() { + } + + public Customer(int id) { + this.id = id; + } + } + + public static class CompanyDto { + + public int id; + + public String name; + + public int sizeCustomer; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSizeCustomer() { + return sizeCustomer; + } + + public void setSizeCustomer(int sizeCustomer) { + this.sizeCustomer = sizeCustomer; + } + + public CompanyDto(){} + + public CompanyDto(int id, String name, int sizeCustomer){ + this.id = id; + this.name = name; + this.sizeCustomer = sizeCustomer; + } + } +} From 692f19c83fef4339112e4fbabcc09437999a1bdf Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 4 Mar 2020 12:06:59 -0600 Subject: [PATCH 2/8] HHH-13619 - Support for JPA's `size` function as a select expression - initial support --- hibernate-core/src/main/antlr/hql-sql.g | 25 +- hibernate-core/src/main/antlr/hql.g | 23 ++ hibernate-core/src/main/antlr/sql-gen.g | 7 + .../hibernate/hql/internal/ast/HqlParser.java | 1 + .../hql/internal/ast/HqlSqlWalker.java | 15 ++ .../hql/internal/ast/SqlGenerator.java | 18 ++ .../hql/internal/ast/exec/DeleteExecutor.java | 1 + .../internal/ast/tree/CollectionPathNode.java | 248 ++++++++++++++++++ .../internal/ast/tree/CollectionSizeNode.java | 207 +++++++++++++++ .../hql/internal/ast/tree/ComponentJoin.java | 3 + .../hql/internal/ast/tree/FromElement.java | 4 + .../internal/ast/tree/ImpliedFromElement.java | 15 ++ .../hql/internal/ast/tree/MethodNode.java | 4 +- .../ComponentInWhereClauseTest.java | 13 + .../java/org/hibernate/test/hql/HQLTest.java | 13 +- .../test/hql/size/ManyToManySizeTest.java | 44 +++- 16 files changed, 630 insertions(+), 11 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java create mode 100644 hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java diff --git a/hibernate-core/src/main/antlr/hql-sql.g b/hibernate-core/src/main/antlr/hql-sql.g index 96c40bbbea..17a9d8ece3 100644 --- a/hibernate-core/src/main/antlr/hql-sql.g +++ b/hibernate-core/src/main/antlr/hql-sql.g @@ -244,6 +244,14 @@ tokens protected void handleResultVariableRef(AST resultVariableRef) throws SemanticException { } + protected AST createCollectionSizeFunction(AST collectionPath, boolean inSelect) throws SemanticException { + throw new UnsupportedOperationException( "Walker should implement" ); + } + + protected AST createCollectionPath(AST qualifier, AST reference) throws SemanticException { + throw new UnsupportedOperationException( "Walker should implement" ); + } + protected AST lookupProperty(AST dot,boolean root,boolean inSelect) throws SemanticException { return dot; } @@ -683,7 +691,10 @@ collectionFunction ; functionCall - : #(METHOD_CALL {inFunctionCall=true;} pathAsIdent ( #(EXPR_LIST (exprOrSubquery [ null ])* ) )? ) { + : #( COLL_SIZE path:collectionPath ) { + #functionCall = createCollectionSizeFunction( #path, inSelect ); + } + | #(METHOD_CALL {inFunctionCall=true;} pathAsIdent ( #(EXPR_LIST (exprOrSubquery [ null ])* ) )? ) { processFunction( #functionCall, inSelect ); inFunctionCall=false; } @@ -694,6 +705,18 @@ functionCall | #(AGGREGATE aggregateExpr ) ; +collectionPath! +// for now we do not support nested path refs. + : #( COLL_PATH ref:identifier (qualifier:collectionPathQualifier)? ) { + resolve( #qualifier ); + #collectionPath = createCollectionPath( #qualifier, #ref ); + } + ; + +collectionPathQualifier + : addrExpr [true] + ; + constant : literal | NULL diff --git a/hibernate-core/src/main/antlr/hql.g b/hibernate-core/src/main/antlr/hql.g index ac9e2a4ac3..31d75fe683 100644 --- a/hibernate-core/src/main/antlr/hql.g +++ b/hibernate-core/src/main/antlr/hql.g @@ -109,6 +109,7 @@ tokens CONSTRUCTOR; CASE2; // a "simple case statement", whereas CASE represents a "searched case statement" CAST; + COLL_PATH; EXPR_LIST; FILTER_ENTITY; // FROM element injected because of a filter expression (happens during compilation phase 2) IN_LIST; @@ -124,6 +125,7 @@ tokens RANGE; ROW_STAR; SELECT_FROM; + COLL_SIZE; UNARY_MINUS; UNARY_PLUS; VECTOR_EXPR; // ( x, y, z ) @@ -723,6 +725,7 @@ atom primaryExpression : { validateSoftKeyword("function") && LA(2) == OPEN && LA(3) == QUOTED_STRING }? jpaFunctionSyntax | { validateSoftKeyword("cast") && LA(2) == OPEN }? castFunction + | { validateSoftKeyword("size") && LA(2) == OPEN }? collectionSizeFunction | identPrimary ( options {greedy=true;} : DOT^ "class" )? | constant | parameter @@ -762,6 +765,26 @@ castTargetType : identifier { handleDotIdent(); } ( options { greedy=true; } : DOT^ identifier )* ; +collectionSizeFunction! + : s:IDENT OPEN p:collectionPath CLOSE { + assert #s.getText().equalsIgnoreCase( "size" ); + #collectionSizeFunction = #( [COLL_SIZE], #p ); + } + ; + +collectionPath! +// for now we do not support nested path refs (for embeddables) + : simpleRef:identifier { + #collectionPath = #( [COLL_PATH], #simpleRef ); + } + | qualifier:collectionPathQualifier DOT propertyName:identifier { + #collectionPath = #( [COLL_PATH], #propertyName, #qualifier ); + }; + +collectionPathQualifier + : identifier ( DOT^ { weakKeywords(); } identifier )* + ; + parameter : COLON^ { expectNamedParameterName(); } IDENT | PARAM^ (NUM_INT)? diff --git a/hibernate-core/src/main/antlr/sql-gen.g b/hibernate-core/src/main/antlr/sql-gen.g index 40d48f0343..c8eeda17f6 100644 --- a/hibernate-core/src/main/antlr/sql-gen.g +++ b/hibernate-core/src/main/antlr/sql-gen.g @@ -127,6 +127,10 @@ options { protected String renderOrderByElement(String expression, String order, String nulls) { throw new UnsupportedOperationException("Concrete SQL generator should override this method."); } + + protected void renderCollectionSize(AST collectionSizeNode) { + throw new UnsupportedOperationException( "Concrete SQL generator should override this method." ); + } } statement @@ -478,6 +482,9 @@ methodCall ( #(EXPR_LIST (arguments)? ) )? { endFunctionTemplate(m); } ) | #( c:CAST { beginFunctionTemplate(c,c); } castExpression {betweenFunctionArguments();} castTargetType { endFunctionTemplate(c); } ) + | cs:COLL_SIZE { + renderCollectionSize( #cs ); + } ; arguments diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java index 80130d8781..7c3fd925f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java @@ -109,6 +109,7 @@ public ParseErrorHandler getParseErrorHandler() { return parseErrorHandler; } + /** * Overrides the base behavior to retry keywords as identifiers. * diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java index d2e2c56dff..f1a0001e54 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java @@ -34,6 +34,8 @@ import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; import org.hibernate.hql.internal.ast.tree.CastFunctionNode; import org.hibernate.hql.internal.ast.tree.CollectionFunction; +import org.hibernate.hql.internal.ast.tree.CollectionPathNode; +import org.hibernate.hql.internal.ast.tree.CollectionSizeNode; import org.hibernate.hql.internal.ast.tree.ConstructorNode; import org.hibernate.hql.internal.ast.tree.DeleteStatement; import org.hibernate.hql.internal.ast.tree.DotNode; @@ -82,6 +84,7 @@ import org.hibernate.persister.collection.CollectionPropertyNames; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.PropertyMapping; import org.hibernate.persister.entity.Queryable; import org.hibernate.sql.JoinType; import org.hibernate.type.AssociationType; @@ -639,6 +642,13 @@ public JoinType getImpliedJoinType() { return impliedJoinType; } + @Override + protected AST createCollectionSizeFunction(AST collectionPath, boolean inSelect) throws SemanticException { + assert collectionPath instanceof CollectionPathNode; + + return new CollectionSizeNode( (CollectionPathNode) collectionPath, this ); + } + @Override protected AST lookupProperty(AST dot, boolean root, boolean inSelect) throws SemanticException { DotNode dotNode = (DotNode) dot; @@ -1221,6 +1231,11 @@ protected void processIndex(AST indexOp) throws SemanticException { indexNode.resolve( true, true ); } + @Override + protected AST createCollectionPath(AST qualifier, AST reference) throws SemanticException { + return CollectionPathNode.from( qualifier, reference, this ); + } + @Override protected void processFunction(AST functionCall, boolean inSelect) throws SemanticException { MethodNode methodNode = (MethodNode) functionCall; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java index 4db85674f0..1582651bcd 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java @@ -17,6 +17,8 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.hql.internal.antlr.SqlGeneratorBase; import org.hibernate.hql.internal.antlr.SqlTokenTypes; +import org.hibernate.hql.internal.ast.tree.CollectionPathNode; +import org.hibernate.hql.internal.ast.tree.CollectionSizeNode; import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.hql.internal.ast.tree.FunctionNode; import org.hibernate.hql.internal.ast.tree.Node; @@ -32,6 +34,7 @@ import org.hibernate.type.Type; import antlr.RecognitionException; +import antlr.SemanticException; import antlr.collections.AST; /** @@ -426,4 +429,19 @@ protected String renderOrderByElement(String expression, String order, String nu ); return sessionFactory.getDialect().renderOrderByElement( expression, null, order, nullPrecedence ); } + + @Override + protected void renderCollectionSize(AST ast) { + assert ast instanceof CollectionSizeNode; + + final CollectionSizeNode collectionSizeNode = (CollectionSizeNode) ast; + + // todo : or `#getStringBuilder()` directly? + try { + writer.clause( collectionSizeNode.toSqlExpression() ); + } + catch (SemanticException e) { + throw new QueryException( "Unable to render collection-size node" ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java index ef16c1e41f..9226f6ba65 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java @@ -19,6 +19,7 @@ import org.hibernate.hql.internal.ast.HqlSqlWalker; import org.hibernate.hql.internal.ast.SqlGenerator; import org.hibernate.hql.internal.ast.tree.DeleteStatement; +import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.metamodel.spi.MetamodelImplementor; import org.hibernate.param.ParameterSpecification; import org.hibernate.persister.collection.AbstractCollectionPersister; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java new file mode 100644 index 0000000000..9d8e2c1ffa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java @@ -0,0 +1,248 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.hql.internal.ast.tree; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.SqlASTFactory; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.QueryableCollection; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.PropertyMapping; +import org.hibernate.type.CollectionType; +import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; +import org.hibernate.type.Type; + +import antlr.SemanticException; +import antlr.collections.AST; + +/** + * @author Steve Ebersole + */ +public class CollectionPathNode extends SqlNode { + /** + * Used to resolve the collection "owner key" columns + */ + private final FromElement ownerFromElement; + + private final CollectionPersister collectionDescriptor; + + private final String collectionPropertyName; + private final String collectionPropertyPath; + private final String collectionQueryPath; + + + + /** + * Instantiate a `CollectionPathNode` + * + * @see #from(AST, AST, HqlSqlWalker) + */ + public CollectionPathNode( + FromElement ownerFromElement, + CollectionPersister collectionDescriptor, + String collectionPropertyName, + String collectionQueryPath, + String collectionPropertyPath) { + this.ownerFromElement = ownerFromElement; + this.collectionDescriptor = collectionDescriptor; + this.collectionPropertyName = collectionPropertyName; + this.collectionQueryPath = collectionQueryPath; + this.collectionPropertyPath = collectionPropertyPath; + + super.setType( SqlASTFactory.COLL_PATH ); + super.setDataType( collectionDescriptor.getCollectionType() ); + super.setText( collectionDescriptor.getRole() ); + } + + /** + * Factory for `CollectionPathNode` instances + * + * @param qualifier The left-hand-side of a dot-ident node - may be null to indicate an ident arg + * @param reference The right-hand-side of the dot-ident or the ident that is an unqualified reference + */ + public static CollectionPathNode from( + AST qualifier, + AST reference, + HqlSqlWalker walker) { + + final String referenceName = reference.getText(); + final String qualifierQueryPath = qualifier == null + ? "" + : ( (FromReferenceNode) qualifier ).getPath(); + final String referencePath = qualifier == null + ? referenceName + : qualifierQueryPath + "." + reference; + + if ( qualifier == null ) { + // If there is no qualifier it means the argument to `size()` was a simple IDENT node as opposed to a DOT-IDENT + // node. In this case, `reference` could technically be a join alias. This is not JPA + // compliant, but is a Hibernate-specific extension + + // size( cu ) + + final FromElement byAlias = walker.getCurrentFromClause().getFromElement( referenceName ); + + if ( byAlias != null ) { + final FromElement ownerRef = byAlias.getOrigin(); + final QueryableCollection collectionDescriptor = byAlias.getQueryableCollection(); + + return new CollectionPathNode( + ownerRef, + collectionDescriptor, + referenceName, + referencePath, + referenceName + ); + } + else { + // we (should) have an unqualified plural-attribute name - look through all of the defined from-elements + // and look for one that exposes that property + + //noinspection unchecked + final List fromElements = walker.getCurrentFromClause().getExplicitFromElements(); + + if ( fromElements.size() == 1 ) { + final FromElement ownerRef = fromElements.get( 0 ); + + final PropertyMapping collectionPropertyMapping = ownerRef.getPropertyMapping( referenceName ); + + //noinspection RedundantClassCall + if ( ! CollectionType.class.isInstance( collectionPropertyMapping.getType() ) ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + final CollectionType collectionType = (CollectionType) collectionPropertyMapping.getType(); + + return new CollectionPathNode( + ownerRef, + walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), + referenceName, + referencePath, + referenceName + ); + } + else { + FromElement discoveredQualifier = null; + + //noinspection ForLoopReplaceableByForEach + for ( int i = 0; i < fromElements.size(); i++ ) { + final FromElement fromElement = fromElements.get( i ); + try { + final PropertyMapping propertyMapping = fromElement.getPropertyMapping( referenceName ); + //noinspection RedundantClassCall + if ( ! CollectionType.class.isInstance( propertyMapping.getType() ) ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + discoveredQualifier = fromElement; + + break; + } + catch (Exception e) { + // try the next + } + } + + if ( discoveredQualifier == null ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + final FromElement ownerRef = discoveredQualifier; + + final PropertyMapping collectionPropertyMapping = ownerRef.getPropertyMapping( referenceName ); + + //noinspection RedundantClassCall + if ( ! CollectionType.class.isInstance( collectionPropertyMapping.getType() ) ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + final CollectionType collectionType = (CollectionType) collectionPropertyMapping.getType(); + + return new CollectionPathNode( + ownerRef, + walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), + referenceName, + referencePath, + referenceName + ); + } + } + } + else { + // we have a dot-ident structure + final FromReferenceNode qualifierFromReferenceNode = (FromReferenceNode) qualifier; + try { + qualifierFromReferenceNode.resolve( false, false ); + } + catch (SemanticException e) { + throw new QueryException( "Unable to resolve collection-path qualifier : " + qualifier.getText(), e ); + } + + final Type qualifierType = qualifierFromReferenceNode.getDataType(); + final FromElement ownerRef = ( (FromReferenceNode) qualifier ).getFromElement(); + + final CollectionType collectionType; + final String mappedPath; + + if ( qualifierType instanceof CompositeType ) { + final CompositeType qualifierCompositeType = (CompositeType) qualifierType; + final int collectionPropertyIndex = (qualifierCompositeType).getPropertyIndex( referenceName ); + collectionType = (CollectionType) qualifierCompositeType.getSubtypes()[collectionPropertyIndex]; + + if ( ownerRef instanceof ComponentJoin ) { + mappedPath = ( (ComponentJoin) ownerRef ).getComponentPath() + "." + referenceName; + } + else { + mappedPath = qualifierQueryPath.substring( qualifierQueryPath.indexOf( "." ) + 1 ); + } + } + else if ( qualifierType instanceof EntityType ) { + final EntityType qualifierEntityType = (EntityType) qualifierType; + final String entityName = qualifierEntityType.getAssociatedEntityName(); + final EntityPersister entityPersister = walker.getSessionFactoryHelper().findEntityPersisterByName( entityName ); + final int propertyIndex = entityPersister.getEntityMetamodel().getPropertyIndex( referenceName ); + collectionType = (CollectionType) entityPersister.getPropertyTypes()[ propertyIndex ]; + mappedPath = referenceName; + } + else { + throw new QueryException( "Unexpected collection-path reference qualifier type : " + qualifier ); + } + + return new CollectionPathNode( + ( (FromReferenceNode) qualifier ).getFromElement(), + walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), + referenceName, + referencePath, + mappedPath + ); + } + } + + public FromElement getCollectionOwnerRef() { + return ownerFromElement; + } + + public CollectionPersister getCollectionDescriptor() { + return collectionDescriptor; + } + + public String getCollectionPropertyName() { + return collectionPropertyName; + } + + public String getCollectionPropertyPath() { + return collectionPropertyPath; + } + + public String getCollectionQueryPath() { + return collectionQueryPath; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java new file mode 100644 index 0000000000..a90a4d7687 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java @@ -0,0 +1,207 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.hql.internal.ast.tree; + +import org.hibernate.AssertionFailure; +import org.hibernate.hql.internal.NameGenerator; +import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.internal.util.StringHelper; +import org.hibernate.persister.collection.CollectionPropertyMapping; +import org.hibernate.persister.collection.CollectionPropertyNames; +import org.hibernate.persister.collection.QueryableCollection; +import org.hibernate.persister.entity.Joinable; +import org.hibernate.type.StandardBasicTypes; + +import org.jboss.logging.Logger; + +import antlr.SemanticException; +import antlr.collections.AST; + +/** + * @author Steve Ebersole + */ +public class CollectionSizeNode extends SqlNode implements SelectExpression { + private static final Logger log = Logger.getLogger( CollectionSizeNode.class ); + + private final CollectionPathNode collectionPathNode; + private final CollectionPropertyMapping collectionPropertyMapping; + + private final HqlSqlWalker walker; + private String alias; + + public CollectionSizeNode(CollectionPathNode collectionPathNode, HqlSqlWalker walker) { + this.collectionPathNode = collectionPathNode; + this.walker = walker; + + this.collectionPropertyMapping = new CollectionPropertyMapping( (QueryableCollection) collectionPathNode.getCollectionDescriptor() ); + + setType( HqlSqlTokenTypes.COLL_SIZE ); + setDataType( StandardBasicTypes.INTEGER ); + setText( "collection-size" ); + } + + public CollectionPathNode getCollectionPathNode() { + return collectionPathNode; + } + + public HqlSqlWalker getWalker() { + return walker; + } + + public String toSqlExpression() throws SemanticException { + // generate subquery in the form: + // + // select count( alias_. ) + // from as alias_ + // where = alias_. + + // need: + // => QueryableCollection#getKeyColumnNames + // => QueryableCollection#getKeyColumnNames + // => QueryableCollection#getTableName + // => ??? + + + final FromElement collectionOwnerFromElement = collectionPathNode.getCollectionOwnerRef(); + final QueryableCollection collectionDescriptor = (QueryableCollection) collectionPathNode.getCollectionDescriptor(); + final String collectionPropertyName = collectionPathNode.getCollectionPropertyName(); + + getWalker().addQuerySpaces( collectionDescriptor.getCollectionSpaces() ); + + // silly : need to prime `SessionFactoryHelper#collectionPropertyMappingByRole` + walker.getSessionFactoryHelper().requireQueryableCollection( collectionDescriptor.getRole() ); + + // owner-key + final String[] ownerKeyColumns; + final AST ast = walker.getAST(); + final String ownerTableAlias; + if ( ast instanceof DeleteStatement || ast instanceof UpdateStatement ) { + ownerTableAlias = collectionOwnerFromElement.getTableName(); + } + else { + ownerTableAlias = collectionOwnerFromElement.getTableAlias(); + } + + final String lhsPropertyName = collectionDescriptor.getCollectionType().getLHSPropertyName(); + if ( lhsPropertyName == null ) { + ownerKeyColumns = StringHelper.qualify( + ownerTableAlias, + ( (Joinable) collectionDescriptor.getOwnerEntityPersister() ).getKeyColumnNames() + ); + } + else { + ownerKeyColumns = collectionOwnerFromElement.toColumns( ownerTableAlias, lhsPropertyName, true ); + } + + // collection-key + final String collectionTableAlias = collectionOwnerFromElement.getFromClause() + .getAliasGenerator() + .createName( collectionPathNode.getCollectionPropertyName() ); + final String[] collectionKeyColumns = StringHelper.qualify( collectionTableAlias, collectionDescriptor.getKeyColumnNames() ); + + + if ( collectionKeyColumns.length != ownerKeyColumns.length ) { + throw new AssertionFailure( "Mismatch between collection key columns" ); + } + + // PropertyMapping(c).toColumns(customers) + // PropertyMapping(c.customers).toColumns(SIZE) + + // size expression (the count function) + final String[] sizeColumns = this.collectionPropertyMapping.toColumns( + collectionTableAlias, + CollectionPropertyNames.COLLECTION_SIZE + ); + assert sizeColumns.length == 1; + final String sizeColumn = sizeColumns[0]; + + final StringBuilder buffer = new StringBuilder( "(select " ).append( sizeColumn ); + buffer.append( " from " ).append( collectionDescriptor.getTableName() ).append( " as " ).append( collectionTableAlias ); + buffer.append( " where " ); + + boolean firstPass = true; + for ( int i = 0; i < ownerKeyColumns.length; i++ ) { + if ( firstPass ) { + firstPass = false; + } + else { + buffer.append( " and " ); + } + + buffer.append( ownerKeyColumns[i] ).append( " = " ).append( collectionKeyColumns[i] ); + } + + buffer.append( ")" ); + + if ( scalarName != null ) { + buffer.append( " as " ).append( scalarName ); + } + + final String subQuery = buffer.toString(); + + log.debugf( + "toSqlExpression( size(%s) ) -> %s", + collectionPathNode.getCollectionQueryPath(), + subQuery + ); + + return subQuery; + } + + private String scalarName; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SelectExpression + + @Override + public void setScalarColumnText(int i) throws SemanticException { + log.debugf( "setScalarColumnText(%s)", i ); + scalarName = NameGenerator.scalarName( i, 0 ); + } + + @Override + public void setScalarColumn(int i) throws SemanticException { + log.debugf( "setScalarColumn(%s)", i ); + setScalarColumnText( i ); + } + + @Override + public int getScalarColumnIndex() { + return -1; + } + + @Override + public FromElement getFromElement() { + return null; + } + + @Override + public boolean isConstructor() { + return false; + } + + @Override + public boolean isReturnableEntity() throws SemanticException { + return false; + } + + @Override + public boolean isScalar() throws SemanticException { + return false; + } + + @Override + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + public String getAlias() { + return alias; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java index 2ea9e55d42..a44391c5f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java @@ -152,6 +152,9 @@ protected String getPropertyPath(String propertyName) { return getComponentPath() + '.' + propertyName; } + // `size( c.component.customers )` + // PropertyMapping(c).toColumns( component.customers ) + @Override public String[] toColumns(String alias, String propertyName) throws QueryException { return getBasePropertyMapping().toColumns( alias, getPropertyPath( propertyName ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java index 7c0db88a37..b4cf19dd7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java @@ -97,6 +97,10 @@ protected FromElement( } + public FromElementType getElementType() { + return elementType; + } + protected void initializeComponentJoin(FromElementType elementType) { fromClause.registerFromElement( this ); elementType.applyTreatAsDeclarations( getWalker().getTreatAsDeclarationsByPath( classAlias ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java index d5e36f32d0..5f6141e5bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java @@ -23,6 +23,21 @@ public class ImpliedFromElement extends FromElement { */ private boolean inProjectionList; + /** + * Here to add debug breakpoints + */ + @SuppressWarnings("unused") + public ImpliedFromElement() { + super(); + } + + /** + * Here to add debug breakpoints + */ + public ImpliedFromElement(FromClause fromClause, FromElement origin, String alias) { + super( fromClause, origin, alias ); + } + public boolean isImplied() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java index 219a8411c9..31584a153d 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java @@ -154,11 +154,13 @@ protected void resolveCollectionProperty(AST expr) throws SemanticException { selectColumns = cpr.toColumns( fromElement.getTableAlias() ); // setDataType( fromElement.getPropertyType( propertyName, propertyName ) ); - selectColumns = fromElement.toColumns( fromElement.getTableAlias(), propertyName, inSelect ); +// selectColumns = fromElement.toColumns( fromElement.getTableAlias(), propertyName, inSelect ); } + if ( collectionNode instanceof DotNode ) { prepareAnyImplicitJoins( (DotNode) collectionNode ); } + if ( !inSelect ) { fromElement.setText( "" ); fromElement.setUseWhereFragment( false ); diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java index 0750751544..755d49e55c 100644 --- a/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java @@ -34,6 +34,7 @@ import org.hibernate.testing.TestForIssue; import org.hibernate.testing.transaction.TransactionUtil; +import org.hibernate.testing.transaction.TransactionUtil2; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -96,6 +97,18 @@ public void testSizeExpressionForTheOneToManyPropertyOfAComponent() { ); } + @Test + public void testSizeExpressionForTheOneToManyPropertyOfAComponentHql() { + TransactionUtil2.inTransaction( + entityManagerFactory(), + session -> { + final String hql = "from Employee e where size( e.projects.previousProjects ) = 2"; + final List resultsList = session.createQuery( hql ).list(); + assertThat( resultsList.size(), is( 1 ) ); + } + ); + } + @Test public void testSizeExpressionForTheElementCollectionPropertyOfAComponent() { TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java index 4d256a378f..3131958c32 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java @@ -60,6 +60,7 @@ import org.hibernate.testing.RequiresDialectFeature; import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.TestForIssue; +import org.junit.Ignore; import org.junit.Test; import antlr.RecognitionException; @@ -571,6 +572,7 @@ public void testCrazyIdFieldNames() { } @Test + @Ignore( "Old parser generated incorrect SQL for `size()`") public void testSizeFunctionAndProperty() { assertTranslation("from Animal a where a.offspring.size > 0"); assertTranslation("from Animal a join a.offspring where a.offspring.size > 1"); @@ -624,16 +626,9 @@ public void testSubselectImplicitJoins() { assertTranslation( "from Simple s where s = some( select sim from Simple sim where sim.other.count=s.other.count )" ); } - @Test - public void testCollectionOfValuesSize() throws Exception { - //SQL *was* missing a comma - assertTranslation( "select size(baz.stringDateMap) from org.hibernate.test.legacy.Baz baz" ); - } - @Test public void testCollectionFunctions() throws Exception { //these are both broken, a join that belongs in the subselect finds its way into the main query - assertTranslation( "from Zoo zoo where size(zoo.animals) > 100" ); assertTranslation( "from Zoo zoo where maxindex(zoo.mammals) = 'dog'" ); } @@ -651,8 +646,10 @@ public void testImpliedManyToManyProperty() throws Exception { } @Test - public void testCollectionSize() throws Exception { + @Ignore( "The old parser generated incorrect SQL for selection of size functions" ) + public void testCollectionSizeSelection() throws Exception { assertTranslation( "select size(zoo.animals) from Zoo zoo" ); + assertTranslation( "select size(baz.stringDateMap) from org.hibernate.test.legacy.Baz baz" ); } @Test diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java index 7414e53635..371c570b59 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java @@ -31,7 +31,49 @@ public class ManyToManySizeTest extends BaseNonConfigCoreFunctionalTestCase { @Test - public void testSizeAsSelectExpression() { + public void testSizeAsRestriction() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select c.id from Company c where size( c.customers ) = 0" + ).list(); + assertThat( results.size(), is( 1 ) ); + assertThat( results.get( 0 ), is( 0 ) ); + } + ); + } + + @Test + public void testSizeAsCompoundSelectExpression() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select c.id, c.name, size( c.customers )" + + " from Company c" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 3 ) ); + + final Object[] first = (Object[]) results.get( 0 ); + assertThat( first[ 0 ], is( 0 ) ); + assertThat( first[ 2 ], is( 0 ) ); + + final Object[] second = (Object[]) results.get( 1 ); + assertThat( second[ 0 ], is( 1 ) ); + assertThat( second[ 2 ], is( 1 ) ); + + final Object[] third = (Object[]) results.get( 2 ); + assertThat( third[ 0 ], is( 2 ) ); + assertThat( third[ 2 ], is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsCtorSelectExpression() { doInHibernate( this::sessionFactory, session -> { From 336c3b9e30616bdfe024a2ce3d802037c54f0b60 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 4 Mar 2020 12:27:46 -0600 Subject: [PATCH 3/8] HHH-13619 - Support for JPA's `size` function as a select expression - code cleanup --- .../hql/internal/ast/HqlSqlWalker.java | 4 +- .../hql/internal/ast/SqlGenerator.java | 12 ++-- .../internal/ast/tree/CollectionPathNode.java | 45 ++++++++++++-- .../internal/ast/tree/CollectionSizeNode.java | 62 +++---------------- 4 files changed, 57 insertions(+), 66 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java index f1a0001e54..c6db01e8ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java @@ -84,7 +84,6 @@ import org.hibernate.persister.collection.CollectionPropertyNames; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.PropertyMapping; import org.hibernate.persister.entity.Queryable; import org.hibernate.sql.JoinType; import org.hibernate.type.AssociationType; @@ -645,8 +644,7 @@ public JoinType getImpliedJoinType() { @Override protected AST createCollectionSizeFunction(AST collectionPath, boolean inSelect) throws SemanticException { assert collectionPath instanceof CollectionPathNode; - - return new CollectionSizeNode( (CollectionPathNode) collectionPath, this ); + return new CollectionSizeNode( (CollectionPathNode) collectionPath ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java index 1582651bcd..8776121c52 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java @@ -423,9 +423,9 @@ protected void nestedFromFragment(AST d, AST parent) { @Override protected String renderOrderByElement(String expression, String order, String nulls) { - final NullPrecedence nullPrecedence = NullPrecedence.parse( nulls, - sessionFactory.getSettings() - .getDefaultNullPrecedence() + final NullPrecedence nullPrecedence = NullPrecedence.parse( + nulls, + sessionFactory.getSettings().getDefaultNullPrecedence() ); return sessionFactory.getDialect().renderOrderByElement( expression, null, order, nullPrecedence ); } @@ -436,11 +436,13 @@ protected void renderCollectionSize(AST ast) { final CollectionSizeNode collectionSizeNode = (CollectionSizeNode) ast; - // todo : or `#getStringBuilder()` directly? try { writer.clause( collectionSizeNode.toSqlExpression() ); } - catch (SemanticException e) { + catch (QueryException qe) { + throw qe; + } + catch (Exception e) { throw new QueryException( "Unable to render collection-size node" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java index 9d8e2c1ffa..1c99ac1828 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java @@ -11,9 +11,11 @@ import org.hibernate.QueryException; import org.hibernate.hql.internal.ast.HqlSqlWalker; import org.hibernate.hql.internal.ast.SqlASTFactory; +import org.hibernate.internal.util.StringHelper; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.Joinable; import org.hibernate.persister.entity.PropertyMapping; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; @@ -38,6 +40,7 @@ public class CollectionPathNode extends SqlNode { private final String collectionPropertyPath; private final String collectionQueryPath; + private final HqlSqlWalker walker; /** @@ -50,12 +53,16 @@ public CollectionPathNode( CollectionPersister collectionDescriptor, String collectionPropertyName, String collectionQueryPath, - String collectionPropertyPath) { + String collectionPropertyPath, + HqlSqlWalker walker) { this.ownerFromElement = ownerFromElement; this.collectionDescriptor = collectionDescriptor; this.collectionPropertyName = collectionPropertyName; this.collectionQueryPath = collectionQueryPath; this.collectionPropertyPath = collectionPropertyPath; + this.walker = walker; + + walker.addQuerySpaces( collectionDescriptor.getCollectionSpaces() ); super.setType( SqlASTFactory.COLL_PATH ); super.setDataType( collectionDescriptor.getCollectionType() ); @@ -99,7 +106,8 @@ public static CollectionPathNode from( collectionDescriptor, referenceName, referencePath, - referenceName + referenceName, + walker ); } else { @@ -126,7 +134,8 @@ public static CollectionPathNode from( walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), referenceName, referencePath, - referenceName + referenceName, + walker ); } else { @@ -171,7 +180,8 @@ public static CollectionPathNode from( walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), referenceName, referencePath, - referenceName + referenceName, + walker ); } } @@ -221,12 +231,13 @@ else if ( qualifierType instanceof EntityType ) { walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), referenceName, referencePath, - mappedPath + mappedPath, + walker ); } } - public FromElement getCollectionOwnerRef() { + public FromElement getCollectionOwnerFromElement() { return ownerFromElement; } @@ -245,4 +256,26 @@ public String getCollectionPropertyPath() { public String getCollectionQueryPath() { return collectionQueryPath; } + + public String[] resolveOwnerKeyColumnExpressions() { + final AST ast = walker.getAST(); + final String ownerTableAlias; + if ( ast instanceof DeleteStatement || ast instanceof UpdateStatement ) { + ownerTableAlias = ownerFromElement.getTableName(); + } + else { + ownerTableAlias = ownerFromElement.getTableAlias(); + } + + final String lhsPropertyName = collectionDescriptor.getCollectionType().getLHSPropertyName(); + if ( lhsPropertyName == null ) { + return StringHelper.qualify( + ownerTableAlias, + ( (Joinable) collectionDescriptor.getOwnerEntityPersister() ).getKeyColumnNames() + ); + } + else { + return ownerFromElement.toColumns( ownerTableAlias, lhsPropertyName, true ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java index a90a4d7687..ad8390d7d4 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java @@ -9,19 +9,14 @@ import org.hibernate.AssertionFailure; import org.hibernate.hql.internal.NameGenerator; import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; -import org.hibernate.hql.internal.ast.HqlSqlWalker; import org.hibernate.internal.util.StringHelper; import org.hibernate.persister.collection.CollectionPropertyMapping; import org.hibernate.persister.collection.CollectionPropertyNames; import org.hibernate.persister.collection.QueryableCollection; -import org.hibernate.persister.entity.Joinable; import org.hibernate.type.StandardBasicTypes; import org.jboss.logging.Logger; -import antlr.SemanticException; -import antlr.collections.AST; - /** * @author Steve Ebersole */ @@ -31,13 +26,10 @@ public class CollectionSizeNode extends SqlNode implements SelectExpression { private final CollectionPathNode collectionPathNode; private final CollectionPropertyMapping collectionPropertyMapping; - private final HqlSqlWalker walker; private String alias; - public CollectionSizeNode(CollectionPathNode collectionPathNode, HqlSqlWalker walker) { + public CollectionSizeNode(CollectionPathNode collectionPathNode) { this.collectionPathNode = collectionPathNode; - this.walker = walker; - this.collectionPropertyMapping = new CollectionPropertyMapping( (QueryableCollection) collectionPathNode.getCollectionDescriptor() ); setType( HqlSqlTokenTypes.COLL_SIZE ); @@ -45,15 +37,12 @@ public CollectionSizeNode(CollectionPathNode collectionPathNode, HqlSqlWalker wa setText( "collection-size" ); } + @SuppressWarnings("unused") public CollectionPathNode getCollectionPathNode() { return collectionPathNode; } - public HqlSqlWalker getWalker() { - return walker; - } - - public String toSqlExpression() throws SemanticException { + public String toSqlExpression() { // generate subquery in the form: // // select count( alias_. ) @@ -67,36 +56,10 @@ public String toSqlExpression() throws SemanticException { // => ??? - final FromElement collectionOwnerFromElement = collectionPathNode.getCollectionOwnerRef(); + final String[] ownerKeyColumns = collectionPathNode.resolveOwnerKeyColumnExpressions(); + + final FromElement collectionOwnerFromElement = collectionPathNode.getCollectionOwnerFromElement(); final QueryableCollection collectionDescriptor = (QueryableCollection) collectionPathNode.getCollectionDescriptor(); - final String collectionPropertyName = collectionPathNode.getCollectionPropertyName(); - - getWalker().addQuerySpaces( collectionDescriptor.getCollectionSpaces() ); - - // silly : need to prime `SessionFactoryHelper#collectionPropertyMappingByRole` - walker.getSessionFactoryHelper().requireQueryableCollection( collectionDescriptor.getRole() ); - - // owner-key - final String[] ownerKeyColumns; - final AST ast = walker.getAST(); - final String ownerTableAlias; - if ( ast instanceof DeleteStatement || ast instanceof UpdateStatement ) { - ownerTableAlias = collectionOwnerFromElement.getTableName(); - } - else { - ownerTableAlias = collectionOwnerFromElement.getTableAlias(); - } - - final String lhsPropertyName = collectionDescriptor.getCollectionType().getLHSPropertyName(); - if ( lhsPropertyName == null ) { - ownerKeyColumns = StringHelper.qualify( - ownerTableAlias, - ( (Joinable) collectionDescriptor.getOwnerEntityPersister() ).getKeyColumnNames() - ); - } - else { - ownerKeyColumns = collectionOwnerFromElement.toColumns( ownerTableAlias, lhsPropertyName, true ); - } // collection-key final String collectionTableAlias = collectionOwnerFromElement.getFromClause() @@ -104,15 +67,10 @@ public String toSqlExpression() throws SemanticException { .createName( collectionPathNode.getCollectionPropertyName() ); final String[] collectionKeyColumns = StringHelper.qualify( collectionTableAlias, collectionDescriptor.getKeyColumnNames() ); - if ( collectionKeyColumns.length != ownerKeyColumns.length ) { throw new AssertionFailure( "Mismatch between collection key columns" ); } - // PropertyMapping(c).toColumns(customers) - // PropertyMapping(c.customers).toColumns(SIZE) - - // size expression (the count function) final String[] sizeColumns = this.collectionPropertyMapping.toColumns( collectionTableAlias, CollectionPropertyNames.COLLECTION_SIZE @@ -159,13 +117,13 @@ public String toSqlExpression() throws SemanticException { // SelectExpression @Override - public void setScalarColumnText(int i) throws SemanticException { + public void setScalarColumnText(int i) { log.debugf( "setScalarColumnText(%s)", i ); scalarName = NameGenerator.scalarName( i, 0 ); } @Override - public void setScalarColumn(int i) throws SemanticException { + public void setScalarColumn(int i) { log.debugf( "setScalarColumn(%s)", i ); setScalarColumnText( i ); } @@ -186,12 +144,12 @@ public boolean isConstructor() { } @Override - public boolean isReturnableEntity() throws SemanticException { + public boolean isReturnableEntity() { return false; } @Override - public boolean isScalar() throws SemanticException { + public boolean isScalar() { return false; } From 26ab3c536296193304e906bc478f58082d6f8420 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 5 Mar 2020 09:58:35 -0600 Subject: [PATCH 4/8] HHH-13619 - Support for JPA's `size` function as a select expression - PR revisions --- .../internal/ast/tree/CollectionPathNode.java | 49 ++++++++++++------- .../internal/ast/tree/CollectionSizeNode.java | 30 +++++------- .../internal/ast/tree/ImpliedFromElement.java | 7 --- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java index 1c99ac1828..8a0e1dda7e 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java @@ -48,6 +48,7 @@ public class CollectionPathNode extends SqlNode { * * @see #from(AST, AST, HqlSqlWalker) */ + @SuppressWarnings("WeakerAccess") public CollectionPathNode( FromElement ownerFromElement, CollectionPersister collectionDescriptor, @@ -81,19 +82,14 @@ public static CollectionPathNode from( HqlSqlWalker walker) { final String referenceName = reference.getText(); - final String qualifierQueryPath = qualifier == null - ? "" - : ( (FromReferenceNode) qualifier ).getPath(); - final String referencePath = qualifier == null - ? referenceName - : qualifierQueryPath + "." + reference; + final String referenceQueryPath; if ( qualifier == null ) { // If there is no qualifier it means the argument to `size()` was a simple IDENT node as opposed to a DOT-IDENT // node. In this case, `reference` could technically be a join alias. This is not JPA // compliant, but is a Hibernate-specific extension - // size( cu ) + referenceQueryPath = referenceName; final FromElement byAlias = walker.getCurrentFromClause().getFromElement( referenceName ); @@ -105,7 +101,7 @@ public static CollectionPathNode from( ownerRef, collectionDescriptor, referenceName, - referencePath, + referenceQueryPath, referenceName, walker ); @@ -133,7 +129,7 @@ public static CollectionPathNode from( ownerRef, walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), referenceName, - referencePath, + referenceQueryPath, referenceName, walker ); @@ -179,7 +175,7 @@ public static CollectionPathNode from( ownerRef, walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), referenceName, - referencePath, + referenceQueryPath, referenceName, walker ); @@ -189,18 +185,22 @@ public static CollectionPathNode from( else { // we have a dot-ident structure final FromReferenceNode qualifierFromReferenceNode = (FromReferenceNode) qualifier; + + final String qualifierQueryPath = ( (FromReferenceNode) qualifier ).getPath(); + referenceQueryPath = qualifierQueryPath + "." + reference; + try { qualifierFromReferenceNode.resolve( false, false ); } catch (SemanticException e) { - throw new QueryException( "Unable to resolve collection-path qualifier : " + qualifier.getText(), e ); + throw new QueryException( "Unable to resolve collection-path qualifier : " + qualifierQueryPath, e ); } final Type qualifierType = qualifierFromReferenceNode.getDataType(); final FromElement ownerRef = ( (FromReferenceNode) qualifier ).getFromElement(); final CollectionType collectionType; - final String mappedPath; + final String referenceMappedPath; if ( qualifierType instanceof CompositeType ) { final CompositeType qualifierCompositeType = (CompositeType) qualifierType; @@ -208,10 +208,10 @@ public static CollectionPathNode from( collectionType = (CollectionType) qualifierCompositeType.getSubtypes()[collectionPropertyIndex]; if ( ownerRef instanceof ComponentJoin ) { - mappedPath = ( (ComponentJoin) ownerRef ).getComponentPath() + "." + referenceName; + referenceMappedPath = ( (ComponentJoin) ownerRef ).getComponentPath() + "." + referenceName; } else { - mappedPath = qualifierQueryPath.substring( qualifierQueryPath.indexOf( "." ) + 1 ); + referenceMappedPath = qualifierQueryPath.substring( qualifierQueryPath.indexOf( "." ) + 1 ); } } else if ( qualifierType instanceof EntityType ) { @@ -220,7 +220,7 @@ else if ( qualifierType instanceof EntityType ) { final EntityPersister entityPersister = walker.getSessionFactoryHelper().findEntityPersisterByName( entityName ); final int propertyIndex = entityPersister.getEntityMetamodel().getPropertyIndex( referenceName ); collectionType = (CollectionType) entityPersister.getPropertyTypes()[ propertyIndex ]; - mappedPath = referenceName; + referenceMappedPath = referenceName; } else { throw new QueryException( "Unexpected collection-path reference qualifier type : " + qualifier ); @@ -230,33 +230,48 @@ else if ( qualifierType instanceof EntityType ) { ( (FromReferenceNode) qualifier ).getFromElement(), walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), referenceName, - referencePath, - mappedPath, + referenceQueryPath, + referenceMappedPath, walker ); } } + @SuppressWarnings("WeakerAccess") public FromElement getCollectionOwnerFromElement() { return ownerFromElement; } + @SuppressWarnings("WeakerAccess") public CollectionPersister getCollectionDescriptor() { return collectionDescriptor; } + /** + * The name of the collection property - e.g. `theCollection` + */ public String getCollectionPropertyName() { return collectionPropertyName; } + /** + * The collection property path relative to the "owning entity" in the mapping model - e.g. `theCollection` + * or `someEmbeddable.theCollection`. + */ + @SuppressWarnings("unused") public String getCollectionPropertyPath() { return collectionPropertyPath; } + /** + * The "full" path. E.g. `a.theCollection` or `a.someEmbeddable.theCollection` + */ + @SuppressWarnings("WeakerAccess") public String getCollectionQueryPath() { return collectionQueryPath; } + @SuppressWarnings("WeakerAccess") public String[] resolveOwnerKeyColumnExpressions() { final AST ast = walker.getAST(); final String ownerTableAlias; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java index ad8390d7d4..4969a6af31 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java @@ -43,28 +43,23 @@ public CollectionPathNode getCollectionPathNode() { } public String toSqlExpression() { - // generate subquery in the form: - // - // select count( alias_. ) - // from as alias_ - // where = alias_. - - // need: - // => QueryableCollection#getKeyColumnNames - // => QueryableCollection#getKeyColumnNames - // => QueryableCollection#getTableName - // => ??? - - - final String[] ownerKeyColumns = collectionPathNode.resolveOwnerKeyColumnExpressions(); - final FromElement collectionOwnerFromElement = collectionPathNode.getCollectionOwnerFromElement(); final QueryableCollection collectionDescriptor = (QueryableCollection) collectionPathNode.getCollectionDescriptor(); - // collection-key + // generate subquery in the form: + // + // select count( alias_. ) + // from as alias_ + // where = alias_. + + // Note that `collectionPropertyMapping.toColumns(.., COLLECTION_SIZE)` returns the complete `count(...)` SQL + // expression, hence he expectation for a single expression regardless of the number of columns in the key. + final String collectionTableAlias = collectionOwnerFromElement.getFromClause() .getAliasGenerator() .createName( collectionPathNode.getCollectionPropertyName() ); + + final String[] ownerKeyColumns = collectionPathNode.resolveOwnerKeyColumnExpressions(); final String[] collectionKeyColumns = StringHelper.qualify( collectionTableAlias, collectionDescriptor.getKeyColumnNames() ); if ( collectionKeyColumns.length != ownerKeyColumns.length ) { @@ -111,11 +106,12 @@ public String toSqlExpression() { return subQuery; } - private String scalarName; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SelectExpression + private String scalarName; + @Override public void setScalarColumnText(int i) { log.debugf( "setScalarColumnText(%s)", i ); diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java index 5f6141e5bf..ccb3b240cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java @@ -31,13 +31,6 @@ public ImpliedFromElement() { super(); } - /** - * Here to add debug breakpoints - */ - public ImpliedFromElement(FromClause fromClause, FromElement origin, String alias) { - super( fromClause, origin, alias ); - } - public boolean isImplied() { return true; } From 8c6f8025e32f422857f0a29fd142c4306b658b9a Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Tue, 10 Mar 2020 13:54:42 -0700 Subject: [PATCH 5/8] HHH-13619 - Support for JPA's `size` function as a select expression - Fix to work on Oracle by removing "as" between table name and alias --- .../org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java index 4969a6af31..a8f868ea66 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java @@ -74,7 +74,7 @@ public String toSqlExpression() { final String sizeColumn = sizeColumns[0]; final StringBuilder buffer = new StringBuilder( "(select " ).append( sizeColumn ); - buffer.append( " from " ).append( collectionDescriptor.getTableName() ).append( " as " ).append( collectionTableAlias ); + buffer.append( " from " ).append( collectionDescriptor.getTableName() ).append( " " ).append( collectionTableAlias ); buffer.append( " where " ); boolean firstPass = true; From 486addab0cef2a9e886b615d35da0620133894d7 Mon Sep 17 00:00:00 2001 From: Sanne Grinovero Date: Wed, 18 Mar 2020 16:06:15 +0000 Subject: [PATCH 6/8] HHH-13897 ResultSetProcessingContextImpl: no need to clear collections before discarding the reference to them --- .../exec/process/internal/ResultSetProcessingContextImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/process/internal/ResultSetProcessingContextImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/process/internal/ResultSetProcessingContextImpl.java index 8b9cfe8486..57bc45bc69 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/process/internal/ResultSetProcessingContextImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/process/internal/ResultSetProcessingContextImpl.java @@ -322,12 +322,10 @@ void wrapUp() { createSubselects(); if ( hydratedEntityRegistrationList != null ) { - hydratedEntityRegistrationList.clear(); hydratedEntityRegistrationList = null; } if ( subselectLoadableEntityKeyMap != null ) { - subselectLoadableEntityKeyMap.clear(); subselectLoadableEntityKeyMap = null; } } From b35ccc8e37b5704ff4589be5b0cf7e89aedfacb9 Mon Sep 17 00:00:00 2001 From: Romain Moreau Date: Fri, 8 Nov 2019 13:56:19 +0100 Subject: [PATCH 7/8] HHH-13711: drop constraints enabled for H2 --- .../src/main/java/org/hibernate/dialect/H2Dialect.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index f36701b32c..73e3d39f75 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -435,10 +435,8 @@ public boolean supportsTuplesInSubqueries() { } @Override - public boolean dropConstraints() { - // We don't need to drop constraints before dropping tables, that just leads to error - // messages about missing tables when we don't have a schema in the database - return false; + public boolean supportsIfExistsAfterAlterTable() { + return true; } @Override From b5443deab3cff37826ab9a22e61995eb81bc3b97 Mon Sep 17 00:00:00 2001 From: romainmoreau <1763676+romainmoreau@users.noreply.github.com> Date: Sat, 8 Feb 2020 16:17:42 +0100 Subject: [PATCH 8/8] Drop constraints using CASCADE Co-Authored-By: William Cekan --- .../java/org/hibernate/dialect/H2Dialect.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 73e3d39f75..7e2a85d8bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -259,11 +259,6 @@ public boolean bindLimitParametersFirst() { return false; } - @Override - public boolean supportsIfExistsAfterTableName() { - return true; - } - @Override public boolean supportsIfExistsBeforeConstraintName() { return true; @@ -434,8 +429,25 @@ public boolean supportsTuplesInSubqueries() { return false; } + // Do not drop constraints explicitly, just do this by cascading instead. @Override - public boolean supportsIfExistsAfterAlterTable() { + public boolean dropConstraints() { + return false; + } + + @Override + public String getCascadeConstraintsString() { + return " CASCADE "; + } + + // CASCADE has to be AFTER IF EXISTS in case it's after the tablename + @Override + public boolean supportsIfExistsAfterTableName() { + return false; + } + + @Override + public boolean supportsIfExistsBeforeTableName() { return true; }