This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 182b06144b19c452085a3574526175debefe75bb Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Mar 20 08:15:59 2026 -0500 hibernate 7: PredicateGenerator cleaned up and spec fixed --- grails-data-hibernate7/core/ISSUES.md | 33 ++--------- .../orm/hibernate/query/PredicateGenerator.java | 50 ++++++++-------- .../hibernatequery/PredicateGeneratorSpec.groovy | 68 +++++++++++++++++++++- 3 files changed, 95 insertions(+), 56 deletions(-) diff --git a/grails-data-hibernate7/core/ISSUES.md b/grails-data-hibernate7/core/ISSUES.md index d27598ec91..b7e1d73a9a 100644 --- a/grails-data-hibernate7/core/ISSUES.md +++ b/grails-data-hibernate7/core/ISSUES.md @@ -5,15 +5,14 @@ BasicCollectionInQuerySpec ByteBuddyGroovyInterceptorSpec DetachedAssociationFunctionSpec +DetachedCriteriaProjectionAliasSpec HibernateMappingFactorySpec HibernateProxyHandler7Spec -JpaCriteriaQueryCreatorSpec -JpaFromProviderSpec -PredicateGeneratorSpec -WhereQueryBugFixSpec WhereQueryOldIssueVerificationSpec +--- + ### 3. ByteBuddy Proxy Initialization & Interception **Symptoms:** - `ByteBuddyGroovyInterceptorSpec` and `HibernateProxyHandler7Spec` failures. @@ -24,17 +23,8 @@ Hibernate 7's `ByteBuddyInterceptor.intercept()` does not distinguish between ac --- -### 4. JpaFromProvider & JpaCriteriaQueryCreator (Joins and Aliases) -**Symptoms:** -- `NullPointerException: Cannot invoke "jakarta.persistence.criteria.Join.alias(String)" because "table" is null` -- Association projection paths fail to resolve correctly in complex queries. - -**Description:** -Referencing an association in a projection (e.g., `projections { property('owner.name') }`) requires an automatic join that wasn't previously necessary or was handled differently. The fix in `JpaFromProvider` requires robust mock handling in tests to avoid NPEs during alias assignment. - --- - --- ### 6. MappingException: Class 'java.util.Set' does not implement 'UserCollectionType' @@ -43,18 +33,9 @@ Referencing an association in a projection (e.g., `projections { property('owner - Affects `BasicCollectionInQuerySpec`. **Description:** -Hibernate 7 changed how collection types are resolved. Some tests using `hasMany` with default collection types are failing because Hibernate 7 expects a specific `UserCollectionType` implementation when a custom type is inferred or explicitly mapped. +Hibernate 7 changed how collection types are resolved. Some tests using `hasMany` with default collection types are failing during `buildSessionFactory`. --- - -### 7. TerminalPathException in SQM Paths -**Symptoms:** -- `org.hibernate.query.sqm.TerminalPathException: Terminal path 'id' has no attribute 'id'` -- Affects `PredicateGeneratorSpec` and `WhereQueryBugFixSpec`. - -**Description:** -In Hibernate 7, once a path is resolved to a terminal attribute (like `id`), further navigation on that path (e.g., trying to access a property on the ID) triggers this exception. This affects how GORM constructs subqueries and criteria filters. - --- ### 8. IDENTITY Generator Default in TCK @@ -66,9 +47,3 @@ The TCK Manager now globally sets `id generator: 'identity'` to avoid `SequenceS --- -### 9. HibernateGormStaticApi HQL Overloads -**Symptoms:** -- `HibernateGormStaticApiSpec` failures related to `executeQuery` and `executeUpdate`. - -**Description:** -Hibernate 7's stricter query parameter rules and the removal of certain `Query` overloads require that HQL strings be handled carefully, especially when mixing positional and named parameters or passing GORM-specific options (like `flushMode`). diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java index ac635ab9b8..5d2f946ae0 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java @@ -29,11 +29,12 @@ import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConversionService; -import grails.gorm.DetachedCriteria; import org.grails.datastore.gorm.GormEntity; import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria; import org.grails.datastore.mapping.core.exceptions.ConfigurationException; import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.model.PersistentProperty; +import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.query.Query; import org.grails.datastore.mapping.query.api.QueryableCriteria; @@ -85,7 +86,7 @@ public class PredicateGenerator { } else if (criterion instanceof Query.DistinctProjection) { return cb.conjunction(); } else if (criterion instanceof DetachedAssociationCriteria<?> c) { - return handleAssociationCriteria(cb, criteriaQuery, root, fromsByProvider, entity, c); + return handleAssociationCriteria(cb, criteriaQuery, root, fromsByProvider, c); } else if (criterion instanceof HibernateAssociationQuery haq) { return handleHibernateAssociationQuery(cb, criteriaQuery, root, fromsByProvider, haq); } else if (criterion instanceof Query.PropertyCriterion pc) { @@ -95,10 +96,8 @@ public class PredicateGenerator { } else if (criterion instanceof Query.PropertyNameCriterion c) { return handlePropertyNameCriterion(cb, fromsByProvider, c); } else if (criterion instanceof Query.Exists c) { - // FIX: Pass the child entity from the subquery context return handleExists(cb, criteriaQuery, root, fromsByProvider, c.getSubquery().getPersistentEntity(), c); } else if (criterion instanceof Query.NotExists c) { - // FIX: Pass the child entity from the subquery context PersistentEntity childEntity = c.getSubquery().getPersistentEntity(); return cb.not(handleExists(cb, criteriaQuery, root, fromsByProvider, childEntity, new Query.Exists(c.getSubquery()))); } @@ -132,14 +131,12 @@ public class PredicateGenerator { CriteriaQuery<?> criteriaQuery, From<?, ?> root, JpaFromProvider fromsByProvider, - PersistentEntity entity, DetachedAssociationCriteria<?> c) { var child = root.join(c.getAssociationPath(), JoinType.LEFT); JpaFromProvider childTablesByName = (JpaFromProvider) fromsByProvider.clone(); childTablesByName.put("root", child); - // FIX: Get the target PersistentEntity from the association PersistentEntity associatedEntity = c.getAssociation().getAssociatedEntity(); return cb.and(getPredicates( @@ -148,7 +145,7 @@ public class PredicateGenerator { child, c.getCriteria(), childTablesByName, - associatedEntity // Pass the entity, not the association + associatedEntity )); } @@ -161,7 +158,6 @@ public class PredicateGenerator { var child = root.join(haq.associationPath, JoinType.LEFT); JpaFromProvider childFroms = (JpaFromProvider) fromsByProvider.clone(); childFroms.put("root", child); - // haq.getEntity() is already the correct child entity return cb.and(getPredicates(cb, criteriaQuery, child, haq.getAssociationCriteria(), childFroms, haq.getEntity())); } @@ -173,7 +169,6 @@ public class PredicateGenerator { PersistentEntity entity, Query.PropertyCriterion pc) { - // Firewall: validate property against the current context's entity String propertyName = pc.getProperty(); if (!"id".equals(propertyName) && !propertyName.contains(".") && entity.getPropertyByName(propertyName) == null) { throw new ConfigurationException("Property [" + propertyName + @@ -185,7 +180,7 @@ public class PredicateGenerator { if (pc instanceof Query.NotIn c) { return handleNotIn(cb, criteriaQuery, fromsByProvider, entity, c, fullyQualifiedPath); } else if (pc instanceof Query.SubqueryCriterion c) { - return handleSubqueryCriterion(cb, criteriaQuery, fromsByProvider, entity, c); + return handleSubqueryCriterion(cb, criteriaQuery, fromsByProvider, c); } else if (pc instanceof Query.In c) { return handleIn(cb, criteriaQuery, fromsByProvider, entity, c, fullyQualifiedPath); } else if (pc instanceof Query.ILike c) { @@ -244,12 +239,10 @@ public class PredicateGenerator { var projection = c.getSubquery().getProjections().get(0); if (projection instanceof Query.PropertyProjection pp) { boolean distinct = projection instanceof Query.DistinctPropertyProjection; - // FIX: Pass subEntity Predicate[] predicates2 = getPredicates(cb, criteriaQuery, from2, c.getValue().getCriteria(), newMap2, subEntity); subquery2.select(from2.get(pp.getPropertyName())).distinct(distinct).where(cb.and(predicates2)); return cb.not(cb.in(fullyQualifiedPath).value(subquery2)); } else if (projection instanceof Query.IdProjection) { - // FIX: Pass subEntity Predicate[] predicates2 = getPredicates(cb, criteriaQuery, from2, c.getValue().getCriteria(), newMap2, subEntity); subquery2.select(from2).where(cb.and(predicates2)); return cb.not(cb.in(fullyQualifiedPath).value(subquery2)); @@ -289,14 +282,12 @@ public class PredicateGenerator { HibernateCriteriaBuilder cb, CriteriaQuery<?> criteriaQuery, JpaFromProvider fromsByProvider, - PersistentEntity entity, Query.SubqueryCriterion c) { Subquery subquery = criteriaQuery.subquery(Number.class); PersistentEntity subEntity = c.getValue().getPersistentEntity(); Root from = subquery.from(subEntity.getJavaClass()); - JpaFromProvider newMap = new JpaFromProvider(fromsByProvider, (DetachedCriteria) c.getValue(), List.of(), criteriaQuery, from); + JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); newMap.put("root", from); - // FIX: Pass subEntity to subquery recursion Predicate[] predicates = getPredicates(cb, criteriaQuery, from, c.getValue().getCriteria(), newMap, subEntity); Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty()); @@ -370,13 +361,12 @@ public class PredicateGenerator { CriteriaQuery<?> criteriaQuery, From<?, ?> root_, JpaFromProvider fromsByProvider, - PersistentEntity entity, // This is now correctly the child entity + PersistentEntity entity, Query.Exists c) { Subquery subquery = criteriaQuery.subquery(Integer.class); Root subRoot = subquery.from(entity.getJavaClass()); - JpaFromProvider newMap = new JpaFromProvider(fromsByProvider, (DetachedCriteria) c.getSubquery(), List.of(), criteriaQuery, subRoot); + JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); newMap.put("root", subRoot); - // Pass 'entity' (which is child) to recursion var predicates = getPredicates(cb, criteriaQuery, subRoot, c.getSubquery().getCriteria(), newMap, entity); var existsPredicate = getExistsPredicate(cb, root_, entity, subRoot); Predicate[] allPredicates = existsPredicate != null @@ -396,18 +386,26 @@ public class PredicateGenerator { var projection = findPropertyOrIdProjection(queryableCriteria); var subProperty = findSubproperty(projection); var path = fromsByProvider.getFullyQualifiedPath(criterion.getProperty()); - var in = findInPredicate(cb, projection, path, subProperty); + boolean isAssociation = isAssociation(entity, criterion.getProperty()); + var in = findInPredicate(cb, projection, path, subProperty, isAssociation); var subquery = criteriaQuery.subquery(getJavaTypeOfInClause((SqmInListPredicate) in)); PersistentEntity subEntity = queryableCriteria.getPersistentEntity(); var from = subquery.from(subEntity.getJavaClass()); - var clonedProviderByName = new JpaFromProvider(fromsByProvider, (DetachedCriteria) queryableCriteria, List.of(), criteriaQuery, from); + var clonedProviderByName = (JpaFromProvider) fromsByProvider.clone(); clonedProviderByName.put("root", from); - // FIX: Pass subEntity var predicates = getPredicates(cb, criteriaQuery, from, queryableCriteria.getCriteria(), clonedProviderByName, subEntity); subquery.select(clonedProviderByName.getFullyQualifiedPath(subProperty)).distinct(true).where(cb.and(predicates)); return in.value(subquery); } + private boolean isAssociation(PersistentEntity entity, String propertyName) { + if (propertyName.equals("id") || (entity.getIdentity() != null && propertyName.equals(entity.getIdentity().getName()))) { + return false; + } + PersistentProperty prop = entity.getPropertyByName(propertyName); + return prop instanceof Association; + } + private Predicate getExistsPredicate( HibernateCriteriaBuilder cb, From<?, ?> root_, PersistentEntity childPersistentEntity, Root subRoot) { return childPersistentEntity.getAssociations().stream() @@ -418,8 +416,12 @@ public class PredicateGenerator { } private JpaInPredicate findInPredicate( - HibernateCriteriaBuilder cb, Object projection, Path path, String subProperty) { - return projection instanceof Query.PropertyProjection ? cb.in(path) : cb.in(((SqmPath) path).get(subProperty)); + HibernateCriteriaBuilder cb, Object projection, Path path, String subProperty, boolean isAssociation) { + if (projection instanceof Query.PropertyProjection || !isAssociation) { + return cb.in(path); + } else { + return cb.in(((SqmPath) path).get(subProperty)); + } } private String findSubproperty(Object projection) { @@ -466,4 +468,4 @@ public class PredicateGenerator { criterion.getProperty(), "null")); } -} \ No newline at end of file +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy index 4a813a1373..5fbd821263 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy @@ -1,15 +1,16 @@ package grails.gorm.specs.hibernatequery +import org.hibernate.query.criteria.HibernateCriteriaBuilder + import grails.gorm.DetachedCriteria import grails.gorm.specs.HibernateGormDatastoreSpec import jakarta.persistence.criteria.CriteriaQuery import jakarta.persistence.criteria.Root import org.grails.datastore.mapping.model.PersistentEntity import org.grails.datastore.mapping.query.Query -import grails.orm.HibernateCriteriaBuilder + import org.grails.orm.hibernate.query.JpaFromProvider import org.grails.orm.hibernate.query.PredicateGenerator -import spock.lang.Shared import grails.gorm.annotation.Entity import org.grails.datastore.gorm.GormEntity @@ -31,7 +32,7 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { query = cb.createQuery(PredicateGeneratorSpecPerson) root = query.from(PredicateGeneratorSpecPerson) personEntity = session.datastore.mappingContext.getPersistentEntity(PredicateGeneratorSpecPerson.name) - fromProvider = new JpaFromProvider(new DetachedCriteria(PredicateGeneratorSpecPerson), query, root) + fromProvider = new JpaFromProvider(new DetachedCriteria(PredicateGeneratorSpecPerson),[], root) } def "test getPredicates with Equals criterion"() { @@ -103,6 +104,66 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { noExceptionThrown() predicates.length == 1 } + + def "test getPredicates with Disjunction"() { + given: + List criteria = [new Query.Disjunction() + .add(new Query.Equals("firstName", "Bob")) + .add(new Query.Equals("firstName", "Alice"))] + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: + predicates.length == 1 + } + + def "test getPredicates with Negation"() { + given: + List criteria = [new Query.Negation().add(new Query.Equals("firstName", "Bob"))] + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: + predicates.length == 1 + } + + def "test getPredicates with Property Comparison"() { + given: + List criteria = [new Query.EqualsProperty("firstName", "lastName")] + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: + predicates.length == 1 + } + + def "test getPredicates with Like and ILike"() { + given: + List criteria = [ + new Query.Like("firstName", "B%"), + new Query.ILike("firstName", "b%") + ] + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: + predicates.length == 2 + } + + def "test getPredicates with Size Comparison"() { + given: + List criteria = [new Query.SizeEquals("pets", 2)] + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: + predicates.length == 1 + } } @Entity @@ -112,6 +173,7 @@ class PredicateGeneratorSpecPerson implements GormEntity<PredicateGeneratorSpecP String lastName Integer age PredicateGeneratorSpecFace face + static hasMany = [pets: PredicateGeneratorSpecPet] } @Entity
