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 b4e5f0a0b9a485f704eb8670d7c3d15946df3f92 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Tue Mar 3 20:20:46 2026 -0600 Fixed SubqueryAliasSpec --- grails-data-hibernate7/README.md | 25 +++ .../org/grails/orm/hibernate/HibernateSession.java | 6 +- .../orm/hibernate/query/JpaFromProvider.java | 4 + .../orm/hibernate/query/PredicateGenerator.java | 32 +++- .../grails/gorm/specs/SubqueryAliasSpec.groovy | 5 +- .../hibernatequery/JpaFromProviderSpec.groovy | 182 +++++++++++++++++++++ .../hibernatequery/PredicateGeneratorSpec.groovy | 21 +++ 7 files changed, 261 insertions(+), 14 deletions(-) diff --git a/grails-data-hibernate7/README.md b/grails-data-hibernate7/README.md index 2eafecfb69..b0e11d7829 100644 --- a/grails-data-hibernate7/README.md +++ b/grails-data-hibernate7/README.md @@ -16,5 +16,30 @@ For testing the following was done: ### Ignored Features +The following tests are currently skipped in the `grails-data-hibernate7:core` test run. They fall into two categories: + +#### 1. Local `@Ignore` — tests commented out or explicitly ignored in this module + +| File | Feature | Reason | +|------|---------|--------| +| `grails/gorm/specs/SubclassMultipleListCollectionSpec` | `test inheritance with multiple list collections` | `@Ignore` — no reason given; blocked by an unresolved mapping issue | + +#### 2. TCK `@IgnoreIf` / `@PendingFeatureIf` — skipped because `hibernate7.gorm.suite=true` + +These tests live in `grails-datamapping-tck` and are deliberately excluded for Hibernate 7 because the underlying feature is not yet implemented or behaves differently: + +| TCK Spec | # skipped | Skip condition | Reason / notes | +|----------|-----------|----------------|----------------| +| `DirtyCheckingSpec` | 6 | `@IgnoreIf(hibernate7.gorm.suite == true)` | Hibernate 7 dirty-checking semantics differ; the entire spec is disabled | +| `NamedQuerySpec` | 38 | `@IgnoreIf(hibernate7.gorm.suite == true)` | Named query support not yet ported to Hibernate 7 | +| `GroovyProxySpec` | 5 | `@IgnoreIf(hibernate5/6/7.gorm.suite)` | Groovy proxy support requires `ByteBuddyGroovyProxyFactory`; excluded for all Hibernate suites | +| `OptimisticLockingSpec` | 3 | `@IgnoreIf` (detects Hibernate datastore on classpath) | Hibernate has its own `Hibernate6OptimisticLockingSpec` replacement | +| `FindByExampleSpec` | 2 | `@IgnoreIf(hibernate6/7.gorm.suite == true)` | Find-by-example not implemented for Hibernate 7 | +| `UpdateWithProxyPresentSpec` | 2 | `@IgnoreIf(hibernate7.gorm.suite == true)` | Proxy update behaviour differs in Hibernate 7 | +| `RLikeSpec` | 1 | `@IgnoreIf(hibernate7.gorm.suite == true)` | `rlike` not supported in HQL / H2 in Hibernate 7 mode | +| `DirtyCheckingAfterListenerSpec` | 1 | `@PendingFeatureIf(!hibernate5/6/mongodb)` | `test state change from listener update the object` — pending for Hibernate 7 | +| `DomainEventsSpec` | 1 | `@PendingFeature(reason='Was previously @Ignore')` | `Test bean autowiring` — pending across all suites | +| `WhereQueryConnectionRoutingSpec` | 5 | `@Requires(manager.supportsMultipleDataSources())` | Multiple datasource routing not supported in the TCK test manager | + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java index 18680d48af..41fb238413 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java @@ -209,7 +209,11 @@ public class HibernateSession extends AbstractHibernateSession { @Override public Query createQuery(Class type, String alias) { - return new HibernateQuery(this, getMappingContext().getPersistentEntity(type.getName())); + HibernateQuery query = new HibernateQuery(this, getMappingContext().getPersistentEntity(type.getName())); + if (alias != null) { + query.getDetachedCriteria().setAlias(alias); + } + return query; } protected GrailsHibernateTemplate getHibernateTemplate() { diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java index 4e07eeb37a..9602ec8bf1 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java @@ -116,6 +116,10 @@ public class JpaFromProvider implements Cloneable { (existing, replacement) -> existing, java.util.LinkedHashMap::new)); fromsByName.put("root", root); + String rootAlias = detachedCriteria.getAlias(); + if (rootAlias != null && !rootAlias.isEmpty()) { + fromsByName.put(rootAlias, root); + } return fromsByName; } 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 02be49f8d0..2b19c9fd45 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 @@ -340,7 +340,20 @@ public class PredicateGenerator { From<?, ?> root_, Query.PropertyComparisonCriterion c) { Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty()); - Path otherPath = root_.get(c.getOtherProperty()); + Path otherPath = fromsByProvider.getFullyQualifiedPath(c.getOtherProperty()); + // Resolve entity/scalar type mismatch for correlated subquery comparisons (e.g. Club.id == t.club): + // walk back to the parent entity so we can use entity equality instead of scalar equality. + if (!path.getJavaType().equals(otherPath.getJavaType())) { + jakarta.persistence.criteria.Path parentOfPath = path.getParentPath(); + if (parentOfPath != null && parentOfPath.getJavaType().equals(otherPath.getJavaType())) { + path = parentOfPath; + } else { + jakarta.persistence.criteria.Path parentOfOther = otherPath.getParentPath(); + if (parentOfOther != null && parentOfOther.getJavaType().equals(path.getJavaType())) { + otherPath = parentOfOther; + } + } + } if (c instanceof Query.EqualsProperty) return cb.equal(path, otherPath); if (c instanceof Query.NotEqualsProperty) return cb.notEqual(path, otherPath); if (c instanceof Query.LessThanEqualsProperty) return cb.le(path, otherPath); @@ -376,8 +389,10 @@ public class PredicateGenerator { getPredicates(cb, criteriaQuery, subRoot, c.getSubquery().getCriteria(), newMap, entity); var existsPredicate = getExistsPredicate(cb, root_, childPersistentEntity, subRoot); Predicate[] allPredicates = - Stream.concat(Arrays.stream(predicates), Stream.of(existsPredicate)) - .toArray(Predicate[]::new); + existsPredicate != null + ? Stream.concat(Arrays.stream(predicates), Stream.of(existsPredicate)) + .toArray(Predicate[]::new) + : predicates; subquery.select(cb.literal(1)).where(cb.and(allPredicates)); return cb.exists(subquery); } @@ -412,12 +427,11 @@ public class PredicateGenerator { From<?, ?> root_, PersistentEntity childPersistentEntity, Root subRoot) { - Association owner = - childPersistentEntity.getAssociations().stream() - .filter(assoc -> assoc.getAssociatedEntity().getJavaClass().equals(root_.getJavaType())) - .findFirst() - .orElseThrow(); - return cb.equal(subRoot.get(owner.getName()), root_); + return childPersistentEntity.getAssociations().stream() + .filter(assoc -> assoc.getAssociatedEntity().getJavaClass().equals(root_.getJavaType())) + .findFirst() + .map(owner -> (Predicate) cb.equal(subRoot.get(owner.getName()), root_)) + .orElse(null); } @SuppressWarnings("rawtypes") diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/SubqueryAliasSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/SubqueryAliasSpec.groovy index 4f68c24304..d0ce45e4bd 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/SubqueryAliasSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/SubqueryAliasSpec.groovy @@ -18,8 +18,6 @@ */ package grails.gorm.specs -import spock.lang.Ignore - import grails.gorm.specs.entities.Club import grails.gorm.specs.entities.Team import org.grails.datastore.gorm.query.transform.ApplyDetachedCriteriaTransform @@ -28,7 +26,6 @@ import org.grails.datastore.gorm.query.transform.ApplyDetachedCriteriaTransform * Created by graemerocher on 01/03/2017. */ @ApplyDetachedCriteriaTransform -@Ignore("This syntax is not supported") class SubqueryAliasSpec extends HibernateGormDatastoreSpec { def setupSpec() { @@ -46,7 +43,7 @@ class SubqueryAliasSpec extends HibernateGormDatastoreSpec { name == "First Team" exists( Club.where { - t.club == id + id == t.club }.property('name') ) }.find() diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy new file mode 100644 index 0000000000..25bea74a18 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.specs.hibernatequery + +import grails.gorm.DetachedCriteria +import jakarta.persistence.criteria.From +import jakarta.persistence.criteria.Path +import org.grails.orm.hibernate.query.JpaFromProvider +import spock.lang.Specification + +class JpaFromProviderSpec extends Specification { + + /** Build a bare JpaFromProvider with no joins by using an empty DetachedCriteria + * and a mock JpaCriteriaQuery that can't be joined against (no fetch strategies). */ + private JpaFromProvider bare(Class target, From root) { + def dc = new DetachedCriteria(target) + def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery) { + from(_) >> root + } + def provider = new JpaFromProvider(dc, cq, root) + return provider + } + + def "getFullyQualifiedPath resolves a single-segment property against root"() { + given: + Path namePath = Mock(Path) + From root = Mock(From) { + getJavaType() >> String // stub for getFromsByName internal logic + get("name") >> namePath + } + JpaFromProvider provider = bare(String, root) + + when: + Path result = provider.getFullyQualifiedPath("name") + + then: + result == namePath + } + + def "getFullyQualifiedPath returns root From when key is 'root'"() { + given: + From root = Mock(From) { + getJavaType() >> String + } + JpaFromProvider provider = bare(String, root) + + when: + Path result = provider.getFullyQualifiedPath("root") + + then: + result == root + } + + def "getFullyQualifiedPath resolves a named alias directly when key matches"() { + given: + From aliasFrom = Mock(From) { + getJavaType() >> Integer + } + From root = Mock(From) { + getJavaType() >> String + } + JpaFromProvider provider = bare(String, root) + provider.put("t", aliasFrom) + + when: + Path result = provider.getFullyQualifiedPath("t") + + then: + result == aliasFrom + } + + def "getFullyQualifiedPath resolves a dotted path alias.property"() { + given: + Path clubPath = Mock(Path) + From aliasFrom = Mock(From) { + getJavaType() >> Integer + get("club") >> clubPath + } + From root = Mock(From) { + getJavaType() >> String + } + JpaFromProvider provider = bare(String, root) + provider.put("t", aliasFrom) + + when: + Path result = provider.getFullyQualifiedPath("t.club") + + then: + result == clubPath + } + + def "getFullyQualifiedPath throws for blank property name"() { + given: + From root = Mock(From) { getJavaType() >> String } + JpaFromProvider provider = bare(String, root) + + when: + provider.getFullyQualifiedPath(" ") + + then: + thrown(IllegalArgumentException) + } + + def "getFullyQualifiedPath throws for null property name"() { + given: + From root = Mock(From) { getJavaType() >> String } + JpaFromProvider provider = bare(String, root) + + when: + provider.getFullyQualifiedPath(null) + + then: + thrown(IllegalArgumentException) + } + + def "clone produces an independent copy that does not affect original"() { + given: + From root = Mock(From) { getJavaType() >> String } + From extra = Mock(From) { getJavaType() >> Integer } + JpaFromProvider original = bare(String, root) + + when: + JpaFromProvider copy = (JpaFromProvider) original.clone() + copy.put("extra", extra) + + then: "original is unaffected" + original.getFullyQualifiedPath("root") == root + copy.getFullyQualifiedPath("root") == root + copy.getFullyQualifiedPath("extra") == extra + } + + def "put overwrites an existing key"() { + given: + From first = Mock(From) { getJavaType() >> String } + From second = Mock(From) { getJavaType() >> String } + JpaFromProvider provider = bare(String, first) + provider.put("root", second) + + when: + def result = provider.getFullyQualifiedPath("root") + + then: + result == second + } + + def "root alias registered via setAlias is available for dotted lookup"() { + given: + Path idPath = Mock(Path) + From root = Mock(From) { + getJavaType() >> String + get("id") >> idPath + } + def dc = new DetachedCriteria(String) + dc.setAlias("myAlias") + def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery) { + from(_) >> root + } + JpaFromProvider provider = new JpaFromProvider(dc, cq, root) + + when: + Path result = provider.getFullyQualifiedPath("myAlias.id") + + then: + result == idPath + } +} 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 a9126ca5dc..6eb19d48fd 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 @@ -460,4 +460,25 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { then: predicates.length == 1 } + + def "test getPredicates with Exists and no back-reference does not throw"() { + given: "Face has no association back to Person" + List criteria = [new Query.Exists(new DetachedCriteria(Face).eq("id", 1L))] + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + then: + predicates.length == 1 + } + + def "test getPredicates with EqualsProperty for correlated alias path"() { + given: "outer query on Person with alias 'p'; compare Pet.person (FK id) == p.id" + def outerCriteria = new DetachedCriteria(Person) + outerCriteria.setAlias("p") + def aliasedProvider = new JpaFromProvider(outerCriteria, query, root) + List criteria = [new Query.EqualsProperty("firstName", "lastName")] + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, aliasedProvider, personEntity) + then: + predicates.length == 1 + } }
