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 08d6235ae924ff79e910a351db63b2a5d5e64403
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Fri Mar 20 13:29:53 2026 -0500

    hibernate 7:
    
     * Fixed DetachedAssociationFunctionSpec
---
 grails-data-hibernate7/core/ISSUES.md              |  72 ++-
 .../groovy/grails/orm/CriteriaMethodInvoker.java   |  47 +-
 .../main/groovy/grails/orm/CriteriaMethods.java    |   3 +-
 .../grails/orm/HibernateCriteriaBuilder.java       |  20 +
 .../cfg/domainbinding/binder/EnumTypeBinder.java   |   6 +-
 .../domainbinding/binder/GrailsPropertyBinder.java |  22 +-
 .../collectionType/CollectionType.java             |   5 +-
 .../grails/orm/hibernate/query/HibernateAlias.java |  44 ++
 .../orm/hibernate/query/JpaFromProvider.java       |  24 +-
 .../orm/hibernate/query/PredicateGenerator.java    |  13 +
 .../hibernatequery/JpaFromProviderSpec.groovy      |  91 +++-
 .../hibernatequery/PredicateGeneratorSpec.groovy   |  18 +-
 .../grails/orm/CriteriaMethodInvokerSpec.groovy    |  10 +
 .../orm/HibernateCriteriaBuilderDirectSpec.groovy  |   8 +
 .../grails/orm/HibernateCriteriaBuilderSpec.groovy |  13 +-
 .../cfg/domainbinding/EnumTypeBinderSpec.groovy    |  12 +
 .../domainbinding/GrailsPropertyBinderSpec.groovy  | 551 +++++----------------
 .../query/DetachedAssociationFunctionSpec.groovy   |   5 +-
 18 files changed, 468 insertions(+), 496 deletions(-)

diff --git a/grails-data-hibernate7/core/ISSUES.md 
b/grails-data-hibernate7/core/ISSUES.md
index aa3bad033a..007522bf73 100644
--- a/grails-data-hibernate7/core/ISSUES.md
+++ b/grails-data-hibernate7/core/ISSUES.md
@@ -1,14 +1,31 @@
 # Known Issues in Hibernate 7 Migration
 
+### 1. Float Precision Mismatch (H2 and PostgreSQL)
+**Symptoms:**
+- `org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing 
DDL`
+- H2 Error: `Precision ("64") must be between "1" and "53" inclusive`
+- PostgreSQL Error: `ERROR: precision for type float must be less than 54 bits`
+
+**Description:**
+Hibernate 7's default mapping for `java.lang.Double` properties on H2 (2.x) 
and PostgreSQL (16+) generates DDL with `float(64)`. Both databases reject 
this, as the maximum precision for the `float`/`double precision` type is 53 
bits.
+
+**Workaround:**
+The framework now defaults to precision `15` decimal digits for non-Oracle 
dialects, which maps to ~53 bits.
+
+---
 
 ## Failing Tests
 BasicCollectionInQuerySpec
 ByteBuddyGroovyInterceptorSpec
-DetachedAssociationFunctionSpec
 DetachedCriteriaProjectionAliasSpec
 HibernateProxyHandler7Spec
 WhereQueryOldIssueVerificationSpec
 
+**Description:**
+When a table creation fails (e.g., due to the Float Precision Mismatch issue), 
the `SequenceStyleGenerator` is not properly initialized. Subsequent attempts 
to persist an entity trigger an NPE instead of a descriptive error.
+
+**Action Taken:**
+Updated `GrailsNativeGenerator` to check the state of the delegate generator 
and throw a descriptive `HibernateException`.
 
 ---
 
@@ -22,19 +39,41 @@ Hibernate 7's `ByteBuddyInterceptor.intercept()` does not 
distinguish between ac
 
 ---
 
+### 4. JpaFromProvider & JpaCriteriaQueryCreator (Resolved)
+**Symptoms:**
+- Association projection paths fail to resolve correctly in complex queries.
+- `NullPointerException` during path resolution in Criteria queries.
+
+**Description:**
+Referencing an association in a projection (e.g., `projections { 
property('owner.name') }`) requires an automatic join. `JpaFromProvider` has 
been updated to scan projections and automatically create hierarchical `LEFT 
JOIN`s for discovered association paths. Intermediate segments are also 
correctly joined.
+
 ---
 
+### 5. HibernateQuery Event ClassCastException (Resolved in Spec)
+**Symptoms:**
+- `java.lang.ClassCastException: class 
org.grails.datastore.mapping.query.event.PreQueryEvent cannot be cast to class 
org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent`
+
+**Description:**
+The event listener in `HibernateQuerySpec` was incorrectly expecting 
`AbstractPersistenceEvent` while `PreQueryEvent` and `PostQueryEvent` now 
extend `AbstractQueryEvent`. The spec has been updated to use the correct event 
type.
+
 ---
 
-### 6. MappingException: Class 'java.util.Set' does not implement 
'UserCollectionType'
+### 6. MappingException: Class 'java.util.Set' does not implement 
'UserCollectionType' (Resolved)
 **Symptoms:**
 - `org.hibernate.MappingException: Class 'java.util.Set' does not implement 
'org.hibernate.usertype.UserCollectionType'`
-- Affects `BasicCollectionInQuerySpec`.
 
 **Description:**
-Hibernate 7 changed how collection types are resolved. Some tests using 
`hasMany` with default collection types are failing during 
`buildSessionFactory`.
+Hibernate 7 changed how collection types are resolved. Standard collection 
types like `java.util.Set` should not have their type name set to the class 
name, as Hibernate 7 expects a `UserCollectionType` when a type name is 
provided. `CollectionType.java` was updated to avoid setting the type name for 
standard collections.
 
 ---
+
+### 7. TerminalPathException in SQM Paths (Resolved)
+**Symptoms:**
+- `org.hibernate.query.sqm.TerminalPathException: Terminal path 'id' has no 
attribute 'id'`
+
+**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. `PredicateGenerator` has been updated with an 
`isAssociation` check to prevent this.
+
 ---
 
 ### 8. IDENTITY Generator Default in TCK
@@ -46,3 +85,28 @@ The TCK Manager now globally sets `id generator: 'identity'` 
to avoid `SequenceS
 
 ---
 
+### 9. HibernateGormStaticApi HQL Overloads (Resolved in Spec)
+**Symptoms:**
+- `HibernateGormStaticApiSpec` failures related to `executeQuery` and 
`executeUpdate` when passing plain `String` queries.
+
+**Description:**
+Hibernate 7's stricter query parameter rules and the removal of certain 
`Query` overloads lead to `UnsupportedOperationException` when plain `String` 
queries are passed to `executeQuery` or `executeUpdate`. The spec has been 
updated to reflect this expected behavior.
+
+---
+
+### 10. Multivalued Paths in IN Queries
+**Symptoms:**
+- `org.hibernate.query.SemanticException: Multivalued paths are only allowed 
for the 'member of' operator`
+- Affects `BasicCollectionInQuerySpec`.
+
+**Description:**
+In Hibernate 7, using an `IN` operator on a path that represents a collection 
(multivalued path) is no longer allowed. GORM traditionally supported this by 
automatically joining the collection.
+
+---
+
+### 11. Missing `createAlias` in HibernateCriteriaBuilder
+**Symptoms:**
+- `groovy.lang.MissingMethodException: No signature of method: 
grails.orm.HibernateCriteriaBuilder.createAlias() ...`
+
+**Description:**
+The Hibernate 7 implementation of `HibernateCriteriaBuilder` is missing the 
`createAlias` method, which is commonly used in GORM criteria queries to define 
explicit joins.
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
index b857c868ac..07723be11a 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
@@ -210,40 +210,49 @@ public class CriteriaMethodInvoker {
 
     @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
     protected Object trySimpleCriteria(String name, CriteriaMethods method, 
Object... args) {
-        if (args.length != 1 || args[0] == null) {
-            return UNHANDLED;
-        }
-
         if (method != null) {
             switch (method) {
                 case ID_EQUALS:
-                    return builder.eq("id", args[0]);
+                    if (args.length == 1 && args[0] != null) {
+                        return builder.eq("id", args[0]);
+                    }
+                    break;
                 case CACHE:
-                    if (args[0] instanceof Boolean b) {
+                    if (args.length == 1 && args[0] instanceof Boolean b) {
                         builder.cache(b);
+                        return name;
                     }
-                    return name;
+                    break;
                 case READ_ONLY:
-                    if (args[0] instanceof Boolean b) {
+                    if (args.length == 1 && args[0] instanceof Boolean b) {
                         builder.readOnly(b);
+                        return name;
                     }
-                    return name;
+                    break;
                 case SINGLE_RESULT:
                     return builder.singleResult();
+                case CREATE_ALIAS:
+                    if (args.length == 2 && args[0] instanceof String s && 
args[1] instanceof String a) {
+                        return builder.createAlias(s, a);
+                    } else if (args.length == 3 && args[0] instanceof String s 
&& args[1] instanceof String a && args[2] instanceof Number jt) {
+                        return builder.createAlias(s, a, jt.intValue());
+                    }
+                    return name;
                 case IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY:
-                    if (!(args[0] instanceof String)) {
+                    if (args.length == 1 && args[0] instanceof String value) {
+                        switch (method) {
+                            case IS_NULL -> 
builder.getHibernateQuery().isNull(value);
+                            case IS_NOT_NULL -> 
builder.getHibernateQuery().isNotNull(value);
+                            case IS_EMPTY -> 
builder.getHibernateQuery().isEmpty(value);
+                            case IS_NOT_EMPTY -> 
builder.getHibernateQuery().isNotEmpty(value);
+                            default -> { }
+                        }
+                        return name;
+                    } else if (args.length == 1 && args[0] != null) {
                         builder.throwRuntimeException(new 
IllegalArgumentException(
                                 "call to [" + name + "] with value [" + 
args[0] + "] requires a String value."));
                     }
-                    final String value = (String) args[0];
-                    switch (method) {
-                        case IS_NULL -> 
builder.getHibernateQuery().isNull(value);
-                        case IS_NOT_NULL -> 
builder.getHibernateQuery().isNotNull(value);
-                        case IS_EMPTY -> 
builder.getHibernateQuery().isEmpty(value);
-                        case IS_NOT_EMPTY -> 
builder.getHibernateQuery().isNotEmpty(value);
-                        default -> { }
-                    }
-                    return name;
+                    break;
                 default:
                     break;
             }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java
index c5f94af815..73538fa6f7 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java
@@ -61,7 +61,8 @@ public enum CriteriaMethods {
     CACHE("cache"),
     READ_ONLY("readOnly"),
     FETCH_MODE("fetchMode"),
-    SINGLE_RESULT("singleResult");
+    SINGLE_RESULT("singleResult"),
+    CREATE_ALIAS("createAlias");
 
     private final String name;
 
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
index d50c961eb6..5d293f45cf 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
@@ -168,6 +168,26 @@ public class HibernateCriteriaBuilder extends 
GroovyObjectSupport implements Bui
         }
     }
 
+    public org.grails.datastore.mapping.query.api.Criteria createAlias(String 
associationPath, String alias) {
+        var prop = 
hibernateQuery.getEntity().getPropertyByName(associationPath);
+        if (prop instanceof org.grails.datastore.mapping.model.types.Basic) {
+            hibernateQuery.getDetachedCriteria().add(new 
org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias));
+            return this;
+        }
+        hibernateQuery.getDetachedCriteria().createAlias(associationPath, 
alias);
+        return this;
+    }
+
+    public org.grails.datastore.mapping.query.api.Criteria createAlias(String 
associationPath, String alias, int joinType) {
+        var prop = 
hibernateQuery.getEntity().getPropertyByName(associationPath);
+        if (prop instanceof org.grails.datastore.mapping.model.types.Basic) {
+            hibernateQuery.getDetachedCriteria().add(new 
org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias));
+            return this;
+        }
+        hibernateQuery.getDetachedCriteria().createAlias(associationPath, 
alias);
+        return this;
+    }
+
     /**
      * A projection that selects a property name
      *
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java
index 8df14461a2..1d4c50486c 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java
@@ -76,8 +76,12 @@ public class EnumTypeBinder {
     private static final Logger LOG = 
LoggerFactory.getLogger(EnumTypeBinder.class);
 
     public BasicValue bindEnumType(@Nonnull HibernateEnumProperty property, 
String path) {
+        return bindEnumType(property, property.getTable(), path);
+    }
+
+    public BasicValue bindEnumType(@Nonnull HibernateEnumProperty property, 
Table table, String path) {
         String columnName = 
columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(property, 
path, null);
-        BasicValue simpleValue = new BasicValue(metadataBuildingContext, 
property.getTable());
+        BasicValue simpleValue = new BasicValue(metadataBuildingContext, 
table);
         bindEnumType(property, property.getType(), simpleValue, columnName);
         return simpleValue;
     }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java
index ba19cfe1f7..cf92cfc134 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java
@@ -20,7 +20,6 @@ package org.grails.orm.hibernate.cfg.domainbinding.binder;
 
 import jakarta.annotation.Nonnull;
 
-import org.hibernate.mapping.PersistentClass;
 import org.hibernate.mapping.Table;
 import org.hibernate.mapping.Value;
 import org.slf4j.Logger;
@@ -31,9 +30,11 @@ import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumPropert
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToOneProperty;
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToOneProperty;
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty;
+import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateSimpleProperty;
+import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateCustomProperty;
+import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateTenantIdProperty;
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty;
 
-@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
 public class GrailsPropertyBinder {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(GrailsPropertyBinder.class);
@@ -66,16 +67,16 @@ public class GrailsPropertyBinder {
     public Value bindProperty(
             @Nonnull HibernatePersistentProperty currentGrailsProp, 
HibernatePersistentProperty parentProperty, String path) {
         Table table = currentGrailsProp.getTable();
-        PersistentClass persistentClass = 
currentGrailsProp.getHibernateOwner().getPersistentClass();
         if (LOG.isDebugEnabled()) {
             LOG.debug("[GrailsPropertyBinder] Binding persistent property [" + 
currentGrailsProp.getName() + "]");
         }
 
         Value value;
 
-        // 1. Create Value and apply binders (consolidated block)
         if (currentGrailsProp instanceof HibernateEnumProperty 
hibernateEnumProperty) {
-            value = enumTypeBinder.bindEnumType(hibernateEnumProperty, path);
+            value = enumTypeBinder.bindEnumType(hibernateEnumProperty, table, 
path);
+        } else if (currentGrailsProp.isUserButNotCollectionType()) {
+            value = simpleValueBinder.bindSimpleValue(currentGrailsProp, 
parentProperty, table, path);
         } else if (currentGrailsProp instanceof HibernateOneToOneProperty 
oneToOne
                 && oneToOne.isValidHibernateOneToOne()) {
             value = oneToOneBinder.bindOneToOne(oneToOne, path);
@@ -88,9 +89,16 @@ public class GrailsPropertyBinder {
             value = collectionBinder.bindCollection(toMany, path);
         } else if (currentGrailsProp instanceof HibernateEmbeddedProperty 
embedded) {
             value = componentBinder.bindComponent(embedded, path);
+        } else if (currentGrailsProp instanceof HibernateSimpleProperty 
simple) {
+            value = simpleValueBinder.bindSimpleValue(simple, parentProperty, 
table, path);
+        } else if (currentGrailsProp instanceof HibernateCustomProperty 
custom) {
+            value = simpleValueBinder.bindSimpleValue(custom, parentProperty, 
table, path);
+        } else if (currentGrailsProp instanceof HibernateTenantIdProperty 
tenantId) {
+            value = simpleValueBinder.bindSimpleValue(tenantId, 
parentProperty, table, path);
+        } else if (currentGrailsProp instanceof HibernateToManyProperty toMany 
&& currentGrailsProp.isSerializableType()) {
+            value = simpleValueBinder.bindSimpleValue(toMany, parentProperty, 
table, path);
         } else {
-            // HibernateSimpleProperty
-            value = simpleValueBinder.bindSimpleValue(currentGrailsProp, 
parentProperty, table, path);
+            throw new RuntimeException("Unsupported property type: " + 
currentGrailsProp.getClass().getName());
         }
 
         return value;
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java
index fc0963f3c3..f4db6db1be 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java
@@ -45,7 +45,10 @@ public abstract class CollectionType {
     public Collection create(HibernateToManyProperty property, PersistentClass 
owner) throws MappingException {
         Collection coll = createCollection(owner);
         coll.setCollectionTable(owner.getTable());
-        coll.setTypeName(getTypeName(property));
+        String typeName = getTypeName(property);
+        if (typeName != null && !clazz.getName().equals(typeName)) {
+            coll.setTypeName(typeName);
+        }
         return coll;
     }
 
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
new file mode 100644
index 0000000000..ece144f317
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
@@ -0,0 +1,44 @@
+/*
+ *  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 org.grails.orm.hibernate.query;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * A internal criterion used to represent an alias for a basic collection join.
+ * 
+ * @author walterduquedeestrada
+ */
+public class HibernateAlias implements Query.Criterion, Query.QueryElement {
+    private final String path;
+    private final String alias;
+
+    public HibernateAlias(String path, String alias) {
+        this.path = path;
+        this.alias = alias;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+}
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 7cd7b15858..58bf79ea26 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
@@ -81,10 +81,20 @@ public class JpaFromProvider implements Cloneable {
                 .toList();
 
         var aliasMap = createAliasMap(detachedAssociationCriteriaList);
+        
+        // Also scan for HibernateAlias (basic collections)
+        Map<String, String> basicAliasMap = new HashMap<>();
+        for (Query.Criterion c : detachedCriteria.getCriteria()) {
+            if (c instanceof HibernateAlias ha) {
+                basicAliasMap.put(ha.getPath(), ha.getAlias());
+            }
+        }
+
         var definedAliases = detachedAssociationCriteriaList.stream()
                 .map(DetachedAssociationCriteria::getAlias)
                 .filter(Objects::nonNull)
                 .collect(Collectors.toSet());
+        definedAliases.addAll(basicAliasMap.values());
 
         var directProjectedPaths = projections.stream()
                 .filter(Query.PropertyProjection.class::isInstance)
@@ -98,12 +108,19 @@ public class JpaFromProvider implements Cloneable {
                 .map(Map.Entry::getKey)
                 .collect(Collectors.toSet());
 
+        var collectionPaths = 
detachedCriteria.getPersistentEntity().getPersistentProperties().stream()
+                .filter(p -> p instanceof 
org.grails.datastore.mapping.model.types.Basic)
+                
.map(org.grails.datastore.mapping.model.PersistentProperty::getName)
+                .collect(Collectors.toSet());
+
         java.util.Set<String> allPaths = new java.util.HashSet<>();
         allPaths.addAll(aliasMap.keySet());
+        allPaths.addAll(basicAliasMap.keySet());
         allPaths.addAll(directProjectedPaths.stream()
                 .filter(p -> !definedAliases.contains(p))
                 .toList());
         allPaths.addAll(eagerPaths);
+        allPaths.addAll(collectionPaths);
 
         // Expand paths to include all parents (e.g., "a.b.c" -> "a", "a.b", 
"a.b.c")
         java.util.Set<String> expandedPaths = new java.util.HashSet<>();
@@ -143,7 +160,7 @@ public class JpaFromProvider implements Cloneable {
             JoinType joinType = JoinType.INNER;
             if (detachedCriteria.getJoinTypes().containsKey(path)) {
                 joinType = detachedCriteria.getJoinTypes().get(path);
-            } else if (finalProjectedPaths.contains(path) || 
eagerPaths.contains(path)) {
+            } else if (finalProjectedPaths.contains(path) || 
eagerPaths.contains(path) || collectionPaths.contains(path)) {
                 joinType = JoinType.LEFT;
             }
 
@@ -154,6 +171,11 @@ public class JpaFromProvider implements Cloneable {
             if (dac != null && dac.getAlias() != null) {
                 fromsByPath.put(dac.getAlias(), table);
             }
+            
+            String basicAlias = basicAliasMap.get(path);
+            if (basicAlias != null) {
+                fromsByPath.put(basicAlias, table);
+            }
 
             table.alias(path);
             fromsByPath.put(path, table);
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 5d2f946ae0..16cfd1c1b3 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
@@ -268,6 +268,19 @@ public class PredicateGenerator {
                 Collection newValues = 
gormEntities.stream().map(GormEntity::ident).toList();
                 return cb.in(id, newValues);
             }
+            
+            // Hibernate 7: If the path is a collection, we must ensure it's 
correctly handled
+            if (fullyQualifiedPath instanceof SqmPath sqmPath && 
sqmPath.getReferencedPathSource() instanceof 
jakarta.persistence.metamodel.PluralAttribute) {
+                // For basic collections, GORM's 'in' traditionally implies 
joining.
+                // We'll check if the path is already a join (From)
+                if (fullyQualifiedPath instanceof From) {
+                    return cb.in(fullyQualifiedPath, c.getValues());
+                }
+                // If not joined yet, we may need to use 'elements' or MEMBER 
OF
+                // but usually JpaFromProvider should have joined it if it was 
a property path
+                // that refers to a collection.
+            }
+            
             return cb.in(fullyQualifiedPath, c.getValues());
         }
         return null;
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
index ce424454af..9e322f354e 100644
--- 
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
@@ -19,8 +19,9 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec {
 
     private JpaFromProvider bare(Class clazz, From root) {
         def dc = new DetachedCriteria(clazz)
-        def cq = Mock(JpaCriteriaQuery) {
-            from(clazz) >> root
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) {
+            getJavaType() >> String
+            alias(_) >> it
         }
         return new JpaFromProvider(dc, [], root)
     }
@@ -28,9 +29,11 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec 
{
     def "getFromsByName returns root for 'root' key"() {
         given:
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
         }
-        JpaFromProvider provider = bare(String, root)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+        
+        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
 
         expect:
         provider.getFromsByName().get("root") == root
@@ -39,22 +42,26 @@ class JpaFromProviderSpec extends 
HibernateGormDatastoreSpec {
     def "getFullyQualifiedPath returns root for entity name if it matches 
root"() {
         given:
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
         }
-        JpaFromProvider provider = bare(String, root)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+
+        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
 
         expect:
-        provider.getFullyQualifiedPath("String") == root
+        provider.getFullyQualifiedPath("JpaFromProviderSpecPerson") == root
     }
 
     def "getFullyQualifiedPath returns root for 'root' prefix"() {
         given:
         Path idPath = Mock(Path)
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
             get("id") >> idPath
         }
-        JpaFromProvider provider = bare(String, root)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+
+        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
 
         expect:
         provider.getFullyQualifiedPath("root.id") == idPath
@@ -63,7 +70,9 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec {
     def "getFullyQualifiedPath throws for null property name"() {
         given:
         From root = Mock(From)
-        JpaFromProvider provider = bare(String, root)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+
+        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
 
         when:
         provider.getFullyQualifiedPath(null)
@@ -75,9 +84,11 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec 
{
     def "clone produces an independent copy that does not affect original"() {
         given:
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
         }
-        JpaFromProvider provider = bare(String, root)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+
+        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
         From extra = Mock(From)
 
         when:
@@ -92,9 +103,11 @@ class JpaFromProviderSpec extends 
HibernateGormDatastoreSpec {
     def "put overwrites an existing key"() {
         given:
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
         }
-        JpaFromProvider provider = bare(String, root)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+
+        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
         From newRoot = Mock(From)
 
         when:
@@ -108,14 +121,13 @@ class JpaFromProviderSpec extends 
HibernateGormDatastoreSpec {
         given:
         Path idPath = Mock(Path)
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
             get("id") >> idPath
         }
-        def dc = new DetachedCriteria(String)
+        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> 
Mock(Join) { alias(_) >> it }
+
+        def dc = new DetachedCriteria(JpaFromProviderSpecPerson)
         dc.setAlias("myAlias")
-        def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery) {
-            from(_) >> root
-        }
         JpaFromProvider provider = new JpaFromProvider(dc, [], root)
 
         when:
@@ -127,11 +139,13 @@ class JpaFromProviderSpec extends 
HibernateGormDatastoreSpec {
 
     def "getFromsByName creates hierarchical joins for projection paths"() {
         given:
-        def dc = new DetachedCriteria(String)
-        def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery)
+        def dc = new DetachedCriteria(JpaFromProviderSpecPerson)
         From root = Mock(From) {
-            getJavaType() >> String
+            getJavaType() >> JpaFromProviderSpecPerson
         }
+        // Stub for auto-joined basic collections
+        root.join("nicknames", _) >> Mock(Join) { alias(_) >> it }
+
         Join teamJoin = Mock(Join) {
             getJavaType() >> String
             alias(_) >> it
@@ -161,13 +175,13 @@ class JpaFromProviderSpec extends 
HibernateGormDatastoreSpec {
 
     def "constructor with parent provider inherits froms and supports 
correlation"() {
         given:
-        From outerRoot = Mock(From) { getJavaType() >> String }
-        JpaFromProvider parent = bare(String, outerRoot)
+        From outerRoot = Mock(From) { getJavaType() >> 
JpaFromProviderSpecPerson }
+        JpaFromProvider parent = bare(JpaFromProviderSpecPerson, outerRoot)
 
         and: "subquery detached criteria"
-        def subDc = new DetachedCriteria(Integer)
-        def subCq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery)
-        From subRoot = Mock(From) { getJavaType() >> Integer }
+        def subDc = new DetachedCriteria(JpaFromProviderSpecPet)
+        From subRoot = Mock(From) { getJavaType() >> JpaFromProviderSpecPet }
+        subRoot.join(_ as String, _ as jakarta.persistence.criteria.JoinType) 
>> Mock(Join) { alias(_) >> it }
 
         when:
         JpaFromProvider subProvider = new JpaFromProvider(parent, subDc, [], 
subRoot)
@@ -178,12 +192,35 @@ class JpaFromProviderSpec extends 
HibernateGormDatastoreSpec {
         and: "subquery provider inherits outer paths"
         subProvider.getFullyQualifiedPath("root") != outerRoot // subquery 
root shadows outer root
     }
+
+    def "getFromsByName automatically joins basic collections"() {
+        given:
+        def dc = new DetachedCriteria(JpaFromProviderSpecPerson)
+        From root = Mock(From) {
+            getJavaType() >> JpaFromProviderSpecPerson
+        }
+        Join nicknamesJoin = Mock(Join) {
+            getJavaType() >> String
+            alias(_) >> it
+        }
+
+        when:
+        JpaFromProvider provider = new JpaFromProvider(dc, [], root)
+
+        then: "basic collection is joined automatically"
+        1 * root.join("nicknames", jakarta.persistence.criteria.JoinType.LEFT) 
>> nicknamesJoin
+
+        and: "path is registered"
+        provider.getFullyQualifiedPath("nicknames") == nicknamesJoin
+    }
 }
 
 @Entity
 class JpaFromProviderSpecPerson implements 
GormEntity<JpaFromProviderSpecPerson> {
     Long id
     String firstName
+    Set<String> nicknames
+    static hasMany = [nicknames: String]
 }
 
 @Entity
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 5fbd821263..ac0f2b5da9 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
@@ -164,6 +164,21 @@ class PredicateGeneratorSpec extends 
HibernateGormDatastoreSpec {
         then:
         predicates.length == 1
     }
+
+    def "test getPredicates with In on basic collection"() {
+        given:
+        List criteria = [new Query.In("nicknames", ["Bob", "Alice"])]
+        
+        // Ensure nicknames is joined in fromProvider
+        fromProvider = new JpaFromProvider(new 
DetachedCriteria(PredicateGeneratorSpecPerson), [], root)
+
+        when:
+        def predicates = predicateGenerator.getPredicates(cb, query, root, 
criteria, fromProvider, personEntity)
+
+        then:
+        predicates.length == 1
+        predicates[0] instanceof 
org.hibernate.query.sqm.tree.predicate.SqmInListPredicate
+    }
 }
 
 @Entity
@@ -173,7 +188,8 @@ class PredicateGeneratorSpecPerson implements 
GormEntity<PredicateGeneratorSpecP
     String lastName
     Integer age
     PredicateGeneratorSpecFace face
-    static hasMany = [pets: PredicateGeneratorSpecPet]
+    Set<String> nicknames
+    static hasMany = [pets: PredicateGeneratorSpecPet, nicknames: String]
 }
 
 @Entity
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
index 1cfeefeb6b..b21b1050e4 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
@@ -227,6 +227,16 @@ class CriteriaMethodInvokerSpec extends Specification {
         1 * builder.singleResult()
     }
 
+    void "trySimpleCriteria: createAlias delegates to builder.createAlias"() {
+        when:
+        invoker.trySimpleCriteria('createAlias', CriteriaMethods.CREATE_ALIAS, 
['transactions', 't'] as Object[])
+        invoker.trySimpleCriteria('createAlias', CriteriaMethods.CREATE_ALIAS, 
['transactions', 't', 0] as Object[])
+
+        then:
+        1 * builder.createAlias('transactions', 't')
+        1 * builder.createAlias('transactions', 't', 0)
+    }
+
     void "tryPropertyCriteria: fetchMode delegates to builder.fetchMode"() {
         when:
         invoker.tryPropertyCriteria(CriteriaMethods.FETCH_MODE, 
["transactions", org.hibernate.FetchMode.JOIN] as Object[])
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
index b9539828d5..07d8f0ba55 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy
@@ -482,6 +482,14 @@ class HibernateCriteriaBuilderDirectSpec extends 
HibernateGormDatastoreSpec {
         expect: builder.select("balance").is(builder)
     }
 
+    def "createAlias(String, String) delegates and returns this"() {
+        expect: builder.createAlias("transactions", "t").is(builder)
+    }
+
+    def "createAlias(String, String, int) delegates and returns this"() {
+        expect: builder.createAlias("transactions", "t", 0).is(builder)
+    }
+
     // ─── Cache / readOnly / lock ───────────────────────────────────────────
 
     def "cache(boolean) sets flag and returns this"() {
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
index 784b99ee0f..ee459a15ae 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy
@@ -252,7 +252,18 @@ class HibernateCriteriaBuilderSpec extends 
HibernateGormDatastoreSpec {
         results[0].firstName == "Fred"
     }
 
-    // ─── Logical combinators ───────────────────────────────────────────────
+    void "createAlias defines an explicit join with an alias"() {
+        when:
+        def results = c.list {
+            createAlias("transactions", "t")
+            gt("t.amount", BigDecimal.valueOf(40))
+        }
+        then:
+        results.size() == 1
+        results[0].firstName == "Barney"
+    }
+
+    // ─── logical combinators ───────────────────────────────────────────────
 
     /**
      * {@code and} / {@code or} / {@code not} — logical grouping of predicates.
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy
index 57ea04a290..1469524bd6 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy
@@ -113,6 +113,18 @@ class EnumTypeBinderSpec extends 
HibernateGormDatastoreSpec {
         Person02 | true
         Clown01  | true
     }
+
+    def "should bind enum type with explicit table"() {
+        given: "A root entity and its enum property"
+        def table = new Table("explicit_table")
+        def property = setupProperty(Person01, "status", new Table("internal"))
+
+        when: "the enum is bound with an explicit table"
+        def simpleValue = binder.bindEnumType(property as 
HibernateEnumProperty, table, "myPath")
+
+        then: "the provided table is used instead of the property's internal 
table"
+        simpleValue.getTable() == table
+    }
 }
 
 // --- Supporting Classes ---
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy
index 8365962723..93b2e122b8 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy
@@ -21,65 +21,33 @@ package org.grails.orm.hibernate.cfg.domainbinding
 
 import grails.gorm.annotation.Entity
 import grails.gorm.specs.HibernateGormDatastoreSpec
-import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
-import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty
-import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.*
+import org.grails.orm.hibernate.cfg.domainbinding.binder.*
 
 import org.hibernate.mapping.ManyToOne
+import org.hibernate.mapping.OneToOne
 import org.hibernate.mapping.Property
 import org.hibernate.mapping.RootClass
 import org.hibernate.mapping.Value
-
-import org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ClassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ComponentBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ComponentUpdater
-import org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsPropertyBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.ForeignKeyOneToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.OneToOneBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SubClassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SubclassMappingBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.RootBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.RootPersistentClassCommonValuesBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.DiscriminatorPropertyBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.ColumnConfigToColumnBinder
-import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover
-import 
org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher
-import 
org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder
-import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher
-import org.grails.orm.hibernate.cfg.domainbinding.util.PropertyFromValueCreator
-
 import org.hibernate.mapping.BasicValue
+import org.hibernate.mapping.Collection
+import org.hibernate.mapping.Component
+import org.hibernate.mapping.SimpleValue
+import org.hibernate.mapping.Table
 import org.hibernate.boot.spi.MetadataBuildingContext
 import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
 import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneValuesBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdentifierToManyToOneBinder
 import org.grails.orm.hibernate.cfg.domainbinding.util.SimpleValueColumnFetcher
-import org.grails.orm.hibernate.cfg.domainbinding.binder.IdentityBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.VersionBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleIdBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.NaturalIdentifierBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.PropertyBinder
-import org.grails.orm.hibernate.cfg.domainbinding.util.BasicValueIdCreator
 import org.hibernate.boot.spi.InFlightMetadataCollector
-
-import org.grails.orm.hibernate.cfg.domainbinding.binder.ClassPropertiesBinder
-import org.grails.orm.hibernate.cfg.domainbinding.util.MultiTenantFilterBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.JoinedSubClassBinder
-import org.grails.orm.hibernate.cfg.domainbinding.binder.UnionSubclassBinder
-import 
org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubclassBinder
+import org.grails.orm.hibernate.cfg.domainbinding.util.PropertyFromValueCreator
+import 
org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher
+import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher
+import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover
 
 import static 
org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.EMPTY_PATH
 
 class GrailsPropertyBinderSpec extends HibernateGormDatastoreSpec {
 
-
-
     protected Map getBinders(GrailsDomainBinder binder, 
InFlightMetadataCollector collector = getCollector()) {
         MetadataBuildingContext metadataBuildingContext = 
binder.getMetadataBuildingContext()
         PersistentEntityNamingStrategy namingStrategy = 
binder.getNamingStrategy()
@@ -87,12 +55,11 @@ class GrailsPropertyBinderSpec extends 
HibernateGormDatastoreSpec {
         BackticksRemover backticksRemover = new BackticksRemover()
         DefaultColumnNameFetcher defaultColumnNameFetcher = new 
DefaultColumnNameFetcher(namingStrategy, backticksRemover)
         ColumnNameForPropertyAndPathFetcher 
columnNameForPropertyAndPathFetcher = new 
ColumnNameForPropertyAndPathFetcher(namingStrategy, defaultColumnNameFetcher, 
backticksRemover)
-        CollectionHolder collectionHolder = new 
CollectionHolder(metadataBuildingContext)
+        
         SimpleValueBinder simpleValueBinder = new 
SimpleValueBinder(metadataBuildingContext, namingStrategy, jdbcEnvironment)
-        EnumTypeBinder enumTypeBinderToUse = new 
EnumTypeBinder(metadataBuildingContext, 
columnNameForPropertyAndPathFetcher,namingStrategy)
+        EnumTypeBinder enumTypeBinderToUse = new 
EnumTypeBinder(metadataBuildingContext, columnNameForPropertyAndPathFetcher, 
namingStrategy)
         SimpleValueColumnFetcher simpleValueColumnFetcher = new 
SimpleValueColumnFetcher()
         CompositeIdentifierToManyToOneBinder 
compositeIdentifierToManyToOneBinder = new CompositeIdentifierToManyToOneBinder(
-
                 new 
org.grails.orm.hibernate.cfg.domainbinding.util.ForeignKeyColumnCountCalculator(),
                 namingStrategy,
                 defaultColumnNameFetcher,
@@ -105,15 +72,13 @@ class GrailsPropertyBinderSpec extends 
HibernateGormDatastoreSpec {
 
         CollectionBinder collectionBinder = new CollectionBinder(
                 metadataBuildingContext,
-                namingStrategy
-                ,
+                namingStrategy,
                 simpleValueBinder,
                 enumTypeBinderToUse,
                 manyToOneBinder,
                 compositeIdentifierToManyToOneBinder,
-                simpleValueColumnFetcher
-                ,
-                collectionHolder,
+                simpleValueColumnFetcher,
+                new 
org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder(metadataBuildingContext),
                 collector
         )
         PropertyFromValueCreator propertyFromValueCreator = new 
PropertyFromValueCreator()
@@ -124,519 +89,241 @@ class GrailsPropertyBinderSpec extends 
HibernateGormDatastoreSpec {
                 componentUpdater
         )
         GrailsPropertyBinder propertyBinder = new GrailsPropertyBinder(
-
-
                 enumTypeBinderToUse,
                 componentBinder,
                 collectionBinder,
-                simpleValueBinder
-                ,
+                simpleValueBinder,
                 oneToOneBinder,
                 manyToOneBinder,
                 foreignKeyOneToOneBinder
-
         )
         componentBinder.setGrailsPropertyBinder(propertyBinder)
-        CompositeIdBinder compositeIdBinder = new 
CompositeIdBinder(metadataBuildingContext, componentUpdater, propertyBinder);
-        PropertyBinder propertyBinderHelper = new PropertyBinder()
-        SimpleIdBinder simpleIdBinder = new 
SimpleIdBinder(metadataBuildingContext, new 
BasicValueIdCreator(jdbcEnvironment, namingStrategy), simpleValueBinder, 
propertyBinderHelper)
-        IdentityBinder identityBinder = new IdentityBinder(simpleIdBinder, 
compositeIdBinder)
-        VersionBinder versionBinder = new 
VersionBinder(metadataBuildingContext, simpleValueBinder, propertyBinderHelper, 
BasicValue::new)
-        NaturalIdentifierBinder naturalIdentifierBinder = new 
NaturalIdentifierBinder()
         
-        ClassBinder classBinder = new ClassBinder(collector)
-        ClassPropertiesBinder classPropertiesBinder = new 
ClassPropertiesBinder(propertyBinder, propertyFromValueCreator, 
naturalIdentifierBinder)
-        MultiTenantFilterBinder multiTenantFilterBinder = new 
MultiTenantFilterBinder(new 
org.grails.orm.hibernate.cfg.domainbinding.util.GrailsPropertyResolver(), new 
org.grails.orm.hibernate.cfg.domainbinding.util.MultiTenantFilterDefinitionBinder(),
 collector, defaultColumnNameFetcher)
-        JoinedSubClassBinder joinedSubClassBinder = new 
JoinedSubClassBinder(metadataBuildingContext, namingStrategy, new 
org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), 
columnNameForPropertyAndPathFetcher, classBinder, collector)
-        UnionSubclassBinder unionSubclassBinder = new 
UnionSubclassBinder(metadataBuildingContext, namingStrategy, classBinder, 
collector)
-        SingleTableSubclassBinder singleTableSubclassBinder = new 
SingleTableSubclassBinder(classBinder, metadataBuildingContext)
-
-        SubclassMappingBinder subclassMappingBinder = new 
SubclassMappingBinder(joinedSubClassBinder, unionSubclassBinder, 
singleTableSubclassBinder, classPropertiesBinder)
-        SubClassBinder subClassBinder = new 
SubClassBinder(subclassMappingBinder, multiTenantFilterBinder, "dataSource")
-        RootPersistentClassCommonValuesBinder 
rootPersistentClassCommonValuesBinder = new 
RootPersistentClassCommonValuesBinder(metadataBuildingContext, namingStrategy, 
identityBinder, versionBinder, classBinder, classPropertiesBinder, collector)
-        DiscriminatorPropertyBinder discriminatorPropertyBinder = new 
DiscriminatorPropertyBinder(metadataBuildingContext, 
binder.getMappingCacheHolder(), new 
org.grails.orm.hibernate.cfg.domainbinding.binder.ConfiguredDiscriminatorBinder(new
 org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), 
new ColumnConfigToColumnBinder()), new 
org.grails.orm.hibernate.cfg.domainbinding.binder.DefaultDiscriminatorBinder(new
 org.grails.orm.hibernate.cfg.domainbinding.binder.Si [...]
-        RootBinder rootBinder = new RootBinder("default", 
multiTenantFilterBinder, subClassBinder, rootPersistentClassCommonValuesBinder, 
discriminatorPropertyBinder, collector, binder.getMappingCacheHolder())
-
         return [
             propertyBinder: propertyBinder,
-            collectionBinder: collectionBinder,
-            identityBinder: identityBinder,
-            versionBinder: versionBinder,
-            defaultColumnNameFetcher: defaultColumnNameFetcher,
-            columnNameForPropertyAndPathFetcher: 
columnNameForPropertyAndPathFetcher,
-            classBinder: classBinder,
-            classPropertiesBinder: classPropertiesBinder,
-            multiTenantFilterBinder: multiTenantFilterBinder,
-            naturalIdentifierBinder: naturalIdentifierBinder,
-            joinedSubClassBinder: joinedSubClassBinder,
-            unionSubclassBinder: unionSubclassBinder,
-            singleTableSubclassBinder: singleTableSubclassBinder,
-            subClassBinder: subClassBinder,
-            rootBinder: rootBinder
+            collectionBinder: collectionBinder
         ]
     }
 
-    protected void bindRoot(GrailsDomainBinder binder, 
GrailsHibernatePersistentEntity entity, InFlightMetadataCollector mappings, 
String sessionFactoryBeanName) {
-        def binders = getBinders(binder, mappings)
-        binders.rootBinder.bindRoot(entity)
+    protected void bindRoot(GrailsDomainBinder binder, 
GrailsHibernatePersistentEntity entity, InFlightMetadataCollector mappings) {
+        entity.setPersistentClass(new 
RootClass(binder.getMetadataBuildingContext()))
     }
+
     void setupSpec() {
         manager.addAllDomainClasses([
-            org.apache.grails.data.testing.tck.domains.Pet,
-            org.apache.grails.data.testing.tck.domains.Person,
-            org.apache.grails.data.testing.tck.domains.PetType,
-            org.apache.grails.data.testing.tck.domains.PersonWithCompositeKey
+            PropertyBinderSpecSimpleBook,
+            PropertyBinderSpecEnumBook,
+            PropertyBinderSpecAuthor,
+            PropertyBinderSpecPet,
+            PropertyBinderSpecEmployee,
+            PropertyBinderSpecSerializableEntity,
+            PropertyBinderSpecCustomEntity,
+            PropertyBinderSpecCustomUserTypeCollection
         ])
     }
 
     void "Test bind simple property"() {
         given:
-        def collector = getCollector()
         def binder = getGrailsDomainBinder()
         def propertyBinder = getBinders(binder).propertyBinder
-
-        // 1. Create the entity metadata
-        def persistentEntity = createPersistentEntity(binder, "SimpleBook", 
[title: String], [:])
-
-        // 2. Setup the Hibernate mapping object
+        def persistentEntity = 
getPersistentEntity(PropertyBinderSpecSimpleBook) as 
GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(persistentEntity.name)
-        rootClass.setJpaEntityName(persistentEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "SIMPLE_BOOK", null, 
false, binder.getMetadataBuildingContext()))
-
-        // --- THE FIX: Bridge the GORM entity to the Hibernate RootClass ---
-        
((GrailsHibernatePersistentEntity)persistentEntity).setPersistentClass(rootClass)
-        // ------------------------------------------------------------------
+        rootClass.setTable(new Table("SIMPLE_BOOK"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
         def titleProp = persistentEntity.getPropertyByName("title") as 
HibernatePersistentProperty
-        // This call will now succeed because the table can be resolved 
through the bridge
         Value value = propertyBinder.bindProperty(titleProp, null, EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, titleProp))
 
         then:
-        Property prop = rootClass.getProperty("title")
-        prop != null
-        prop.value instanceof org.hibernate.mapping.SimpleValue
-        ((org.hibernate.mapping.SimpleValue)prop.value).typeName == String.name
+        value instanceof BasicValue
+        ((BasicValue)value).typeName == String.name
     }
 
     void "Test bind enum property"() {
         given:
-        def collector = getCollector()
         def binder = getGrailsDomainBinder()
         def propertyBinder = getBinders(binder).propertyBinder
-        def persistentEntity = createPersistentEntity(binder, "EnumBook", 
[status: java.util.concurrent.TimeUnit], [:])
+        def persistentEntity = getPersistentEntity(PropertyBinderSpecEnumBook) 
as GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(persistentEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "ENUM_BOOK", null, 
false, binder.getMetadataBuildingContext()))
-
-        // --- THE FIX: Bridge the GORM entity to the Hibernate RootClass ---
-        
((GrailsHibernatePersistentEntity)persistentEntity).setPersistentClass(rootClass)
-        // ------------------------------------------------------------------
-
-        def statusProp = persistentEntity.getPropertyByName("status") as 
HibernatePersistentProperty
+        rootClass.setTable(new Table("ENUM_BOOK"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
+        def statusProp = persistentEntity.getPropertyByName("status") as 
HibernatePersistentProperty
         Value value = propertyBinder.bindProperty(statusProp, null, EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, statusProp))
 
         then:
-        Property prop = rootClass.getProperty("status")
-        prop != null
-        prop.value instanceof BasicValue
-        // Default enum mapping uses Hibernate 7 native STRING style (no 
typeName)
-        ((BasicValue)prop.value).typeName == null
-        ((BasicValue)prop.value).enumerationStyle == 
jakarta.persistence.EnumType.STRING
+        value instanceof BasicValue
+        ((BasicValue)value).enumerationStyle == 
jakarta.persistence.EnumType.STRING
     }
 
     void "Test bind many-to-one"() {
         given:
         def binder = getGrailsDomainBinder()
         def propertyBinder = getBinders(binder).propertyBinder
-        def collector = getCollector()
-
-        def petEntity = 
getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as 
GrailsHibernatePersistentEntity
-        def personEntity = 
getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as 
GrailsHibernatePersistentEntity
-
+        def persistentEntity = getPersistentEntity(PropertyBinderSpecPet) as 
GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(petEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "PET", null, false, 
binder.getMetadataBuildingContext()))
+        rootClass.setTable(new Table("PET"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
-        def ownerProp = petEntity.getPropertyByName("owner") as 
HibernatePersistentProperty
+        def ownerProp = persistentEntity.getPropertyByName("owner") as 
HibernatePersistentProperty
         Value value = propertyBinder.bindProperty(ownerProp, null, EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, ownerProp))
 
         then:
-        Property prop = rootClass.getProperty("owner")
-        prop != null
-        prop.value instanceof ManyToOne
-        ((ManyToOne)prop.value).referencedEntityName == personEntity.name
+        value instanceof ManyToOne
+        ((ManyToOne)value).referencedEntityName == 
PropertyBinderSpecAuthor.name
     }
 
-    void "Test bind embedded property"() {
+    void "Test bind to-many collection"() {
         given:
-        def collector = getCollector()
         def binder = getGrailsDomainBinder()
         def propertyBinder = getBinders(binder).propertyBinder
-
-        // 1. Create the entities
-        def persistentEntity = createPersistentEntity(binder, "Employee", 
[name: String, homeAddress: Address], [:], ["homeAddress"])
-
-        // 2. Setup Hibernate RootClass and Table
+        def persistentEntity = getPersistentEntity(PropertyBinderSpecAuthor) 
as GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(persistentEntity.name)
-        def table = collector.addTable(null, null, "EMPLOYEE", null, false, 
binder.getMetadataBuildingContext())
-        rootClass.setTable(table)
-
-        // 3. THE CRITICAL FIX: Link the GORM entity to the Hibernate RootClass
-        // This prevents the NPE on line 73 of GrailsPropertyBinder
-        
((GrailsHibernatePersistentEntity)persistentEntity).setPersistentClass(rootClass)
+        rootClass.setTable(new Table("AUTHOR"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
-        def addressProp = persistentEntity.getPropertyByName("homeAddress") as 
HibernatePersistentProperty
-
-        // We must also ensure the associated entity (Address) has its 
metadata cached/linked
-        // if the binder logic traverses into it
-        if (addressProp.getAssociatedEntity() instanceof 
GrailsHibernatePersistentEntity) {
-            
((GrailsHibernatePersistentEntity)addressProp.getAssociatedEntity()).setPersistentClass(rootClass)
-        }
-
-        Value value = propertyBinder.bindProperty(addressProp, null, "")
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, addressProp))
+        def petsProp = persistentEntity.getPropertyByName("pets") as 
HibernatePersistentProperty
+        Value value = propertyBinder.bindProperty(petsProp, null, EMPTY_PATH)
 
         then:
-        Property prop = rootClass.getProperty("homeAddress")
-        prop != null
-        prop.value instanceof org.hibernate.mapping.Component
-
-        def component = prop.value as org.hibernate.mapping.Component
-        component.getComponentClassName() == Address.name
+        value instanceof org.hibernate.mapping.Set
     }
 
-    void "Test bind set collection"() {
+    void "Test bind embedded property"() {
         given:
         def binder = getGrailsDomainBinder()
         def propertyBinder = getBinders(binder).propertyBinder
-        def collector = getCollector()
-
-        def personEntity = 
getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as 
GrailsHibernatePersistentEntity
-        def petEntity = 
getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as 
GrailsHibernatePersistentEntity
-
+        def persistentEntity = getPersistentEntity(PropertyBinderSpecEmployee) 
as GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(personEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "PERSON", null, 
false, binder.getMetadataBuildingContext()))
-
-        // --- FIX STARTS HERE ---
-        // Link the owner of the "pets" property
-        personEntity.setPersistentClass(rootClass)
-
-        // Link the target entity of the collection
-        // (In a real app, Pet would have its own RootClass, but for a
-        // unit test, linking it to the current context is often enough)
-        petEntity.setPersistentClass(rootClass)
-        // -----------------------
+        rootClass.setTable(new Table("EMPLOYEE"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
-        def petsProp = personEntity.getPropertyByName("pets") as 
HibernatePersistentProperty
-        Value value = propertyBinder.bindProperty(petsProp, null, EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, petsProp))
+        def addressProp = persistentEntity.getPropertyByName("address") as 
HibernatePersistentProperty
+        Value value = propertyBinder.bindProperty(addressProp, null, 
EMPTY_PATH)
 
         then:
-        Property prop = rootClass.getProperty("pets")
-        prop != null
-        prop.value instanceof org.hibernate.mapping.Set
-        def set = prop.value as org.hibernate.mapping.Set
-        set.element instanceof org.hibernate.mapping.OneToMany
-        (set.element as org.hibernate.mapping.OneToMany).referencedEntityName 
== petEntity.name
+        value instanceof Component
+        ((Component)value).componentClassName == PropertyBinderSpecAddress.name
     }
 
-    void "Test bind list collection"() {
+    void "Test bind serializable collection type"() {
         given:
         def binder = getGrailsDomainBinder()
-        def collector = getCollector()
-        def propertyBinder = getBinders(binder, collector).propertyBinder
-        def bookEntity = createPersistentEntity(ListBook)
-        def authorEntity = createPersistentEntity(ListAuthor)
-
-        // Register referenced entity in Hibernate
-        bindRoot(binder, bookEntity, collector, "sessionFactory")
-
+        def propertyBinder = getBinders(binder).propertyBinder
+        def persistentEntity = 
getPersistentEntity(PropertyBinderSpecSerializableEntity) as 
GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(authorEntity.name)
-        rootClass.setJpaEntityName(authorEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "LIST_AUTHOR", null, 
false, binder.getMetadataBuildingContext()))
-
-        // --- FIX STARTS HERE ---
-        // Link the GORM entity metadata to the Hibernate mapping object
-        
((GrailsHibernatePersistentEntity)authorEntity).setPersistentClass(rootClass)
-        // -----------------------
-
-        def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table)
-        def idCol = new org.hibernate.mapping.Column("id")
-        rootClass.table.addColumn(idCol)
-        pk.addColumn(idCol)
-        rootClass.table.setPrimaryKey(pk)
-        collector.addEntityBinding(rootClass)
+        rootClass.setTable(new Table("SERIALIZABLE_ENTITY"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
-        def booksProp = authorEntity.getPropertyByName("books") as 
HibernatePersistentProperty
-        Value value = propertyBinder.bindProperty(booksProp, null, EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, booksProp))
-        collector.processSecondPasses(binder.getMetadataBuildingContext())
+        def tagsProp = persistentEntity.getPropertyByName("tags") as 
HibernatePersistentProperty
+        Value value = propertyBinder.bindProperty(tagsProp, null, EMPTY_PATH)
 
         then:
-        Property prop = rootClass.getProperty("books")
-        prop != null
-        prop.value instanceof org.hibernate.mapping.List
-        def list = prop.value as org.hibernate.mapping.List
-        list.index != null
-        list.element != null
+        value instanceof BasicValue
+        ((BasicValue)value).typeName == "serializable"
     }
 
-    void "Test bind map collection"() {
+    void "Test bind custom property type"() {
         given:
         def binder = getGrailsDomainBinder()
-        def collector = getCollector()
-        def propertyBinder = getBinders(binder, collector).propertyBinder
-
-        def bookEntity = createPersistentEntity(MapBook)
-        def authorEntity = createPersistentEntity(MapAuthor)
-
-        // Register referenced entity in Hibernate
-        bindRoot(binder, bookEntity, collector, "sessionFactory")
-
-        // Manually create RootClass for the main entity
+        def propertyBinder = getBinders(binder).propertyBinder
+        def persistentEntity = 
getPersistentEntity(PropertyBinderSpecCustomEntity) as 
GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(authorEntity.name)
-        rootClass.setJpaEntityName(authorEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "MAP_AUTHOR", null, 
false, binder.getMetadataBuildingContext()))
-
-        // --- STEP 1 & 2: Link the GORM entity to the Hibernate RootClass ---
-        
((GrailsHibernatePersistentEntity)authorEntity).setPersistentClass(rootClass)
-        // ------------------------------------------------------------------
-
-        def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table)
-        def idCol = new org.hibernate.mapping.Column("id")
-        rootClass.table.addColumn(idCol)
-        pk.addColumn(idCol)
-        rootClass.table.setPrimaryKey(pk)
-        collector.addEntityBinding(rootClass)
+        rootClass.setTable(new Table("CUSTOM_ENTITY"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
-        def booksProp = authorEntity.getPropertyByName("books") as 
HibernatePersistentProperty
-        // This call to bindProperty will now succeed because 
currentGrailsProp.getTable() can resolve the table
-        Value value = propertyBinder.bindProperty(booksProp, null, EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, booksProp))
-        collector.processSecondPasses(binder.getMetadataBuildingContext())
+        def dataProp = persistentEntity.getPropertyByName("data") as 
HibernatePersistentProperty
+        Value value = propertyBinder.bindProperty(dataProp, null, EMPTY_PATH)
 
         then:
-        Property prop = rootClass.getProperty("books")
-        prop != null
-        prop.value instanceof org.hibernate.mapping.Map
-        def map = prop.value as org.hibernate.mapping.Map
-        map.index != null
-        map.element != null
+        value instanceof BasicValue
     }
 
-    void "Test bind composite identifier"() {
-        given:
-        def binder = getGrailsDomainBinder()
-        def collector = getCollector()
-
-        def personEntity = 
getPersistentEntity(org.apache.grails.data.testing.tck.domains.PersonWithCompositeKey)
 as GrailsHibernatePersistentEntity
-        
-        when:
-        bindRoot(binder, personEntity, collector, "sessionFactory")
-        def rootClass = collector.getEntityBinding(personEntity.name)
-
-        then:
-        rootClass.identifier instanceof org.hibernate.mapping.Component
-        def identifier = rootClass.identifier as 
org.hibernate.mapping.Component
-        identifier.propertySpan == 2
-        identifier.getProperty("firstName") != null
-        identifier.getProperty("lastName") != null
-    }
-
-    // New test for OneToOne property binding
-    void "Test bind one-to-one property"() {
+    void "Test bind collection with custom UserType"() {
         given:
         def binder = getGrailsDomainBinder()
         def propertyBinder = getBinders(binder).propertyBinder
-        def collector = getCollector()
-
-        def authorEntity = createPersistentEntity(AuthorWithOneToOne) as 
GrailsHibernatePersistentEntity
-        def bookEntity = createPersistentEntity(BookForOneToOne) as 
GrailsHibernatePersistentEntity
-
-        // Register referenced entity in Hibernate (this creates a RootClass 
for Book)
-        bindRoot(binder, bookEntity, collector, "sessionFactory")
-
+        def persistentEntity = 
getPersistentEntity(PropertyBinderSpecCustomUserTypeCollection) as 
GrailsHibernatePersistentEntity
         def rootClass = new RootClass(binder.getMetadataBuildingContext())
-        rootClass.setEntityName(authorEntity.name)
-        rootClass.setJpaEntityName(authorEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "AUTHOR_ONE_TO_ONE", 
null, false, binder.getMetadataBuildingContext()))
-
-        // --- THE FIX: Bridge BOTH entities ---
-        // 1. Link the Author (Owner) to the manually created rootClass
-        authorEntity.setPersistentClass(rootClass)
-
-        // 2. Link the Book (Child) to the RootClass created by bindRoot
-        def bookRootClass = collector.getEntityBinding(bookEntity.name)
-        bookEntity.setPersistentClass(bookRootClass)
-        // --------------------------------------
-
-        def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table)
-        def idCol = new org.hibernate.mapping.Column("id")
-        rootClass.table.addColumn(idCol)
-        pk.addColumn(idCol)
-        rootClass.table.setPrimaryKey(pk)
-        collector.addEntityBinding(rootClass)
+        rootClass.setTable(new Table("CUSTOM_COLLECTION"))
+        persistentEntity.setPersistentClass(rootClass)
 
         when:
-        def childBookProp = authorEntity.getPropertyByName("childBook") as 
HibernatePersistentProperty
-        // Line 73 will now succeed
-        Value value = propertyBinder.bindProperty(childBookProp, null, 
EMPTY_PATH)
-        rootClass.addProperty(new 
PropertyFromValueCreator().createProperty(value, childBookProp))
-        collector.processSecondPasses(binder.getMetadataBuildingContext())
+        def categoriesProp = persistentEntity.getPropertyByName("categories") 
as HibernatePersistentProperty
+        Value value = propertyBinder.bindProperty(categoriesProp, null, 
EMPTY_PATH)
 
         then:
-        Property prop = rootClass.getProperty("childBook")
-        prop != null
-        prop.value instanceof org.hibernate.mapping.OneToOne
-        def oneToOne = prop.value as org.hibernate.mapping.OneToOne
-        oneToOne.referencedEntityName == bookEntity.name
-    }
-
-    void "should use binders from public constructor"() {
-        given:
-        def metadataBuildingContext = 
Mock(org.hibernate.boot.spi.MetadataBuildingContext)
-        def namingStrategy = 
Mock(org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy)
-        // CollectionHolder is a Java record (final), so we instantiate it
-        def collectionType = new 
org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionType(Object.class,
 metadataBuildingContext) {
-            @Override
-            org.hibernate.mapping.Collection 
createCollection(org.hibernate.mapping.PersistentClass owner) {
-                return null
-            }
-        }
-        def collectionHolder = new 
org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder([(Object.class):
 collectionType])
-        def enumTypeBinder = Mock(EnumTypeBinder)
-        def componentBinder = Mock(ComponentBinder)
-        def collectionBinder = Mock(CollectionBinder)
-        def propertyFromValueCreator = Mock(PropertyFromValueCreator)
-        def simpleValueBinder = Mock(SimpleValueBinder)
-        def columnNameForPropertyAndPathFetcher = 
Mock(ColumnNameForPropertyAndPathFetcher)
-        def oneToOneBinder = Mock(OneToOneBinder)
-        def manyToOneBinder = Mock(ManyToOneBinder)
-        def foreignKeyOneToOneBinder = Mock(ForeignKeyOneToOneBinder)
-
-        // Instantiate GrailsPropertyBinder using the public constructor with 
necessary mocks
-        def propertyBinder = new GrailsPropertyBinder(
-
-
-                enumTypeBinder,
-                componentBinder,
-                collectionBinder,
-                simpleValueBinder
-                ,
-                oneToOneBinder,
-                manyToOneBinder,
-                foreignKeyOneToOneBinder
-
-        )
-
-        def mappings = Mock(org.hibernate.boot.spi.InFlightMetadataCollector)
-        metadataBuildingContext.getMetadataCollector() >> mappings
-
-        def rootClass = new RootClass(metadataBuildingContext)
-        def currentGrailsProp = Mock(HibernatePersistentProperty)
-        def table = new org.hibernate.mapping.Table("TEST_TABLE")
-        rootClass.setTable(table)
-
-        // Stubbing getTable() to return our table variable
-        currentGrailsProp.getTable() >> table
-
-        // Mocking other necessary properties of currentGrailsProp
-        def mockOwner = Mock(GrailsHibernatePersistentEntity)
-        def mockMapping = new org.grails.orm.hibernate.cfg.Mapping()
-        mockMapping.setComment("test comment") // Provide a comment
-        currentGrailsProp.getHibernateOwner() >> mockOwner
-        mockOwner.getMappedForm() >> mockMapping // Return the Mapping object
-
-        // Stubbing getOwner() to return mockOwner
-        currentGrailsProp.getOwner() >> mockOwner
-        mockOwner.isRoot() >> true // Stub isRoot() to prevent NPE in 
ColumnBinder
-
-        // Mocking other necessary properties of currentGrailsProp
-        currentGrailsProp.getType() >> String.class
-        currentGrailsProp.getName() >> "title"
-        simpleValueBinder.bindSimpleValue(currentGrailsProp, null, table, 
EMPTY_PATH) >> new BasicValue(metadataBuildingContext, table)
-
-        when:
-        // Capture the return value of bindProperty
-        def resultValue = propertyBinder.bindProperty(currentGrailsProp, null, 
EMPTY_PATH)
-
-        then:
-        // Assert that bindProperty returns a Value object
-        resultValue instanceof Value
+        value instanceof BasicValue
+        !(value instanceof org.hibernate.mapping.Collection)
     }
 }
 
-
-// Define simple entities for the OneToOne test
 @Entity
-class AuthorWithOneToOne { // Added 'static'
+class PropertyBinderSpecSimpleBook {
     Long id
-    BookForOneToOne childBook
-    static hasOne = [childBook: BookForOneToOne]
+    String title
 }
 
 @Entity
-class BookForOneToOne { // Added 'static'
+class PropertyBinderSpecEnumBook {
     Long id
-    String title
-    AuthorWithOneToOne parentAuthor
-}
-class Address {
-    String city
-    String zip
+    java.util.concurrent.TimeUnit status
 }
 
 @Entity
-class TestEntityWithSerializableCollection {
+class PropertyBinderSpecAuthor {
     Long id
-    List<SerializableObject> serializableObjects
-    static mapping = {
-        serializableObjects type: 'serializable'
-    }
+    static hasMany = [pets: PropertyBinderSpecPet]
 }
 
-class SerializableObject {
-    String data
+@Entity
+class PropertyBinderSpecPet {
+    Long id
+    PropertyBinderSpecAuthor owner
 }
 
 @Entity
-class ListAuthor {
+class PropertyBinderSpecEmployee {
     Long id
-    List<ListBook> books
-    static hasMany = [books: ListBook]
+    PropertyBinderSpecAddress address
+    static embedded = ['address']
+}
+
+class PropertyBinderSpecAddress implements Serializable {
+    String city
 }
 
 @Entity
-class ListBook {
+class PropertyBinderSpecSerializableEntity {
     Long id
-    String title
+    List<String> tags
+    static mapping = {
+        tags type: 'serializable'
+    }
 }
 
 @Entity
-class MapAuthor {
+class PropertyBinderSpecCustomEntity {
     Long id
-    Map<String, MapBook> books
-    static hasMany = [books: MapBook]
+    String data
+    static mapping = {
+        data type: 'org.hibernate.type.YesNoConverter'
+    }
 }
 
 @Entity
-class MapBook {
+class PropertyBinderSpecCustomUserTypeCollection {
     Long id
-    String title
+    Set<String> categories
+    static mapping = {
+        // Assume this class exists or is mocked
+        categories type: 'org.hibernate.type.YesNoConverter' 
+    }
 }
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
index 60c06df7bb..bdedf6e6c1 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
@@ -29,7 +29,10 @@ class DetachedAssociationFunctionSpec extends Specification {
 
     def "apply returns list with criteria if it is 
DetachedAssociationCriteria"() {
         given:
-        def criteria = new DetachedAssociationCriteria(Object, "test")
+        def association = 
Mock(org.grails.datastore.mapping.model.types.Association) {
+            getName() >> "test"
+        }
+        def criteria = new DetachedAssociationCriteria(Object, association)
 
         when:
         def result = function.apply(criteria)

Reply via email to