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 a10ab1f03fb04aa2b0a47ec21bf01951bb5a32b4
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Thu Mar 19 08:27:29 2026 -0500

    hibernate 7:
    
    ### Mapping Closure Evaluation Bug (Fixed)
    **Issue:** In `HibernateMappingBuilder.groovy`, the `index` property was 
being aggressively converted to a `String` via `.toString()`. When a closure 
was provided (e.g., `index: { column 'foo' }`), the internal Groovy closure 
class name was stored instead of the closure itself, preventing proper 
evaluation during the second pass.
    **Action:** Removed the premature `.toString()` conversion in 
`HibernateMappingBuilder.handlePropertyInternal`.
    
    ### Named Unique Constraints Support (Implemented)
    **Issue:** During the exploration of `ColumnConfig` possibilities, it was 
noted that the `unique` property was restricted to `boolean`, preventing the 
use of named unique constraints (uniqueness groups) via the DSL.
    **Action:** Transitioned `unique` to `Object` in `ColumnConfig.groovy` and 
added an `isUnique()` helper for Java compatibility. Updated 
`HibernateMappingBuilder` and `ColumnConfigToColumnBinder` to handle the 
flexible type. This now allows configuration like `unique: 'groupName'` or 
`unique: ['group1', 'group2']` through the mapping DSL.
    
    ### Multi-Column Property Re-evaluation Bug (Fixed)
    **Issue:** Found a bug in `PropertyDefinitionDelegate` where re-evaluating 
a property with multiple columns would always overwrite the first column 
instead of correctly updating subsequent ones.
    **Action:** Fixed `PropertyDefinitionDelegate` to use the current `index` 
when accessing existing `ColumnConfig` objects. Added 
`PropertyDefinitionDelegateSpec` to verify the fix.
---
 .../grails/orm/hibernate/cfg/ColumnConfig.groovy   |  82 +-
 .../cfg/PropertyDefinitionDelegate.groovy          |   4 +-
 .../binder/ColumnConfigToColumnBinder.java         |   2 +-
 .../hibernate/HibernateMappingBuilder.groovy       |   4 +-
 .../hibernate/HibernateToManyProperty.java         |  98 ++-
 .../secondpass/CollectionSecondPassBinder.java     |  59 +-
 .../domainbinding/util/CascadeBehaviorFetcher.java |  95 ++-
 .../mapping/HibernateMappingBuilderSpec.groovy     | 458 +++++++++++
 .../mapping/HibernateMappingBuilderTests.groovy    | 902 ---------------------
 .../gorm/specs/HibernateGormDatastoreSpec.groovy   |  13 +-
 .../orm/hibernate/cfg/ColumnConfigSpec.groovy      | 155 ++++
 .../cfg/PropertyDefinitionDelegateSpec.groovy      |  61 ++
 .../CascadeBehaviorFetcherSpec.groovy              | 258 +++---
 .../ColumnConfigToColumnBinderSpec.groovy          |  48 ++
 .../hibernate/HibernateToManyPropertySpec.groovy   | 131 ++-
 .../CollectionSecondPassBinderSpec.groovy          |  48 +-
 .../secondpass/MapSecondPassBinderSpec.groovy      |  38 +-
 17 files changed, 1314 insertions(+), 1142 deletions(-)

diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy
index 8f2b92c3f0..5cdae991d7 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/ColumnConfig.groovy
@@ -49,10 +49,90 @@ class ColumnConfig {
      * The index, can be either a boolean or a string for the name of the index
      */
     def index
+
+    /**
+     * Parses the index field when stored as a Groovy-style string literal.
+     * Expected format: [column:item_idx, type:integer] or column:item_idx, 
type:integer
+     * Returns an empty map if parsing fails or the value is invalid.
+     * Throws IllegalArgumentException only if the format is clearly broken 
(fail-fast for bad developer input).
+     */
+    Map<String, String> getIndexAsMap() {
+        Object raw = this.index
+        if (raw == null) return [:]
+
+        if (raw instanceof Map) {
+            // Already a map → return as-is (though unlikely)
+            return raw.collectEntries { k, v -> [k.toString(), v.toString()] } 
as Map<String, String>
+        }
+
+        if (!(raw instanceof String)) {
+            // If it's a closure or something else, we can't parse it as a 
string map.
+            // Let the caller handle other types (like closures).
+            return [:]
+        }
+        String rawStr = raw.toString()
+
+        String content = rawStr.trim()
+
+        // Remove surrounding [ ] if present
+        if (content.startsWith('[') && content.endsWith(']')) {
+            content = content.substring(1, content.length() - 1).trim()
+        }
+
+        if (!content) return [:]
+
+        Map<String, String> result = [:]
+
+        // Split on top-level commas (simple heuristic: assume no commas 
inside values)
+        content.split(',').each { pair ->
+            def trimmed = pair.trim()
+            if (!trimmed) return
+
+            def kv = trimmed.split(':', 2)
+            if (kv.length != 2) {
+                // If it's the only pair and doesn't have a colon, treat it as 
the column name
+                if (content == trimmed && !content.contains(',')) {
+                    result['column'] = content
+                    return
+                }
+                // Invalid pair → fail fast (developer mistake)
+                throw new IllegalArgumentException(
+                        "Invalid index pair format '$trimmed' in string: 
'$raw'"
+                )
+            }
+
+            String key = kv[0].trim()
+            String value = kv[1].trim()
+
+            // Strip surrounding quotes from value if present
+            if ((value.startsWith("'") && value.endsWith("'")) ||
+                    (value.startsWith('"') && value.endsWith('"'))) {
+                value = value.substring(1, value.length() - 1)
+            }
+
+            result[key] = value
+        }
+
+        if (result.isEmpty()) {
+            throw new IllegalArgumentException("No valid key:value pairs found 
in index string: '$raw'")
+        }
+
+        return result
+    }
     /**
      * Whether the column is unique
      */
-    boolean unique = false
+    def unique = false
+
+    /**
+     * @return Whether the column is unique
+     */
+    boolean isUnique() {
+        if (unique instanceof Boolean) {
+            return (Boolean) unique
+        }
+        return unique != null && unique != false
+    }
     /**
      * The length of the column
      */
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy
index 6a0de90938..275c4513a4 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegate.groovy
@@ -54,7 +54,7 @@ class PropertyDefinitionDelegate {
         ColumnConfig column
         if (index < config.columns.size()) {
             // configure existing
-            column = config.columns[0]
+            column = config.columns[index]
         }
         else {
             column = new ColumnConfig()
@@ -65,7 +65,7 @@ class PropertyDefinitionDelegate {
         column.sqlType = args['sqlType']
         column.enumType = args['enumType'] ?: column.enumType
         column.index = args['index']
-        column.unique = args['unique'] ?: false
+        column.unique = args['unique'] != null ? args['unique'] : false
         column.length = args['length'] ? args['length'] as Integer : -1
         column.precision = args['precision'] ? args['precision'] as Integer  : 
-1
         column.scale = args['scale'] ? args['scale'] as Integer : -1
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java
index 9a9a6757be..af4704fe2b 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnConfigToColumnBinder.java
@@ -41,7 +41,7 @@ public class ColumnConfigToColumnBinder {
 
             Optional.ofNullable(mappedForm)
                     .filter(mf -> !mf.isUniqueWithinGroup())
-                    .ifPresent(mf -> column.setUnique(config.getUnique()));
+                    .ifPresent(mf -> column.setUnique(config.isUnique()));
         });
     }
 }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy
index 632cec0fe5..ba1ea1fdae 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy
@@ -366,9 +366,9 @@ class HibernateMappingBuilder implements 
MappingConfigurationBuilder<Mapping, Pr
             Object enumTypeVal = namedArgs['enumType']
             if (enumTypeVal) cc.enumType = enumTypeVal.toString()
             Object indexVal = namedArgs['index']
-            if (indexVal) cc.index = indexVal.toString()
+            if (indexVal) cc.index = indexVal
             Object ccUniqueVal = namedArgs['unique']
-            if (ccUniqueVal) cc.unique = ccUniqueVal instanceof Boolean ? 
(Boolean) ccUniqueVal : ccUniqueVal
+            if (ccUniqueVal != null) cc.unique = ccUniqueVal
             Object readVal = namedArgs['read']
             if (readVal) cc.read = readVal.toString()
             Object writeVal = namedArgs['write']
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
index 20bdb67177..2ac3f2c971 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
@@ -97,20 +97,86 @@ public interface HibernateToManyProperty extends 
PropertyWithMapping<PropertyCon
                 && !(this instanceof Basic);
     }
 
-    default String getIndexColumnType(String defaultType) {
-        return java.util.Optional.ofNullable(getMappedForm())
-                .map(PropertyConfig::getIndexColumn)
-                .map(ic -> getTypeName(ic, 
getHibernateOwner().getMappedForm()))
-                .orElse(defaultType);
+    default String getIndexColumnName(PersistentEntityNamingStrategy 
namingStrategy) {
+        PropertyConfig mapped = getMappedForm();
+
+        if (mapped != null && mapped.getIndexColumn() != null) {
+            PropertyConfig indexColConfig = mapped.getIndexColumn();
+            if (!indexColConfig.getColumns().isEmpty()) {
+                String name = indexColConfig.getColumns().get(0).getName();
+                if (StringUtils.hasText(name)) {
+                    return name;
+                }
+            }
+        }
+
+        if (mapped == null || mapped.getColumns().isEmpty()) {
+            return namingStrategy.resolveColumnName(getName()) + UNDERSCORE + 
IndexedCollection.DEFAULT_INDEX_COLUMN_NAME;
+        }
+
+        ColumnConfig primaryCol = mapped.getColumns().get(0);
+        Object rawIndex = primaryCol.getIndex();
+        if (rawIndex instanceof groovy.lang.Closure) {
+            PropertyConfig indexColConfig = 
PropertyConfig.configureNew((groovy.lang.Closure<?>) rawIndex);
+            if (!indexColConfig.getColumns().isEmpty()) {
+                String name = indexColConfig.getColumns().get(0).getName();
+                if (StringUtils.hasText(name)) {
+                    return name;
+                }
+            }
+        }
+
+        try {
+            Map<String, String> indexMap = primaryCol.getIndexAsMap();
+            String colName = indexMap.get("column");
+
+            if (StringUtils.hasText(colName)) {
+                return colName;
+            }
+        }
+        catch (Exception e) {
+            // ignore
+        }
+
+        return namingStrategy.resolveColumnName(getName()) + UNDERSCORE + 
IndexedCollection.DEFAULT_INDEX_COLUMN_NAME;
     }
 
-    default String getIndexColumnName(PersistentEntityNamingStrategy 
namingStrategy) {
-        return java.util.Optional.ofNullable(getMappedForm())
-                .map(PropertyConfig::getIndexColumn)
-                .map(PropertyConfig::getColumn)
-                .orElseGet(() -> namingStrategy.resolveColumnName(getName())
-                        + GrailsDomainBinder.UNDERSCORE
-                        + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME);
+    default String getIndexColumnType(String defaultType) {
+        PropertyConfig mapped = getMappedForm();
+
+        if (mapped != null && mapped.getIndexColumn() != null) {
+            PropertyConfig indexColConfig = mapped.getIndexColumn();
+            if (StringUtils.hasText(indexColConfig.getTypeName())) {
+                return indexColConfig.getTypeName();
+            }
+        }
+
+        if (mapped == null || mapped.getColumns().isEmpty()) {
+            return defaultType;
+        }
+
+        ColumnConfig primaryCol = mapped.getColumns().get(0);
+        Object rawIndex = primaryCol.getIndex();
+        if (rawIndex instanceof groovy.lang.Closure) {
+            PropertyConfig indexColConfig = 
PropertyConfig.configureNew((groovy.lang.Closure<?>) rawIndex);
+            if (StringUtils.hasText(indexColConfig.getTypeName())) {
+                return indexColConfig.getTypeName();
+            }
+        }
+
+        try {
+            Map<String, String> indexMap = primaryCol.getIndexAsMap();
+            String typeName = indexMap.get("type");
+
+            if (StringUtils.hasText(typeName)) {
+                return typeName;
+            }
+        }
+        catch (Exception e) {
+            // ignore
+        }
+
+        return defaultType;
     }
 
     default String getMapElementName(PersistentEntityNamingStrategy 
namingStrategy) {
@@ -128,9 +194,9 @@ public interface HibernateToManyProperty extends 
PropertyWithMapping<PropertyCon
                 .map(PropertyConfig::getJoinTableColumnConfig)
                 .map(ColumnConfig::getName)
                 .orElseGet(() -> 
namingStrategy.resolveColumnName(getHibernateAssociatedEntity()
-                                .getHibernateRootEntity()
-                                .getJavaClass()
-                                .getSimpleName())
+                        .getHibernateRootEntity()
+                        .getJavaClass()
+                        .getSimpleName())
                         + GrailsDomainBinder.FOREIGN_KEY_SUFFIX);
     }
 
@@ -162,4 +228,4 @@ public interface HibernateToManyProperty extends 
PropertyWithMapping<PropertyCon
 
     void setCollection(Collection collection);
     Collection getCollection();
-}
+}
\ No newline at end of file
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
index 315c28ada7..fe8d68ae51 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinder.java
@@ -20,21 +20,23 @@ package 
org.grails.orm.hibernate.cfg.domainbinding.secondpass;
 
 import java.util.*;
 import java.util.Map;
-
 import jakarta.annotation.Nonnull;
 
+import org.grails.datastore.mapping.model.types.Basic;
 import org.hibernate.MappingException;
 import org.hibernate.mapping.*;
 import org.hibernate.mapping.Collection;
-
 import 
org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionForPropertyConfigBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToManyProperty;
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty;
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty;
 
-/** Refactored from CollectionBinder to handle collection second pass binding. 
*/
+/**
+ * Refactored from CollectionBinder to handle collection second pass binding.
+ */
 @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
 public class CollectionSecondPassBinder {
+
     private final CollectionOrderByBinder collectionOrderByBinder;
     private final CollectionMultiTenantFilterBinder 
collectionMultiTenantFilterBinder;
     private final CollectionKeyColumnUpdater collectionKeyColumnUpdater;
@@ -44,7 +46,6 @@ public class CollectionSecondPassBinder {
     private final CollectionWithJoinTableBinder collectionWithJoinTableBinder;
     private final CollectionForPropertyConfigBinder 
collectionForPropertyConfigBinder;
 
-    /** Creates a new {@link CollectionSecondPassBinder} instance. */
     public CollectionSecondPassBinder(
             CollectionKeyColumnUpdater collectionKeyColumnUpdater,
             UnidirectionalOneToManyBinder unidirectionalOneToManyBinder,
@@ -64,27 +65,29 @@ public class CollectionSecondPassBinder {
         this.collectionMultiTenantFilterBinder = 
collectionMultiTenantFilterBinder;
     }
 
-    /** Bind collection second pass. */
-    public void bindCollectionSecondPass(
-            @Nonnull HibernateToManyProperty property,
-            Map<?, ?> persistentClasses) {
-
+    public void bindCollectionSecondPass(@Nonnull HibernateToManyProperty 
property, Map<?, ?> persistentClasses) {
         Collection collection = property.getCollection();
-        PersistentClass associatedClass = resolveAssociatedClass(property, 
persistentClasses);
-        collectionOrderByBinder.bind(property, associatedClass);
-        bindOneToManyAssociation(property, associatedClass);
-
-        collectionMultiTenantFilterBinder.bind(property);
-        collection.setSorted(property.isSorted());
 
-        collectionKeyColumnUpdater.bind(property, associatedClass);
-        collection.setCacheConcurrencyStrategy(property.getCacheUsage());
-
-        bindCollectionElement(property);
+        if (property instanceof Basic) {
+            // Basic collections (scalars/enums) don't have an associated 
PersistentClass
+            collectionMultiTenantFilterBinder.bind(property);
+            collection.setSorted(property.isSorted());
+            collectionKeyColumnUpdater.bind(property, null);
+            collection.setCacheConcurrencyStrategy(property.getCacheUsage());
+            bindCollectionElement(property);
+        } else {
+            PersistentClass associatedClass = resolveAssociatedClass(property, 
persistentClasses);
+            collectionOrderByBinder.bind(property, associatedClass);
+            bindOneToManyAssociation(property, associatedClass);
+            collectionMultiTenantFilterBinder.bind(property);
+            collection.setSorted(property.isSorted());
+            collectionKeyColumnUpdater.bind(property, associatedClass);
+            collection.setCacheConcurrencyStrategy(property.getCacheUsage());
+            bindCollectionElement(property);
+        }
     }
 
-    private void bindOneToManyAssociation(
-            HibernateToManyProperty property, PersistentClass associatedClass) 
{
+    private void bindOneToManyAssociation(HibernateToManyProperty property, 
PersistentClass associatedClass) {
         Collection collection = property.getCollection();
         if (!collection.isOneToMany()) {
             return;
@@ -97,25 +100,21 @@ public class CollectionSecondPassBinder {
         
collectionForPropertyConfigBinder.bindCollectionForPropertyConfig(property);
     }
 
-    private void bindCollectionElement(
-            HibernateToManyProperty property) {
+    private void bindCollectionElement(HibernateToManyProperty property) {
         if (property instanceof HibernateManyToManyProperty manyToMany && 
manyToMany.isBidirectional()) {
             manyToManyElementBinder.bind(manyToMany);
         } else if (property.isBidirectionalOneToManyMap() && 
property.isBidirectional()) {
             bidirectionalMapElementBinder.bind(property);
-        } else if (property instanceof HibernateOneToManyProperty 
oneToManyProperty
-                && oneToManyProperty.isUnidirectionalOneToMany()) {
+        } else if (property instanceof HibernateOneToManyProperty 
oneToManyProperty && oneToManyProperty.isUnidirectionalOneToMany()) {
             unidirectionalOneToManyBinder.bind(oneToManyProperty);
         } else if (property.supportsJoinColumnMapping()) {
             
collectionWithJoinTableBinder.bindCollectionWithJoinTable(property);
         }
     }
 
-    private @Nonnull PersistentClass resolveAssociatedClass(
-            HibernateToManyProperty property, Map<?, ?> persistentClasses) {
+    protected PersistentClass resolveAssociatedClass(HibernateToManyProperty 
property, Map<?, ?> persistentClasses) {
         return Optional.ofNullable(property.getHibernateAssociatedEntity())
                 .map(referenced -> (PersistentClass) 
persistentClasses.get(referenced.getName()))
-                .orElseThrow(
-                        () -> new MappingException("Association [" + 
property.getName() + "] has no associated class"));
+                .orElseThrow(() -> new MappingException("Association [" + 
property.getName() + "] has no associated class"));
     }
-}
+}
\ No newline at end of file
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java
index c9be0c3066..41a972206d 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/CascadeBehaviorFetcher.java
@@ -1,20 +1,20 @@
 /*
- *  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
+ * 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
+ * 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.
+ * 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.cfg.domainbinding.util;
 
@@ -37,28 +37,38 @@ import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentP
 
 import static 
org.grails.orm.hibernate.cfg.domainbinding.util.CascadeBehavior.*;
 
-/** The cascade behavior fetcher class. */
+/**
+ * The cascade behavior fetcher class.
+ */
 public class CascadeBehaviorFetcher {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(CascadeBehaviorFetcher.class);
 
     private final LogCascadeMapping logCascadeMapping;
 
-    /** Creates a new {@link CascadeBehaviorFetcher} instance. */
+    /**
+     * Creates a new {@link CascadeBehaviorFetcher} instance.
+     */
     public CascadeBehaviorFetcher(LogCascadeMapping logCascadeMapping) {
         this.logCascadeMapping = logCascadeMapping;
     }
 
-    /** Creates a new {@link CascadeBehaviorFetcher} instance. */
+    /**
+     * Creates a new {@link CascadeBehaviorFetcher} instance.
+     */
     public CascadeBehaviorFetcher() {
         this(new LogCascadeMapping(LOG));
     }
 
-    /** Gets the cascade behaviour. */
+    /**
+     * Gets the cascade behaviour.
+     */
     public String getCascadeBehaviour(Association<?> association) {
-        var cascadeStrategy =
-                getDefinedBehavior((HibernatePersistentProperty) 
association).orElse(getImpliedBehavior(association));
+        var cascadeStrategy = getDefinedBehavior((HibernatePersistentProperty) 
association)
+                .orElse(getImpliedBehavior(association));
+
         logCascadeMapping.logCascadeMapping(association, cascadeStrategy);
+
         return cascadeStrategy.getValue();
     }
 
@@ -69,35 +79,50 @@ public class CascadeBehaviorFetcher {
     }
 
     private CascadeBehavior getImpliedBehavior(Association<?> association) {
-        if (association.getAssociatedEntity() == null) {
-            // NEW BEHAVIOR, FAIL-FAST
-            throw new MappingException("Relationship " + association + " has 
no associated entity");
+        // Handle types that do not require an associated entity first
+        if (association instanceof Basic) {
+            return ALL;
         }
+
+        if (Map.class.isAssignableFrom(association.getType())) {
+            return association.isCorrectlyOwned() ? ALL : SAVE_UPDATE;
+        }
+
         if (association instanceof Embedded) {
             return ALL;
         }
+
+        // Fail-fast only for entity relationships that are truly missing an 
association
+        if (association.getAssociatedEntity() == null) {
+            throw new MappingException("Relationship " + association + " has 
no associated entity");
+        }
+
         if (association.isHasOne()) {
             return ALL;
-        } else if (association instanceof HibernateOneToOneProperty) {
+        }
+        else if (association instanceof HibernateOneToOneProperty) {
             return association.isOwningSide() ? ALL : SAVE_UPDATE;
-        } else if (association instanceof HibernateOneToManyProperty) {
+        }
+        else if (association instanceof HibernateOneToManyProperty) {
             return association.isCorrectlyOwned() ? ALL : SAVE_UPDATE;
-        } else if (association instanceof HibernateManyToManyProperty) {
+        }
+        else if (association instanceof HibernateManyToManyProperty) {
             return association.isCorrectlyOwned() || association.isCircular() 
? SAVE_UPDATE : NONE;
-        } else if (association instanceof HibernateManyToOneProperty) {
+        }
+        else if (association instanceof HibernateManyToOneProperty) {
             if (association.isCorrectlyOwned() && !association.isCircular()) {
                 return ALL;
-            } else if (association.isCompositeIdProperty()) {
+            }
+            else if (association.isCompositeIdProperty()) {
                 return ALL;
-            } else {
+            }
+            else {
                 return NONE;
             }
-        } else if (association instanceof Basic) {
-            return ALL;
-        } else if (Map.class.isAssignableFrom(association.getType())) {
-            return association.isCorrectlyOwned() ? ALL : SAVE_UPDATE;
-        } else {
+        }
+        else {
             throw new MappingException("Unrecognized association type " + 
association.getType());
         }
     }
-}
+
+}
\ No newline at end of file
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy
index d9bd858484..e9f8d6f75b 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy
@@ -40,6 +40,462 @@ class HibernateMappingBuilderSpec extends Specification {
         builder().evaluate(cl)
     }
 
+    // 
-------------------------------------------------------------------------
+    // table / catalog / schema / comment
+    // 
-------------------------------------------------------------------------
+
+    def "table with name only"() {
+        when:
+        Mapping m = evaluate { table 'myTable' }
+
+        then:
+        m.tableName == 'myTable'
+    }
+
+    def "table with catalog and schema"() {
+        when:
+        Mapping m = evaluate { table name: 'table', catalog: 'CRM', schema: 
'dbo' }
+
+        then:
+        m.table.name == 'table'
+        m.table.schema == 'dbo'
+        m.table.catalog == 'CRM'
+    }
+
+    def "table comment is stored"() {
+        when:
+        Mapping m = evaluate { comment 'wahoo' }
+
+        then:
+        m.comment == 'wahoo'
+    }
+
+    // 
-------------------------------------------------------------------------
+    // version / autoTimestamp
+    // 
-------------------------------------------------------------------------
+
+    def "version column can be changed"() {
+        when:
+        Mapping m = evaluate { version 'v_number' }
+
+        then:
+        m.getPropertyConfig("version").column == 'v_number'
+    }
+
+    def "versioning can be disabled"() {
+        when:
+        Mapping m = evaluate { version false }
+
+        then:
+        !m.versioned
+    }
+
+    def "autoTimestamp can be disabled"() {
+        when:
+        Mapping m = evaluate { autoTimestamp false }
+
+        then:
+        !m.autoTimestamp
+    }
+
+    // 
-------------------------------------------------------------------------
+    // discriminator
+    // 
-------------------------------------------------------------------------
+
+    def "discriminator value only"() {
+        when:
+        Mapping m = evaluate { discriminator 'one' }
+
+        then:
+        m.discriminator.value == 'one'
+        m.discriminator.column == null
+    }
+
+    def "discriminator with column name"() {
+        when:
+        Mapping m = evaluate { discriminator value: 'one', column: 'type' }
+
+        then:
+        m.discriminator.value == 'one'
+        m.discriminator.column.name == 'type'
+    }
+
+    def "discriminator with column map"() {
+        when:
+        Mapping m = evaluate { discriminator value: 'one', column: [name: 
'type', sqlType: 'integer'] }
+
+        then:
+        m.discriminator.value == 'one'
+        m.discriminator.column.name == 'type'
+        m.discriminator.column.sqlType == 'integer'
+    }
+
+    def "discriminator with formula and other settings"() {
+        when:
+        Mapping m = evaluate {
+            discriminator value: '1', formula: "case when CLASS_TYPE in ('a', 
'b', 'c') then 0 else 1 end", type: 'integer', insert: false
+        }
+
+        then:
+        m.discriminator.value == '1'
+        m.discriminator.formula == "case when CLASS_TYPE in ('a', 'b', 'c') 
then 0 else 1 end"
+        m.discriminator.type == 'integer'
+        !m.discriminator.insertable
+    }
+
+    // 
-------------------------------------------------------------------------
+    // inheritance
+    // 
-------------------------------------------------------------------------
+
+    def "tablePerHierarchy false disables it"() {
+        when:
+        Mapping m = evaluate { tablePerHierarchy false }
+
+        then:
+        !m.tablePerHierarchy
+    }
+
+    def "tablePerSubclass true disables tablePerHierarchy"() {
+        when:
+        Mapping m = evaluate { tablePerSubclass true }
+
+        then:
+        !m.tablePerHierarchy
+    }
+
+    def "tablePerConcreteClass true enables it and disables 
tablePerHierarchy"() {
+        when:
+        Mapping m = evaluate { tablePerConcreteClass true }
+
+        then:
+        m.tablePerConcreteClass
+        !m.tablePerHierarchy
+    }
+
+    // 
-------------------------------------------------------------------------
+    // cache settings
+    // 
-------------------------------------------------------------------------
+
+    def "default cache strategy"() {
+        when:
+        Mapping m = evaluate { cache true }
+
+        then:
+        m.cache.usage.toString() == 'read-write'
+        m.cache.include.toString() == 'all'
+    }
+
+    def "custom cache strategy"() {
+        when:
+        Mapping m = evaluate { cache usage: 'read-only', include: 'non-lazy' }
+
+        then:
+        m.cache.usage.toString() == 'read-only'
+        m.cache.include.toString() == 'non-lazy'
+    }
+
+    def "custom cache strategy with usage string only"() {
+        when:
+        Mapping m = evaluate { cache 'read-only' }
+
+        then:
+        m.cache.usage.toString() == 'read-only'
+        m.cache.include.toString() == 'all'
+    }
+
+    def "invalid cache values are ignored and defaults used"() {
+        when:
+        Mapping m = evaluate { cache usage: 'rubbish', include: 'more-rubbish' 
}
+
+        then:
+        m.cache.usage.toString() == 'read-write'
+        m.cache.include.toString() == 'all'
+    }
+
+    // 
-------------------------------------------------------------------------
+    // identity / id
+    // 
-------------------------------------------------------------------------
+
+    def "identity column mapping"() {
+        when:
+        Mapping m = evaluate { id column: 'foo_id', type: Integer }
+
+        then:
+        m.identity.type == Long // Default remains Long? No, wait.
+        // In HibernateMappingBuilderTests:
+        // assertEquals Long, mapping.identity.type
+        // assertEquals 'foo_id', mapping.getPropertyConfig("id").column
+        // assertEquals Integer, mapping.getPropertyConfig("id").type
+        m.getPropertyConfig("id").column == 'foo_id'
+        m.getPropertyConfig("id").type == Integer
+        m.identity.generator == 'native'
+    }
+
+    def "default id strategy"() {
+        when:
+        Mapping m = evaluate { }
+
+        then:
+        m.identity.type == Long
+        m.identity.column == 'id'
+        m.identity.generator == 'native'
+    }
+
+    def "hilo id strategy"() {
+        when:
+        Mapping m = evaluate { id generator: 'hilo', params: [table: 
'hi_value', column: 'next_value', max_lo: 100] }
+
+        then:
+        m.identity.column == 'id'
+        m.identity.generator == 'hilo'
+        m.identity.params.table == 'hi_value'
+    }
+
+    def "composite id strategy"() {
+        when:
+        Mapping m = evaluate { id composite: ['one', 'two'], compositeClass: 
HibernateMappingBuilder }
+
+        then:
+        m.identity instanceof org.grails.orm.hibernate.cfg.CompositeIdentity
+        m.identity.propertyNames == ['one', 'two']
+        m.identity.compositeClass == HibernateMappingBuilder
+    }
+
+    def "natural id mapping"() {
+        expect:
+        evaluate { id natural: 'one' }.identity.natural.propertyNames == 
['one']
+        evaluate { id natural: ['one', 'two'] }.identity.natural.propertyNames 
== ['one', 'two']
+        evaluate { id natural: [properties: ['one', 'two'], mutable: true] 
}.identity.natural.mutable
+    }
+
+    // 
-------------------------------------------------------------------------
+    // other root settings
+    // 
-------------------------------------------------------------------------
+
+    def "autoImport defaults to true and can be disabled"() {
+        expect:
+        evaluate { }.autoImport
+        !evaluate { autoImport false }.autoImport
+    }
+
+    def "dynamicUpdate and dynamicInsert"() {
+        when:
+        Mapping m = evaluate {
+            dynamicUpdate true
+            dynamicInsert true
+        }
+
+        then:
+        m.dynamicUpdate
+        m.dynamicInsert
+
+        when:
+        m = evaluate { }
+
+        then:
+        !m.dynamicUpdate
+        !m.dynamicInsert
+    }
+
+    def "batchSize config"() {
+        when:
+        Mapping m = evaluate {
+            batchSize 10
+            things batchSize: 15
+        }
+
+        then:
+        m.batchSize == 10
+        m.getPropertyConfig('things').batchSize == 15
+    }
+
+    def "class sort order"() {
+        when:
+        Mapping m = evaluate {
+            sort "name"
+            order "desc"
+        }
+
+        then:
+        m.sort.name == "name"
+        m.sort.direction == "desc"
+    }
+
+    def "class sort order via map"() {
+        when:
+        Mapping m = evaluate {
+            sort name: 'desc'
+        }
+
+        then:
+        m.sort.namesAndDirections == [name: 'desc']
+    }
+
+    def "property ignoreNotFound is stored"() {
+        expect:
+        evaluate { foos ignoreNotFound: true 
}.getPropertyConfig("foos").ignoreNotFound
+        !evaluate { foos ignoreNotFound: false 
}.getPropertyConfig("foos").ignoreNotFound
+    }
+
+    def "property association sort order"() {
+        when:
+        Mapping m = evaluate {
+            columns {
+                things sort: 'name'
+            }
+        }
+
+        then:
+        m.getPropertyConfig('things').sort == 'name'
+    }
+
+    def "property lazy settings"() {
+        expect:
+        evaluate { things column: 'foo' 
}.getPropertyConfig('things').getLazy() == null
+        !evaluate { things lazy: false }.getPropertyConfig('things').lazy
+    }
+
+    def "property cascades"() {
+        expect:
+        evaluate { things cascade: 'persist,merge' 
}.getPropertyConfig('things').cascade == 'persist,merge'
+        evaluate { columns { things cascade: 'all' } 
}.getPropertyConfig('things').cascade == 'all'
+    }
+
+    def "property fetch modes"() {
+        expect:
+        evaluate { things fetch: 'join' 
}.getPropertyConfig('things').fetchMode == FetchMode.JOIN
+        evaluate { things fetch: 'select' 
}.getPropertyConfig('things').fetchMode == FetchMode.SELECT
+        evaluate { things column: 'foo' 
}.getPropertyConfig('things').fetchMode == FetchMode.DEFAULT
+    }
+
+    def "property enumType"() {
+        expect:
+        evaluate { things column: 'foo' }.getPropertyConfig('things').enumType 
== 'default'
+        evaluate { things enumType: 'ordinal' 
}.getPropertyConfig('things').enumType == 'ordinal'
+    }
+
+    def "property joinTable mapping"() {
+        when:
+        Mapping m1 = evaluate { things joinTable: true }
+        Mapping m2 = evaluate { things joinTable: 'foo' }
+        Mapping m3 = evaluate { things joinTable: [name: 'foo', key: 'foo_id', 
column: 'bar_id'] }
+
+        then:
+        m1.getPropertyConfig('things').joinTable != null
+        m2.getPropertyConfig('things').joinTable.name == 'foo'
+        m3.getPropertyConfig('things').joinTable.name == 'foo'
+        m3.getPropertyConfig('things').joinTable.key.name == 'foo_id'
+        m3.getPropertyConfig('things').joinTable.column.name == 'bar_id'
+    }
+
+    def "property custom association caching"() {
+        when:
+        Mapping m1 = evaluate { firstName cache: [usage: 'read-only', include: 
'non-lazy'] }
+        Mapping m2 = evaluate { firstName cache: 'read-only' }
+        Mapping m3 = evaluate { firstName cache: true }
+
+        then:
+        m1.getPropertyConfig('firstName').cache.usage.toString() == 'read-only'
+        m1.getPropertyConfig('firstName').cache.include.toString() == 
'non-lazy'
+        m2.getPropertyConfig('firstName').cache.usage.toString() == 'read-only'
+        m3.getPropertyConfig('firstName').cache.usage.toString() == 
'read-write'
+        m3.getPropertyConfig('firstName').cache.include.toString() == 'all'
+    }
+
+    def "simple column mappings"() {
+        when:
+        Mapping m = evaluate {
+            firstName column: 'First_Name'
+            lastName column: 'Last_Name'
+        }
+
+        then:
+        m.getPropertyConfig('firstName').column == 'First_Name'
+        m.getPropertyConfig('lastName').column == 'Last_Name'
+    }
+
+    def "complex column mappings"() {
+        when:
+        Mapping m = evaluate {
+            firstName column: 'First_Name',
+                    lazy: true,
+                    unique: true,
+                    type: java.sql.Clob,
+                    length: 255,
+                    index: 'foo',
+                    sqlType: 'text'
+        }
+
+        then:
+        m.columns.firstName.column == 'First_Name'
+        m.columns.firstName.lazy
+        m.columns.firstName.isUnique()
+        m.columns.firstName.type == java.sql.Clob
+        m.columns.firstName.length == 255
+        m.columns.firstName.getIndexName() == 'foo'
+        m.columns.firstName.sqlType == 'text'
+    }
+
+    def "property with multiple columns"() {
+        when:
+        Mapping m = evaluate {
+            amount type: MyUserType, {
+                column name: "value"
+                column name: "currency", sqlType: "char", length: 3
+            }
+        }
+
+        then:
+        m.columns.amount.columns.size() == 2
+        m.columns.amount.columns[0].name == "value"
+        m.columns.amount.columns[1].name == "currency"
+        m.columns.amount.columns[1].sqlType == "char"
+        m.columns.amount.columns[1].length == 3
+    }
+
+    def "disallowed multi-column property access"() {
+        given:
+        def b = builder()
+        b.evaluate {
+            amount type: MyUserType, {
+                column name: "value"
+                column name: "currency"
+            }
+        }
+
+        when:
+        b.evaluate { amount scale: 2 }
+
+        then:
+        thrown(Throwable)
+    }
+
+    def "property with user type and params"() {
+        when:
+        Mapping m = evaluate {
+            amount type: MyUserType, params: [param1: "amountParam1", param2: 
65]
+        }
+
+        then:
+        m.getPropertyConfig('amount').type == MyUserType
+        m.getPropertyConfig('amount').typeParams.param1 == "amountParam1"
+        m.getPropertyConfig('amount').typeParams.param2 == 65
+    }
+
+    def "property insertable and updatable"() {
+        when:
+        Mapping m = evaluate {
+            firstName insertable: true, updatable: true
+            lastName insertable: false, updatable: false
+        }
+
+        then:
+        m.getPropertyConfig('firstName').insertable
+        m.getPropertyConfig('firstName').updatable
+        !m.getPropertyConfig('lastName').insertable
+        !m.getPropertyConfig('lastName').updatable
+    }
+
     // 
-------------------------------------------------------------------------
     // autowire / tenantId
     // 
-------------------------------------------------------------------------
@@ -399,4 +855,6 @@ class HibernateMappingBuilderSpec extends Specification {
         noExceptionThrown()
         m.getPropertyConfig('myProp') == null
     }
+
+    static class MyUserType {}
 }
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy
deleted file mode 100644
index ac586e0477..0000000000
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderTests.groovy
+++ /dev/null
@@ -1,902 +0,0 @@
-/*
- *  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.hibernate.mapping
-
-import java.sql.Clob
-
-import org.grails.orm.hibernate.cfg.CompositeIdentity
-import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateMappingBuilder
-import org.grails.orm.hibernate.cfg.PropertyConfig
-
-import org.hibernate.FetchMode
-import org.junit.jupiter.api.Test
-
-import static org.junit.jupiter.api.Assertions.assertEquals
-import static org.junit.jupiter.api.Assertions.assertFalse
-import static org.junit.jupiter.api.Assertions.assertNull
-import static org.junit.jupiter.api.Assertions.assertThrows
-import static org.junit.jupiter.api.Assertions.assertTrue
-
-/**
- * Tests that the Hibernate mapping DSL constructs a valid Mapping object.
- *
- * @author Graeme Rocher
- * @since 1.0
- */
-class HibernateMappingBuilderTests {
-
-//    void testWildcardApplyToAllProperties() {
-//        def builder = new HibernateMappingBuilder("Foo")
-//        def mapping = builder.evaluate {
-//            '*'(column:"foo")
-//            '*-1'(column:"foo")
-//            '1-1'(column:"foo")
-//            '1-*'(column:"foo")
-//            '*-*'(column:"foo")
-//            one cache:true
-//            two ignoreNoteFound:false
-//        }
-//    }
-
-    @Test
-    void testIncludes() {
-        def callable = {
-            foos lazy:false
-        }
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            includes callable
-            foos ignoreNotFound:true
-        }
-
-        def pc = mapping.getPropertyConfig("foos")
-        assert pc.ignoreNotFound : "should have ignoreNotFound enabled"
-        assert !pc.lazy : "should not be lazy"
-    }
-
-    @Test
-    void testIgnoreNotFound() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            foos ignoreNotFound:true
-        }
-
-        assertTrue mapping.getPropertyConfig("foos").ignoreNotFound, "ignore 
not found should have been true"
-
-        mapping = builder.evaluate {
-            foos ignoreNotFound:false
-        }
-        assertFalse mapping.getPropertyConfig("foos").ignoreNotFound, "ignore 
not found should have been false"
-
-        mapping = builder.evaluate { // default
-            foos lazy:false
-        }
-        assertFalse mapping.getPropertyConfig("foos").ignoreNotFound, "ignore 
not found should have been false"
-    }
-
-    @Test
-    void testNaturalId() {
-
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            id natural: 'one'
-        }
-
-        assertEquals(['one'], mapping.identity.natural.propertyNames)
-
-        mapping = builder.evaluate {
-            id natural: ['one','two']
-        }
-
-        assertEquals(['one','two'], mapping.identity.natural.propertyNames)
-
-        mapping = builder.evaluate {
-            id natural: [properties:['one','two'], mutable:true]
-        }
-
-        assertEquals(['one','two'], mapping.identity.natural.propertyNames)
-        assertTrue mapping.identity.natural.mutable
-    }
-
-    @Test
-    void testDiscriminator() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            discriminator 'one'
-        }
-
-        assertEquals "one", mapping.discriminator.value
-        assertNull mapping.discriminator.column
-
-        mapping = builder.evaluate {
-            discriminator value:'one', column:'type'
-        }
-
-        assertEquals "one", mapping.discriminator.value
-        assertEquals "type", mapping.discriminator.column.name
-
-        mapping = builder.evaluate {
-            discriminator value:'one', column:[name:'type', sqlType:'integer']
-        }
-
-        assertEquals "one", mapping.discriminator.value
-        assertEquals "type", mapping.discriminator.column.name
-        assertEquals "integer", mapping.discriminator.column.sqlType
-    }
-
-    @Test
-    void testDiscriminatorMap() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            discriminator value:'1', formula:"case when CLASS_TYPE in ('a', 
'b', 'c') then 0 else 1 end",type:'integer',insert:false
-        }
-
-        assertEquals "1", mapping.discriminator.value
-        assertNull mapping.discriminator.column
-
-        assertEquals "case when CLASS_TYPE in ('a', 'b', 'c') then 0 else 1 
end", mapping.discriminator.formula
-        assertEquals "integer", mapping.discriminator.type
-        assertFalse mapping.discriminator.insertable
-    }
-
-    @Test
-    void testAutoImport() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate { }
-
-        assertTrue mapping.autoImport, "default auto-import should be true"
-
-        mapping = builder.evaluate {
-            autoImport false
-        }
-
-        assertFalse mapping.autoImport, "auto-import should be false"
-    }
-
-    @Test
-    void testTableWithCatalogueAndSchema() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table name:"table", catalog:"CRM", schema:"dbo"
-        }
-
-        assertEquals 'table',mapping.table.name
-        assertEquals 'dbo',mapping.table.schema
-        assertEquals 'CRM',mapping.table.catalog
-    }
-
-    @Test
-    void testIndexColumn() {
-
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            things indexColumn:[name:"chapter_number", type:"string", length:3]
-        }
-
-        PropertyConfig pc = mapping.getPropertyConfig("things")
-        assertEquals "chapter_number",pc.indexColumn.column
-        assertEquals "string",pc.indexColumn.type
-        assertEquals 3, pc.indexColumn.length
-    }
-
-    @Test
-    void testDynamicUpdate() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            dynamicUpdate true
-            dynamicInsert true
-        }
-
-        assertTrue mapping.dynamicUpdate
-        assertTrue mapping.dynamicInsert
-
-        builder = new HibernateMappingBuilder("Foo")
-        mapping = builder.evaluate {}
-
-        assertFalse mapping.dynamicUpdate
-        assertFalse mapping.dynamicInsert
-    }
-
-    @Test
-    void testBatchSizeConfig() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            batchSize 10
-            things batchSize:15
-        }
-
-        assertEquals 10, mapping.batchSize
-        assertEquals 15,mapping.getPropertyConfig('things').batchSize
-    }
-
-    @Test
-    void testChangeVersionColumn() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            version 'v_number'
-        }
-
-        assertEquals 'v_number', mapping.getPropertyConfig("version").column
-    }
-
-    @Test
-    void testClassSortOrder() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            sort "name"
-            order "desc"
-            columns {
-                things sort:'name'
-            }
-        }
-
-        assertEquals "name", mapping.sort.name
-        assertEquals "desc", mapping.sort.direction
-        assertEquals 'name',mapping.getPropertyConfig('things').sort
-
-        mapping = builder.evaluate {
-            sort name:'desc'
-
-            columns {
-                things sort:'name'
-            }
-        }
-
-        assertEquals "name", mapping.sort.name
-        assertEquals "desc", mapping.sort.direction
-        assertEquals 'name',mapping.getPropertyConfig('things').sort
-    }
-
-    @Test
-    void testAssociationSortOrder() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            columns {
-                things sort:'name'
-            }
-        }
-
-        assertEquals 'name',mapping.getPropertyConfig('things').sort
-    }
-
-    @Test
-    void testLazy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            columns {
-                things cascade:'persist,merge'
-            }
-        }
-
-        assertNull mapping.getPropertyConfig('things').getLazy(), "should have 
been lazy"
-
-        mapping = builder.evaluate {
-            columns {
-                things lazy:false
-            }
-        }
-
-        assertFalse mapping.getPropertyConfig('things').lazy, "shouldn't have 
been lazy"
-    }
-
-    @Test
-    void testCascades() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            columns {
-                things cascade:'persist,merge'
-            }
-        }
-
-        assertEquals 
'persist,merge',mapping.getPropertyConfig('things').cascade
-    }
-
-    @Test
-    void testFetchModes() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            columns {
-                things fetch:'join'
-                others fetch:'select'
-                mores column:'yuck'
-            }
-        }
-
-        assertEquals 
FetchMode.JOIN,mapping.getPropertyConfig('things').fetchMode
-        assertEquals 
FetchMode.SELECT,mapping.getPropertyConfig('others').fetchMode
-        assertEquals 
FetchMode.DEFAULT,mapping.getPropertyConfig('mores').fetchMode
-    }
-
-    @Test
-    void testEnumType() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            columns {
-                things column:'foo'
-            }
-        }
-
-        assertEquals 'default',mapping.getPropertyConfig('things').enumType
-
-        mapping = builder.evaluate {
-            columns {
-                things enumType:'ordinal'
-            }
-        }
-
-        assertEquals 'ordinal',mapping.getPropertyConfig('things').enumType
-    }
-
-    @Test
-    void testCascadesWithColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            things cascade:'persist,merge'
-        }
-        assertEquals 
'persist,merge',mapping.getPropertyConfig('things').cascade
-    }
-
-    @Test
-    void testJoinTableMapping() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            columns {
-                things joinTable:true
-            }
-        }
-
-        assert mapping.getPropertyConfig('things')?.joinTable
-
-        mapping = builder.evaluate {
-            columns {
-                things joinTable:'foo'
-            }
-        }
-
-        PropertyConfig property = mapping.getPropertyConfig('things')
-        assert property?.joinTable
-        assertEquals "foo", property.joinTable.name
-
-        mapping = builder.evaluate {
-            columns {
-                things joinTable:[name:'foo', key:'foo_id', column:'bar_id']
-            }
-        }
-
-        property = mapping.getPropertyConfig('things')
-        assert property?.joinTable
-        assertEquals "foo", property.joinTable.name
-        assertEquals "foo_id", property.joinTable.key.name
-        assertEquals "bar_id", property.joinTable.column.name
-    }
-
-    @Test
-    void testJoinTableMappingWithoutColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            things joinTable:true
-        }
-
-        assert mapping.getPropertyConfig('things')?.joinTable
-
-        mapping = builder.evaluate {
-            things joinTable:'foo'
-        }
-
-        PropertyConfig property = mapping.getPropertyConfig('things')
-        assert property?.joinTable
-        assertEquals "foo", property.joinTable.name
-
-        mapping = builder.evaluate {
-            things joinTable:[name:'foo', key:'foo_id', column:'bar_id']
-        }
-
-        property = mapping.getPropertyConfig('things')
-        assert property?.joinTable
-        assertEquals "foo", property.joinTable.name
-        assertEquals "foo_id", property.joinTable.key.name
-        assertEquals "bar_id", property.joinTable.column.name
-    }
-
-    @Test
-    void testCustomInheritanceStrategy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            tablePerHierarchy false
-        }
-
-        assertFalse mapping.tablePerHierarchy
-
-        mapping = builder.evaluate {
-            table 'myTable'
-            tablePerSubclass true
-        }
-
-        assertFalse mapping.tablePerHierarchy
-    }
-
-    @Test
-    void testTablePerConcreteClass() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            tablePerConcreteClass true
-        }
-
-        assertTrue mapping.tablePerConcreteClass
-        assertFalse mapping.tablePerHierarchy
-    }
-
-    @Test
-    void testAutoTimeStamp() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            autoTimestamp false
-        }
-
-        assertFalse mapping.autoTimestamp
-    }
-
-    @Test
-    void testCustomAssociationCachingConfig1() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            columns {
-                firstName cache:[usage:'read-only', include:'non-lazy']
-            }
-        }
-
-        def cc = mapping.getPropertyConfig('firstName')
-        assertEquals 'read-only', cc.cache.usage.toString()
-        assertEquals 'non-lazy', cc.cache.include.toString()
-    }
-
-    @Test
-    void testCustomAssociationCachingConfig1WithoutColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            firstName cache:[usage:'read-only', include:'non-lazy']
-        }
-
-        def cc = mapping.getPropertyConfig('firstName')
-        assertEquals 'read-only', cc.cache.usage.toString()
-        assertEquals 'non-lazy', cc.cache.include.toString()
-    }
-
-    @Test
-    void testCustomAssociationCachingConfig2() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-
-            columns {
-                firstName cache:'read-only'
-            }
-        }
-
-        def cc = mapping.getPropertyConfig('firstName')
-        assertEquals 'read-only', cc.cache.usage.toString()
-    }
-
-    @Test
-    void testCustomAssociationCachingConfig2WithoutColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            firstName cache:'read-only'
-        }
-
-        def cc = mapping.getPropertyConfig('firstName')
-        assertEquals 'read-only', cc.cache.usage.toString()
-    }
-
-    @Test
-    void testAssociationCachingConfig() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-
-            columns {
-                firstName cache:true
-            }
-        }
-
-        def cc = mapping.getPropertyConfig('firstName')
-        assertEquals 'read-write', cc.cache.usage.toString()
-        assertEquals 'all', cc.cache.include.toString()
-    }
-
-    @Test
-    void testAssociationCachingConfigWithoutColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            firstName cache:true
-        }
-
-        def cc = mapping.getPropertyConfig('firstName')
-        assertEquals 'read-write', cc.cache.usage.toString()
-        assertEquals 'all', cc.cache.include.toString()
-    }
-
-    @Test
-    void testEvaluateTableName() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-        }
-
-        assertEquals 'myTable', mapping.tableName
-    }
-
-    @Test
-    void testDefaultCacheStrategy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            cache true
-        }
-
-        assertEquals 'read-write', mapping.cache.usage.toString()
-        assertEquals 'all', mapping.cache.include.toString()
-    }
-
-    @Test
-    void testCustomCacheStrategy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            cache usage:'read-only', include:'non-lazy'
-        }
-
-        assertEquals 'read-only', mapping.cache.usage.toString()
-        assertEquals 'non-lazy', mapping.cache.include.toString()
-    }
-
-    @Test
-    void testCustomCacheStrategy2() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            cache 'read-only'
-        }
-
-        assertEquals 'read-only', mapping.cache.usage.toString()
-        assertEquals 'all', mapping.cache.include.toString()
-    }
-
-    @Test
-    void testInvalidCacheValues() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            cache usage:'rubbish', include:'more-rubbish'
-        }
-
-        // should be ignored and logged to console
-        assertEquals 'read-write', mapping.cache.usage.toString()
-        assertEquals 'all', mapping.cache.include.toString()
-    }
-
-    @Test
-    void testEvaluateVersioning() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-        }
-
-        assertEquals 'myTable', mapping.tableName
-        assertFalse mapping.versioned
-    }
-
-    @Test
-    void testIdentityColumnMapping() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            id column:'foo_id', type:Integer
-        }
-
-        assertEquals Long, mapping.identity.type
-        assertEquals 'foo_id', mapping.getPropertyConfig("id").column
-        assertEquals Integer, mapping.getPropertyConfig("id").type
-        assertEquals 'native', mapping.identity.generator
-    }
-
-    @Test
-    void testDefaultIdStrategy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-        }
-
-        assertEquals Long, mapping.identity.type
-        assertEquals 'id', mapping.identity.column
-        assertEquals 'native', mapping.identity.generator
-    }
-
-    @Test
-    void testHiloIdStrategy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            id generator:'hilo', 
params:[table:'hi_value',column:'next_value',max_lo:100]
-        }
-
-        assertEquals Long, mapping.identity.type
-        assertEquals 'id', mapping.identity.column
-        assertEquals 'hilo', mapping.identity.generator
-        assertEquals 'hi_value', mapping.identity.params.table
-    }
-
-    @Test
-    void testCompositeIdStrategy() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            id composite:['one','two'], compositeClass:HibernateMappingBuilder
-        }
-        def compositeId = mapping.identity
-
-        assert compositeId instanceof CompositeIdentity
-
-        assertEquals( "one",  compositeId.propertyNames[0])
-        assertEquals "two", compositeId.propertyNames[1]
-        assertEquals HibernateMappingBuilder, compositeId.compositeClass
-    }
-
-    @Test
-    void testSimpleColumnMappingsWithoutColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            firstName column:'First_Name'
-            lastName column:'Last_Name'
-        }
-
-        assertEquals "First_Name",mapping.getPropertyConfig('firstName').column
-        assertEquals "Last_Name",mapping.getPropertyConfig('lastName').column
-    }
-
-    @Test
-    void testSimpleColumnMappings() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            columns {
-                firstName column:'First_Name'
-                lastName column:'Last_Name'
-            }
-        }
-
-        assertEquals "First_Name",mapping.getPropertyConfig('firstName').column
-        assertEquals "Last_Name",mapping.getPropertyConfig('lastName').column
-    }
-
-    @Test
-    void testComplexColumnMappings() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            columns {
-                firstName  column:'First_Name',
-                        lazy:true,
-                        unique:true,
-                        type: Clob,
-                        length:255,
-                        index:'foo',
-                        sqlType: 'text'
-
-                lastName column:'Last_Name'
-            }
-        }
-
-        assertEquals "First_Name",mapping.columns.firstName.column
-        assertTrue mapping.columns.firstName.lazy
-        assertTrue mapping.columns.firstName.unique
-        assertEquals Clob,mapping.columns.firstName.type
-        assertEquals 255,mapping.columns.firstName.length
-        assertEquals 'foo',mapping.columns.firstName.getIndexName()
-        assertEquals "text",mapping.columns.firstName.sqlType
-        assertEquals "Last_Name",mapping.columns.lastName.column
-    }
-
-    @Test
-    void testComplexColumnMappingsWithoutColumnsBlock() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            table 'myTable'
-            version false
-            firstName  column:'First_Name',
-                    lazy:true,
-                    unique:true,
-                    type: Clob,
-                    length:255,
-                    index:'foo',
-                    sqlType: 'text'
-
-            lastName column:'Last_Name'
-        }
-
-        assertEquals "First_Name",mapping.columns.firstName.column
-        assertTrue mapping.columns.firstName.lazy
-        assertTrue mapping.columns.firstName.unique
-        assertEquals Clob,mapping.columns.firstName.type
-        assertEquals 255,mapping.columns.firstName.length
-        assertEquals 'foo',mapping.columns.firstName.getIndexName()
-        assertEquals "text",mapping.columns.firstName.sqlType
-        assertEquals "Last_Name",mapping.columns.lastName.column
-    }
-
-    @Test
-    void testPropertyWithMultipleColumns() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            amount type: MyUserType, {
-                column name: "value"
-                column name: "currency", sqlType: "char", length: 3
-            }
-        }
-
-        assertEquals 2, mapping.columns.amount.columns.size()
-        assertEquals "value", mapping.columns.amount.columns[0].name
-        assertEquals "currency", mapping.columns.amount.columns[1].name
-        assertEquals "char", mapping.columns.amount.columns[1].sqlType
-        assertEquals 3, mapping.columns.amount.columns[1].length
-
-        assertThrows Throwable, { mapping.columns.amount.column }
-        assertThrows Throwable, { mapping.columns.amount.sqlType }
-    }
-
-    @Test
-    void testConstrainedPropertyWithMultipleColumns() {
-        def builder = new HibernateMappingBuilder("Foo")
-        builder.evaluate {
-            amount type: MyUserType, {
-                column name: "value"
-                column name: "currency", sqlType: "char", length: 3
-            }
-        }
-        def mapping = builder.evaluate {
-            amount nullable: true
-        }
-
-        assertEquals 2, mapping.columns.amount.columns.size()
-        assertEquals "value", mapping.columns.amount.columns[0].name
-        assertEquals "currency", mapping.columns.amount.columns[1].name
-        assertEquals "char", mapping.columns.amount.columns[1].sqlType
-        assertEquals 3, mapping.columns.amount.columns[1].length
-
-        assertThrows Throwable, { mapping.columns.amount.column }
-        assertThrows Throwable, { mapping.columns.amount.sqlType }
-    }
-
-    @Test
-    void testDisallowedConstrainedPropertyWithMultipleColumns() {
-        def builder = new HibernateMappingBuilder("Foo")
-        builder.evaluate {
-            amount type: MyUserType, {
-                column name: "value"
-                column name: "currency", sqlType: "char", length: 3
-            }
-        }
-        assertThrows(Throwable, {
-            builder.evaluate {
-                amount scale: 2
-            }
-        }, "Cannot treat multi-column property as a single-column property")
-    }
-
-    @Test
-    void testPropertyWithUserTypeAndNoParams() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            amount type: MyUserType
-        }
-
-        assertEquals MyUserType, mapping.getPropertyConfig('amount').type
-        assertNull mapping.getPropertyConfig('amount').typeParams
-    }
-
-    @Test
-    void testPropertyWithUserTypeAndTypeParams() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            amount type: MyUserType, params : [ param1 : "amountParam1", 
param2 : 65 ]
-            value type: MyUserType, params : [ param1 : "valueParam1", param2 
: 21 ]
-        }
-
-        assertEquals MyUserType, mapping.getPropertyConfig('amount').type
-        assertEquals "amountParam1", 
mapping.getPropertyConfig('amount').typeParams.param1
-        assertEquals 65, mapping.getPropertyConfig('amount').typeParams.param2
-        assertEquals MyUserType, mapping.getPropertyConfig('value').type
-        assertEquals "valueParam1", 
mapping.getPropertyConfig('value').typeParams.param1
-        assertEquals 21, mapping.getPropertyConfig('value').typeParams.param2
-    }
-
-    @Test
-    void testInsertablePropertyConfig() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            firstName insertable:true
-            lastName insertable:false
-        }
-        assertTrue mapping.getPropertyConfig('firstName').insertable
-        assertFalse mapping.getPropertyConfig('lastName').insertable
-    }
-
-    @Test
-    void testUpdatablePropertyConfig() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            firstName updatable:true
-            lastName updatable:false
-        }
-        assertTrue mapping.getPropertyConfig('firstName').updatable
-        assertFalse mapping.getPropertyConfig('lastName').updatable
-    }
-
-    @Test
-    void testDefaultValue() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            comment 'wahoo'
-            name comment: 'bar'
-            foo defaultValue: '5'
-        }
-        assertEquals '5', 
mapping.getPropertyConfig('foo').columns[0].defaultValue
-        assertNull mapping.getPropertyConfig('name').columns[0].defaultValue
-    }
-
-    @Test
-    void testColumnComment() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            comment 'wahoo'
-            name comment: 'bar'
-            foo defaultValue: '5'
-        }
-        assertEquals 'bar', 
mapping.getPropertyConfig('name').columns[0].comment
-        assertNull mapping.getPropertyConfig('foo').columns[0].comment
-    }
-
-    @Test
-    void testTableComment() {
-        def builder = new HibernateMappingBuilder("Foo")
-        def mapping = builder.evaluate {
-            comment 'wahoo'
-            name comment: 'bar'
-            foo defaultValue: '5'
-        }
-        assertEquals 'wahoo', mapping.comment
-    }
-    // dummy user type
-    static class MyUserType {}
-}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
index 40d12e9519..95e239aefd 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
@@ -101,7 +101,7 @@ class HibernateGormDatastoreSpec extends 
GrailsDataTckSpec<GrailsDataHibernate7T
     GrailsHibernatePersistentEntity createPersistentEntity(Class clazz, 
GrailsDomainBinder binder) {
         def entity = getMappingContext().addPersistentEntity(clazz) as 
GrailsHibernatePersistentEntity
         if (entity != null) {
-            MappingCacheHolder.getInstance().cacheMapping(entity)
+            getMappingContext().getMappingCacheHolder().cacheMapping(entity)
         }
         entity
     }
@@ -166,6 +166,17 @@ class HibernateGormDatastoreSpec extends 
GrailsDataTckSpec<GrailsDataHibernate7T
         return  new HibernateQuery(session, getPersistentEntity(clazz))
     }
 
+    /**
+     * Triggers the first-pass Hibernate mapping for all registered entities.
+     * This initializes the Hibernate Collection, Table, and Column objects
+     * required for SecondPass binder tests.
+     */
+    protected void hibernateFirstPass() {
+        def gdb = getGrailsDomainBinder()
+        def collector = gdb.getMetadataBuildingContext().getMetadataCollector()
+        gdb.contribute(collector, getMappingContext())
+    }
+
     /**
      * Returns true when a Docker daemon is reachable on this machine.
      * <p>
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/ColumnConfigSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/ColumnConfigSpec.groovy
new file mode 100644
index 0000000000..0a543998f2
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/ColumnConfigSpec.groovy
@@ -0,0 +1,155 @@
+/*
+ *  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.cfg
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class ColumnConfigSpec extends Specification {
+
+    void "test default values"() {
+        when:
+        def config = new ColumnConfig()
+
+        then:
+        config.enumType == 'default'
+        config.unique == false
+        config.length == -1
+        config.precision == -1
+        config.scale == -1
+    }
+
+    void "test configureNew with closure"() {
+        when:
+        def config = ColumnConfig.configureNew {
+            name "my_column"
+            sqlType "varchar(255)"
+            index "my_index"
+            unique true
+            length 100
+            precision 10
+            scale 2
+            defaultValue "default_val"
+            comment "my comment"
+            read "read_sql"
+            write "write_sql"
+        }
+
+        then:
+        config.name == "my_column"
+        config.sqlType == "varchar(255)"
+        config.index == "my_index"
+        config.unique == true
+        config.length == 100
+        config.precision == 10
+        config.scale == 2
+        config.defaultValue == "default_val"
+        config.comment == "my comment"
+        config.read == "read_sql"
+        config.write == "write_sql"
+    }
+
+    void "test configureNew with map"() {
+        when:
+        def config = ColumnConfig.configureNew(
+            name: "my_column",
+            sqlType: "varchar(255)",
+            index: "my_index",
+            unique: true,
+            length: 100,
+            precision: 10,
+            scale: 2,
+            defaultValue: "default_val",
+            comment: "my comment",
+            read: "read_sql",
+            write: "write_sql"
+        )
+
+        then:
+        config.name == "my_column"
+        config.sqlType == "varchar(255)"
+        config.index == "my_index"
+        config.unique == true
+        config.length == 100
+        config.precision == 10
+        config.scale == 2
+        config.defaultValue == "default_val"
+        config.comment == "my comment"
+        config.read == "read_sql"
+        config.write == "write_sql"
+    }
+
+    @Unroll
+    void "test getIndexAsMap with valid input: #input"() {
+        given:
+        def config = new ColumnConfig(index: input)
+
+        expect:
+        config.getIndexAsMap() == expected
+
+        where:
+        input                                      | expected
+        null                                       | [:]
+        [:]                                        | [:]
+        [column: 'foo', type: 'string']            | [column: 'foo', type: 
'string']
+        "my_idx"                                   | [column: "my_idx"]
+        "invalid_format"                           | [column: "invalid_format"]
+        "[]"                                       | [:]
+        "  "                                       | [:]
+        "column:item_idx, type:integer"            | [column: "item_idx", 
type: "integer"]
+        "[column:item_idx, type:integer]"          | [column: "item_idx", 
type: "integer"]
+        "column:'item_idx', type:'integer'"        | [column: "item_idx", 
type: "integer"]
+        'column:"item_idx", type:"integer"'        | [column: "item_idx", 
type: "integer"]
+        "  column : item_idx ,  type : integer  "  | [column: "item_idx", 
type: "integer"]
+    }
+
+    @Unroll
+    void "test getIndexAsMap with invalid input: #input"() {
+        given:
+        def config = new ColumnConfig(index: input)
+
+        when:
+        config.getIndexAsMap()
+
+        then:
+        thrown(IllegalArgumentException)
+
+        where:
+        input << [
+            "column:foo, invalid",
+            "column:foo, invalid:bar, extra"
+        ]
+    }
+
+    void "test getIndexAsMap with non-string non-map input returns empty 
map"() {
+        given:
+        def config = new ColumnConfig(index: { "closure" })
+
+        expect:
+        config.getIndexAsMap() == [:]
+    }
+
+    void "test toString"() {
+        given:
+        def config = new ColumnConfig(name: "foo", index: "bar", unique: true, 
length: 10, precision: 5, scale: 2)
+
+        expect:
+        config.toString() == "column[name:foo, index:bar, unique:true, 
length:10, precision:5, scale:2]"
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegateSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegateSpec.groovy
new file mode 100644
index 0000000000..4192a8f910
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyDefinitionDelegateSpec.groovy
@@ -0,0 +1,61 @@
+/*
+ *  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.cfg
+
+import spock.lang.Specification
+
+class PropertyDefinitionDelegateSpec extends Specification {
+
+    def "test column method with multiple columns"() {
+        given:
+        def config = new PropertyConfig()
+        def delegate = new PropertyDefinitionDelegate(config)
+
+        when:
+        delegate.column(name: 'col1', sqlType: 'varchar(255)')
+        delegate.column(name: 'col2', sqlType: 'integer')
+
+        then:
+        config.columns.size() == 2
+        config.columns[0].name == 'col1'
+        config.columns[0].sqlType == 'varchar(255)'
+        config.columns[1].name == 'col2'
+        config.columns[1].sqlType == 'integer'
+    }
+
+    def "test re-evaluation of column method with multiple columns"() {
+        given:
+        def config = new PropertyConfig()
+        def delegate1 = new PropertyDefinitionDelegate(config)
+        delegate1.column(name: 'col1', sqlType: 'varchar(255)')
+        delegate1.column(name: 'col2', sqlType: 'integer')
+
+        when: "re-evaluating with a new delegate instance but same config"
+        def delegate2 = new PropertyDefinitionDelegate(config)
+        delegate2.column(name: 'new_col1', sqlType: 'text')
+        delegate2.column(name: 'new_col2', sqlType: 'long')
+
+        then:
+        config.columns.size() == 2
+        config.columns[0].name == 'new_col1'
+        config.columns[0].sqlType == 'text'
+        config.columns[1].name == 'new_col2'
+        config.columns[1].sqlType == 'long'
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy
index e322220694..abc8792b68 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CascadeBehaviorFetcherSpec.groovy
@@ -1,30 +1,27 @@
 /*
- *  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
+ * 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
+ * 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.
+ * 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.cfg.domainbinding
 
 import jakarta.persistence.Embeddable
-
 import org.hibernate.MappingException
 import spock.lang.Shared
 import spock.lang.Unroll
-
 import grails.gorm.annotation.Entity
 import grails.gorm.specs.HibernateGormDatastoreSpec
 import org.grails.orm.hibernate.cfg.domainbinding.util.CascadeBehaviorFetcher
@@ -42,8 +39,6 @@ import static 
org.grails.orm.hibernate.cfg.domainbinding.util.CascadeBehavior.SA
 
 class CascadeBehaviorFetcherSpec extends HibernateGormDatastoreSpec {
 
-
-
     // A single, comprehensive source of truth for all metadata test scenarios.
     private static final List cascadeMetadataTestData = [
             // --- UNIDIRECTIONAL hasMany (should be supported in Hibernate 
6+) ---
@@ -57,28 +52,25 @@ class CascadeBehaviorFetcherSpec extends 
HibernateGormDatastoreSpec {
             ["uni: explicit 'persist'", AW_Persist_Uni, "books", BookUni, 
PERSIST.getValue()],
             ["uni: invalid string", AW_Invalid_Uni, "books", BookUni, 
MappingException],
 
-
-
-
-
             // --- OTHER RELATIONSHIP TYPES ---
-            ["uni: string"                 , AW_Default_Uni    , "books", 
BookUni          , SAVE_UPDATE.getValue()],
-            ["uni: String"                 , AW_Default_String    , "books", 
String          , MappingException],
-            ["bi: default"                  , AW_Default_Bi     , "books", 
Book_BT_Default   , ALL.getValue()],
-            ["bi: hasOne (with belongsTo)"  , AW_HasOne_Bi      , "profile" , 
Profile_BT      , ALL.getValue()], // Conservative default
-            ["uni: hasOne (no belongsTo)"   , AW_HasOne_Uni     , "passport", 
Passport        , ALL.getValue()], // Should be supported
-            ["many-to-many (owning side)"   , Post              , "tags"    , 
Tag_BT          , SAVE_UPDATE.getValue()],
+            ["uni: string", AW_Default_Uni, "books", BookUni, 
SAVE_UPDATE.getValue()],
+            // FIX: This now expects ALL instead of MappingException to 
support Basic collections
+            ["uni: String collection", AW_Default_String, "books", String, 
ALL.getValue()],
+            ["bi: default", AW_Default_Bi, "books", Book_BT_Default, 
ALL.getValue()],
+            ["bi: hasOne (with belongsTo)", AW_HasOne_Bi, "profile", 
Profile_BT, ALL.getValue()],
+            ["uni: hasOne (no belongsTo)", AW_HasOne_Uni, "passport", 
Passport, ALL.getValue()],
+            ["many-to-many (owning side)", Post, "tags", Tag_BT, 
SAVE_UPDATE.getValue()],
             ["many-to-many (circular subclass)", Dog, "animals", Mammal, 
SAVE_UPDATE.getValue()],
-            ["many-to-many (inverse side)"  , Tag_BT            , "posts"   , 
Post            , NONE.getValue()],
+            ["many-to-many (inverse side)", Tag_BT, "posts", Post, 
NONE.getValue()],
             ["many-to-many (circular superclass)", Mammal, "dogs", Dog, 
NONE.getValue()],
-            ["many-to-one (belongsTo)"      , Book_BT_Default   , "author"  , 
AW_Default_Bi   , NONE.getValue()],
-            ["many-to-one (unidirectional)" , A                 , "manyToOne", 
ManyToOne      , SAVE_UPDATE.getValue()],
-            ["many-to-one (bidirectional but superclass)"      , Bird   , 
"canary"  , Canary   , NONE.getValue()],
+            ["many-to-one (belongsTo)", Book_BT_Default, "author", 
AW_Default_Bi, NONE.getValue()],
+            ["many-to-one (unidirectional)", A, "manyToOne", ManyToOne, 
SAVE_UPDATE.getValue()],
+            ["many-to-one (bidirectional but superclass)", Bird, "canary", 
Canary, NONE.getValue()],
 
-//             --- Additional Hibernate 6+ specific scenarios ---
+            // --- Additional Hibernate 6+ specific scenarios ---
             ["uni: hasMany with explicit none", AW_None_Uni, "books", BookUni, 
NONE.getValue()],
-            ["bi: hasOne default conservative", AW_HasOne_Default, "profile", 
Profile_Default , ALL.getValue()],
-            ["orphan removal scenario"        , AW_OrphanRemoval , "books", 
Book_Orphan      , ALL_DELETE_ORPHAN.getValue()],
+            ["bi: hasOne default conservative", AW_HasOne_Default, "profile", 
Profile_Default, ALL.getValue()],
+            ["orphan removal scenario", AW_OrphanRemoval, "books", 
Book_Orphan, ALL_DELETE_ORPHAN.getValue()],
 
             // --- Map Association Scenarios ---
             ["map with belongsTo", ImpliedMapParent_All, "settings", 
ImpliedMapChild_All, ALL.getValue()],
@@ -91,11 +83,7 @@ class CascadeBehaviorFetcherSpec extends 
HibernateGormDatastoreSpec {
             ["embedded association", EOwner, "address", EAddress, 
ALL.getValue()]
     ]
 
-
-    @Shared
-    CascadeBehaviorFetcher fetcher = new CascadeBehaviorFetcher()
-
-
+    @Shared CascadeBehaviorFetcher fetcher = new CascadeBehaviorFetcher()
 
     @Unroll
     void "test cascade behavior fetcher for #description"() {
@@ -107,7 +95,6 @@ class CascadeBehaviorFetcherSpec extends 
HibernateGormDatastoreSpec {
         when: "Getting the cascade behavior"
         def result = null
         def thrownException = null
-
         try {
             result = fetcher.getCascadeBehaviour(testProperty)
         } catch (Exception e) {
@@ -116,140 +103,191 @@ class CascadeBehaviorFetcherSpec extends 
HibernateGormDatastoreSpec {
 
         then: "The result matches the expectation"
         if (expectation instanceof Class && 
Exception.isAssignableFrom(expectation)) {
-            // Expecting an exception
-            if (thrownException == null) {
-                println "Error for description: '${description}'. Expected 
${expectation.simpleName} to be thrown but no exception was thrown."
-            } else if (!expectation.isAssignableFrom(thrownException.class)) {
-                println "Error for description: '${description}'. Expected 
${expectation.simpleName} but got ${thrownException.class.simpleName}."
-            }
             assert thrownException != null
             assert expectation.isAssignableFrom(thrownException.class)
         } else {
-            // Expecting a string result
-            if (thrownException != null) {
-                println "Error for description: '${description}'. Unexpected 
exception thrown: ${thrownException?.message}"
-                thrownException.printStackTrace()
-            }
             assert thrownException == null
-            if (result != expectation) {
-                println "Error for description: '${description}'. Expected 
cascade behavior '${expectation}' but got '${result}'."
-            }
             assert result == expectation
         }
 
         where:
         [description, ownerClass, associationName, childClass, expectation] << 
cascadeMetadataTestData
     }
+}
 
+// --- Test Domain Classes ---
 
+@Entity class BookUni { String title }
 
+@Entity
+class AW_All_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'all' }
+}
 
+@Entity
+class AW_SaveUpdate_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'persist,merge' }
 }
 
-// --- Test Domain Classes for Various Scenarios ---
-// Naming Convention:
-//   AW_ = AuthorWith...
-//   _Uni = Unidirectional hasMany (child has no belongsTo)
-//   _Bi = Bidirectional hasMany (child has a belongsTo)
-//   _BT = Suffix for a child class that has a `belongsTo`
+@Entity
+class AW_Merge_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'merge' }
+}
 
-// --- One-to-Many: Unidirectional ---
-@Entity class BookUni { String title }
+@Entity
+class AW_Delete_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'delete' }
+}
 
-@Entity class AW_All_Uni { static hasMany = [books: BookUni]; static mapping = 
{ books cascade: 'all' } }
-@Entity class AW_SaveUpdate_Uni { static hasMany = [books: BookUni]; static 
mapping = { books cascade: 'persist,merge' } }
-@Entity class AW_Merge_Uni { static hasMany = [books: BookUni]; static mapping 
= { books cascade: 'merge' } }
-@Entity class AW_Delete_Uni { static hasMany = [books: BookUni]; static 
mapping = { books cascade: 'delete' } }
-@Entity class AW_Lock_Uni { static hasMany = [books: BookUni]; static mapping 
= { books cascade: 'lock' } }
-@Entity class AW_Replicate_Uni { static hasMany = [books: BookUni]; static 
mapping = { books cascade: 'replicate' } }
-@Entity class AW_Evict_Uni { static hasMany = [books: BookUni]; static mapping 
= { books cascade: 'evict' } }
-@Entity class AW_Persist_Uni { static hasMany = [books: BookUni]; static 
mapping = { books cascade: 'persist' } }
-@Entity class AW_Invalid_Uni { static hasMany = [books: BookUni]; static 
mapping = { books cascade: 'invalid-string' } }
+@Entity
+class AW_Lock_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'lock' }
+}
 
-@Entity class AW_Default_Uni { static hasMany = [books: BookUni] }
-class Buffalo{}
-@Entity class AW_Default_String { String title; static hasMany = [books: 
Buffalo]}
-@Entity class Book_BT_Default { String title; static belongsTo = [author: 
AW_Default_Bi] }
-@Entity class AW_Default_Bi { static hasMany = [books: Book_BT_Default] }
+@Entity
+class AW_Replicate_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'replicate' }
+}
 
 @Entity
-class A {
-    ManyToOne manyToOne
+class AW_Evict_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'evict' }
 }
+
 @Entity
-class ManyToOne {
+class AW_Persist_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'persist' }
 }
 
+@Entity
+class AW_Invalid_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'invalid-string' }
+}
 
+@Entity class AW_Default_Uni { static hasMany = [books: BookUni] }
 
+// FIX: Replaced class Buffalo with String to test Basic collections properly
+@Entity
+class AW_Default_String {
+    String title
+    static hasMany = [books: String]
+}
 
+@Entity
+class Book_BT_Default {
+    String title
+    static belongsTo = [author: AW_Default_Bi]
+}
+
+@Entity class AW_Default_Bi { static hasMany = [books: Book_BT_Default] }
+
+@Entity class A { ManyToOne manyToOne }
+@Entity class ManyToOne { }
 
-// --- One-to-One ---
 @Entity class Passport { String passportNumber }
-@Entity class AW_HasOne_Uni { static hasOne = [passport: Passport] } // 
Unidirectional
+@Entity class AW_HasOne_Uni { static hasOne = [passport: Passport] }
 
-@Entity class Profile_BT { String bio; static belongsTo = [author: 
AW_HasOne_Bi] }
-@Entity class AW_HasOne_Bi { static hasOne = [profile: Profile_BT] } // 
Bidirectional
+@Entity
+class Profile_BT {
+    String bio
+    static belongsTo = [author: AW_HasOne_Bi]
+}
 
-// --- Many-to-Many ---
-@Entity class Post { String content; static hasMany = [tags: Tag_BT] }
-@Entity class Tag_BT { String name; static hasMany = [posts: Post]; static 
belongsTo = Post }
-@Entity class Mammal { String name;  static hasMany = [dogs: Dog]}
-@Entity class Dog extends Mammal { String foo; static hasMany = [animals: 
Mammal] }
+@Entity class AW_HasOne_Bi { static hasOne = [profile: Profile_BT] }
 
+@Entity
+class Post {
+    String content
+    static hasMany = [tags: Tag_BT]
+}
+
+@Entity
+class Tag_BT {
+    String name
+    static hasMany = [posts: Post]
+    static belongsTo = Post
+}
+
+@Entity class Mammal { String name; static hasMany = [dogs: Dog] }
+@Entity class Dog extends Mammal { String foo; static hasMany = [animals: 
Mammal] }
 
 @Entity class Bird { String title; static belongsTo = [canary: Canary] }
 @Entity class Canary { static hasMany = [birds: Bird] }
 
-@Entity class AW_None_Uni { static hasMany = [books: BookUni]; static mapping 
= { books cascade: 'none' } }
-@Entity class Profile_Default { String bio; static belongsTo = [author: 
AW_HasOne_Default] }
+@Entity
+class AW_None_Uni {
+    static hasMany = [books: BookUni]
+    static mapping = { books cascade: 'none' }
+}
+
+@Entity
+class Profile_Default {
+    String bio
+    static belongsTo = [author: AW_HasOne_Default]
+}
+
 @Entity class AW_HasOne_Default { static hasOne = [profile: Profile_Default] }
-@Entity class Book_Orphan { String title; static belongsTo = [author: 
AW_OrphanRemoval] }
-@Entity class AW_OrphanRemoval { static hasMany = [books: Book_Orphan]; static 
mapping = { books cascade: 'all-delete-orphan' } }
 
-// --- Map Association Scenarios ---
-@Entity class ImpliedMapParent_All {
+@Entity
+class Book_Orphan {
+    String title
+    static belongsTo = [author: AW_OrphanRemoval]
+}
+
+@Entity
+class AW_OrphanRemoval {
+    static hasMany = [books: Book_Orphan]
+    static mapping = { books cascade: 'all-delete-orphan' }
+}
+
+@Entity
+class ImpliedMapParent_All {
     static hasMany = [settings: ImpliedMapChild_All]
     Map<String, ImpliedMapChild_All> settings
 }
-@Entity class ImpliedMapChild_All {
+
+@Entity
+class ImpliedMapChild_All {
     String value
     static belongsTo = [parent: ImpliedMapParent_All]
 }
-@Entity class ImpliedMapParent_SaveUpdate {
+
+@Entity
+class ImpliedMapParent_SaveUpdate {
     static hasMany = [settings: ImpliedMapChild_SaveUpdate]
     Map<String, ImpliedMapChild_SaveUpdate> settings
 }
-@Entity class ImpliedMapChild_SaveUpdate { String value }
 
+@Entity class ImpliedMapChild_SaveUpdate { String value }
 
-// --- Composite ID Scenario ---
 @Entity
 class CompositeIdParent {
     Long id
     String name
     static hasMany = [children: CompositeIdManyToOne]
 }
+
 @Entity
 class CompositeIdManyToOne implements Serializable {
     String name
     CompositeIdParent parent
-
-    static mapping = {
-        id composite: ['name', 'parent']
-    }
-
+    static mapping = { id composite: ['name', 'parent'] }
     static belongsTo = [parent: CompositeIdParent]
 }
 
-// --- Embedded Association Scenario ---
 @Entity
 class EOwner {
     EAddress address
     static embedded = ['address']
 }
 
-@Embeddable
-class EAddress {
-    String street
-}
+@Embeddable class EAddress { String street }
\ No newline at end of file
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy
index 0d457d47fc..d26c61c0db 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnConfigToColumnBinderSpec.groovy
@@ -90,6 +90,54 @@ class ColumnConfigToColumnBinderSpec extends Specification {
         !column.unique
     }
 
+    def "column config honors uniqueness property when set to a string (named 
group)"() {
+        given:
+        def columnConfig = new ColumnConfig(unique: "group1")
+        PropertyConfig mappedForm = new PropertyConfig(unique: "group1")
+
+        when:
+        binder.bindColumnConfigToColumn(column, columnConfig, mappedForm)
+
+        then:
+        !column.unique // Should be false because it's handled via unique 
groups in Hibernate
+    }
+
+    def "column config honors uniqueness property when set to a list 
(composite groups)"() {
+        given:
+        def columnConfig = new ColumnConfig(unique: ["group1", "group2"])
+        PropertyConfig mappedForm = new PropertyConfig(unique: ["group1", 
"group2"])
+
+        when:
+        binder.bindColumnConfigToColumn(column, columnConfig, mappedForm)
+
+        then:
+        !column.unique
+    }
+
+    def "column config honors uniqueness property when set to boolean true"() {
+        given:
+        def columnConfig = new ColumnConfig(unique: true)
+        PropertyConfig mappedForm = new PropertyConfig(unique: true)
+
+        when:
+        binder.bindColumnConfigToColumn(column, columnConfig, mappedForm)
+
+        then:
+        column.unique
+    }
+
+    def "column config honors uniqueness property when set to boolean false"() 
{
+        given:
+        def columnConfig = new ColumnConfig(unique: false)
+        PropertyConfig mappedForm = new PropertyConfig(unique: false)
+
+        when:
+        binder.bindColumnConfigToColumn(column, columnConfig, mappedForm)
+
+        then:
+        !column.unique
+    }
+
     def "column config honors uniqueness property when mappedForm is empty"() {
         given:
         def columnConfig = new ColumnConfig()
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy
index aaca72d0d3..3fcdd2777d 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy
@@ -20,20 +20,19 @@ package org.grails.orm.hibernate.cfg.domainbinding.hibernate
 
 import grails.gorm.annotation.Entity
 import grails.gorm.specs.HibernateGormDatastoreSpec
-import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty
 
 class HibernateToManyPropertySpec extends HibernateGormDatastoreSpec {
 
-    def setupSpec() {
-        manager.addAllDomainClasses([HTMPBook, HTMPAuthor, HTMPAuthorCustom, 
HTMPStudent, HTMPCourse])
-    }
+    // Removed setupSpec to prevent loading all entities at once
 
     void "resolveJoinTableForeignKeyColumnName derives name from associated 
entity when no explicit config"() {
-        given:
-        def authorEntity = mappingContext.getPersistentEntity(HTMPAuthor.name)
-        HibernateToManyProperty property = (HibernateToManyProperty) 
authorEntity.getPropertyByName("books")
+        given: "Register only entities for this specific test"
+        def property = createTestHibernateToManyProperty(HTMPAuthor, "books")
         def namingStrategy = getGrailsDomainBinder().namingStrategy
 
+        and: "Trigger Hibernate First Pass"
+        hibernateFirstPass()
+
         when:
         String columnName = 
property.resolveJoinTableForeignKeyColumnName(namingStrategy)
 
@@ -42,11 +41,13 @@ class HibernateToManyPropertySpec extends 
HibernateGormDatastoreSpec {
     }
 
     void "resolveJoinTableForeignKeyColumnName uses explicit join table column 
name when configured"() {
-        given:
-        def authorEntity = 
mappingContext.getPersistentEntity(HTMPAuthorCustom.name)
-        HibernateToManyProperty property = (HibernateToManyProperty) 
authorEntity.getPropertyByName("books")
+        given: "Register only entities for this specific test"
+        def property = createTestHibernateToManyProperty(HTMPAuthorCustom, 
"books")
         def namingStrategy = getGrailsDomainBinder().namingStrategy
 
+        and: "Trigger Hibernate First Pass"
+        hibernateFirstPass()
+
         when:
         String columnName = 
property.resolveJoinTableForeignKeyColumnName(namingStrategy)
 
@@ -55,15 +56,73 @@ class HibernateToManyPropertySpec extends 
HibernateGormDatastoreSpec {
     }
 
     void "isAssociationColumnNullable returns false for ManyToMany"() {
+        given: "Register only entities for this specific test"
+        createPersistentEntity(HTMPCourse) // Course is needed because Student 
refers to it
+        def studentProp = createTestHibernateToManyProperty(HTMPStudent, 
"courses")
+
         when:
-        def studentEntity = 
mappingContext.getPersistentEntity(HTMPStudent.name)
-        def coursesProp = studentEntity.getPropertyByName("courses")
+        hibernateFirstPass()
 
         then:
-        !coursesProp.isAssociationColumnNullable()
+        !studentProp.isAssociationColumnNullable()
+    }
+
+    void "test index column configuration"() {
+        given: "Register the HTMPOrder entity using the helper"
+        def property = createTestHibernateToManyProperty(HTMPOrder, "items")
+        def namingStrategy = getGrailsDomainBinder().namingStrategy
+
+        and: "Trigger Hibernate First Pass"
+        hibernateFirstPass()
+
+        expect: "The index column name and type are resolved from the column 
list"
+        verifyAll(property) {
+            getIndexColumnName(namingStrategy) == "item_idx"
+            getIndexColumnType("integer") == "integer"
+        }
+    }
+
+    void "test index column configuration with map"() {
+        given:
+        def property = createTestHibernateToManyProperty(HTMPOrderMap, "items")
+        def namingStrategy = getGrailsDomainBinder().namingStrategy
+
+        and: "Trigger Hibernate First Pass"
+        hibernateFirstPass()
+
+        expect:
+        verifyAll(property) {
+            getIndexColumnName(namingStrategy) == "map_idx"
+            getIndexColumnType("integer") == "string"
+        }
+    }
+
+    void "test index column configuration with closure"() {
+        given:
+        def property = createTestHibernateToManyProperty(HTMPOrderClosure, 
"items")
+        def namingStrategy = getGrailsDomainBinder().namingStrategy
+
+        and: "Trigger Hibernate First Pass"
+        hibernateFirstPass()
+
+        expect:
+        verifyAll(property) {
+            getIndexColumnName(namingStrategy) == "closure_idx"
+            getIndexColumnType("integer") == "long"
+        }
+    }
+
+    /**
+     * Helper to register entity and return the property
+     */
+    protected HibernateToManyProperty 
createTestHibernateToManyProperty(Class<?> domainClass, String propertyName) {
+        def entity = createPersistentEntity(domainClass)
+        return (HibernateToManyProperty) entity.getPropertyByName(propertyName)
     }
 }
 
+// --- Supporting Entities ---
+
 @Entity
 class HTMPBook {
     Long id
@@ -74,7 +133,6 @@ class HTMPBook {
 class HTMPAuthor {
     Long id
     String name
-    Set<HTMPBook> books
     static hasMany = [books: HTMPBook]
 }
 
@@ -82,7 +140,6 @@ class HTMPAuthor {
 class HTMPAuthorCustom {
     Long id
     String name
-    Set<HTMPBook> books
     static hasMany = [books: HTMPBook]
     static mapping = {
         books joinTable: [column: 'custom_book_fk']
@@ -93,7 +150,6 @@ class HTMPAuthorCustom {
 class HTMPStudent {
     Long id
     String name
-    Set<HTMPCourse> courses
     static hasMany = [courses: HTMPCourse]
 }
 
@@ -101,6 +157,47 @@ class HTMPStudent {
 class HTMPCourse {
     Long id
     String title
-    Set<HTMPStudent> students
     static hasMany = [students: HTMPStudent]
 }
+
+import grails.persistence.Entity
+
+@Entity // Only if outside grails-app/domain
+class HTMPOrder {
+    Long id
+
+    List<String> items // Remove the = []
+
+    static hasMany = [items: String]
+
+    static mapping = {
+        items joinTable: [
+                name: "htmp_order_items",
+                key: "order_id",
+                column: "item_value"
+        ], index: "item_idx" // Defines the column for the List index
+    }
+}
+
+@Entity
+class HTMPOrderMap {
+    Long id
+    List<String> items
+    static hasMany = [items: String]
+    static mapping = {
+        items index: [column: 'map_idx', type: 'string']
+    }
+}
+
+@Entity
+class HTMPOrderClosure {
+    Long id
+    List<String> items
+    static hasMany = [items: String]
+    static mapping = {
+        items index: {
+            column name: 'closure_idx'
+            type 'long'
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy
index 6861fe269e..328eff9f83 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionSecondPassBinderSpec.groovy
@@ -19,6 +19,7 @@
 
 package org.grails.orm.hibernate.cfg.domainbinding.secondpass
 
+
 import grails.gorm.annotation.Entity
 import grails.gorm.specs.HibernateGormDatastoreSpec
 import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty
@@ -69,16 +70,43 @@ class CollectionSecondPassBinderSpec extends 
HibernateGormDatastoreSpec {
         return property
     }
 
-    def "resolveAssociatedClass throws MappingException when property has no 
associated entity"() {
-        given:
-        def property = 
createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as 
HibernateToManyProperty
-        def spiedProperty = Spy(property)
-        spiedProperty.getHibernateAssociatedEntity() >> null
+    def "bindCollectionSecondPass succeeds for Basic String collection"() {
+        given: "An entity with a basic String collection"
+        def property = createTestHibernateToManyProperty(HTMPOrder, "items") 
as HibernateToManyProperty
 
-        when:
-        binder.resolveAssociatedClass(spiedProperty, [:])
+        and: "We trigger the first pass mapping"
+        hibernateFirstPass()
+
+        expect: "The Hibernate collection object is now initialized"
+        property.getCollection() != null
+
+        when: "Binding second pass"
+        binder.bindCollectionSecondPass(property, [:])
 
         then:
+        noExceptionThrown()
+    }
+
+    def "resolveAssociatedClass throws MappingException when entity 
association is missing from persistentClasses"() {
+        given: "A standard entity association (not a Basic collection)"
+        def property = 
createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as 
HibernateToManyProperty
+
+        when: "Attempting to resolve associated class with an empty map"
+        binder.resolveAssociatedClass(property, [:])
+
+        then: "A MappingException is thrown because this is a real entity 
relationship"
+        def ex = thrown(org.hibernate.MappingException)
+        ex.message.contains("items")
+    }
+
+    def "resolveAssociatedClass throws MappingException when entity 
association has no class in persistentClasses"() {
+        given: "An entity association (not a Basic collection)"
+        def property = 
createTestHibernateToManyProperty(CSPBTestEntityWithMany, "items") as 
HibernateToManyProperty
+
+        when: "Attempting to resolve associated class with an empty map"
+        binder.resolveAssociatedClass(property, [:])
+
+        then: "A MappingException is thrown because this is a real entity 
association"
         def ex = thrown(org.hibernate.MappingException)
         ex.message.contains("items")
     }
@@ -124,3 +152,9 @@ class CSPBAssociatedItem {
     CSPBTestEntityWithMany parent
     static belongsTo = [parent: CSPBTestEntityWithMany]
 }
+@Entity
+class HTMPOrder {
+    Long id
+    List<String> items = []
+    static hasMany = [items: String]
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy
index b912873e8a..1e1780dd73 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy
@@ -51,8 +51,8 @@ import org.hibernate.mapping.BasicValue
 import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover
 import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher
 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.PropertyFromValueCreator
+import 
org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder
 import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder
 import org.grails.orm.hibernate.cfg.domainbinding.binder.SubClassBinder
 import org.grails.orm.hibernate.cfg.domainbinding.binder.SubclassMappingBinder
@@ -174,8 +174,8 @@ class MapSecondPassBinderSpec extends 
HibernateGormDatastoreSpec {
             org.apache.grails.data.testing.tck.domains.Pet,
             org.apache.grails.data.testing.tck.domains.Person,
             org.apache.grails.data.testing.tck.domains.PetType,
-            MSBAuthor,
-            MSBBook
+            MapSPBAuthor,
+            MapSPBBook
         ])
     }
 
@@ -189,22 +189,22 @@ class MapSecondPassBinderSpec extends 
HibernateGormDatastoreSpec {
         def collectionBinder = binders.collectionBinder
         def mapBinder = collectionBinder.mapSecondPassBinder
 
-        def authorEntity = getPersistentEntity(MSBAuthor) as 
GrailsHibernatePersistentEntity
-        def bookEntity = getPersistentEntity(MSBBook) as 
GrailsHibernatePersistentEntity
+        def authorEntity = getPersistentEntity(MapSPBAuthor) as 
GrailsHibernatePersistentEntity
+        def bookEntity = getPersistentEntity(MapSPBBook) as 
GrailsHibernatePersistentEntity
         def booksProp = authorEntity.getPropertyByName("books") as 
HibernateToManyProperty
 
         def rootClass = new RootClass(metadataBuildingContext)
         rootClass.setEntityName(authorEntity.name)
         rootClass.setClassName(authorEntity.name)
         rootClass.setJpaEntityName(authorEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "MSB_AUTHOR", null, 
false, metadataBuildingContext))
+        rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", 
null, false, metadataBuildingContext))
         collector.addEntityBinding(rootClass)
 
         def bookRootClass = new RootClass(metadataBuildingContext)
         bookRootClass.setEntityName(bookEntity.name)
         bookRootClass.setClassName(bookEntity.name)
         bookRootClass.setJpaEntityName(bookEntity.name)
-        bookRootClass.setTable(collector.addTable(null, null, "MSB_BOOK", 
null, false, metadataBuildingContext))
+        bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", 
null, false, metadataBuildingContext))
         collector.addEntityBinding(bookRootClass)
 
         def persistentClasses = [
@@ -239,27 +239,27 @@ class MapSecondPassBinderSpec extends 
HibernateGormDatastoreSpec {
         def collectionBinder = binders.collectionBinder
         def mapBinder = collectionBinder.mapSecondPassBinder
 
-        def authorEntity = getPersistentEntity(MSBAuthor) as 
GrailsHibernatePersistentEntity
-        def bookEntity = getPersistentEntity(MSBBook) as 
GrailsHibernatePersistentEntity
+        def authorEntity = getPersistentEntity(MapSPBAuthor) as 
GrailsHibernatePersistentEntity
+        def bookEntity = getPersistentEntity(MapSPBBook) as 
GrailsHibernatePersistentEntity
         def booksProp = authorEntity.getPropertyByName("books") as 
HibernateToManyProperty
 
         def rootClass = new RootClass(metadataBuildingContext)
         rootClass.setEntityName(authorEntity.name)
         rootClass.setClassName(authorEntity.name)
         rootClass.setJpaEntityName(authorEntity.name)
-        rootClass.setTable(collector.addTable(null, null, "MSB_AUTHOR", null, 
false, metadataBuildingContext))
+        rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", 
null, false, metadataBuildingContext))
         collector.addEntityBinding(rootClass)
 
         def bookRootClass = new RootClass(metadataBuildingContext)
         bookRootClass.setEntityName(bookEntity.name)
         bookRootClass.setClassName(bookEntity.name)
         bookRootClass.setJpaEntityName(bookEntity.name)
-        bookRootClass.setTable(collector.addTable(null, null, "MSB_BOOK", 
null, false, metadataBuildingContext))
+        bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", 
null, false, metadataBuildingContext))
         collector.addEntityBinding(bookRootClass)
 
         def persistentClasses = [
             (authorEntity.name): rootClass,
-            (MSBBook.name): bookRootClass
+            (MapSPBBook.name): bookRootClass
         ]
 
         def map = new org.hibernate.mapping.Map(metadataBuildingContext, 
rootClass)
@@ -267,7 +267,7 @@ class MapSecondPassBinderSpec extends 
HibernateGormDatastoreSpec {
         map.setCollectionTable(rootClass.getTable())
         
         def element = new 
org.hibernate.mapping.ManyToOne(metadataBuildingContext, 
map.getCollectionTable())
-        element.setReferencedEntityName(MSBBook.name)
+        element.setReferencedEntityName(MapSPBBook.name)
         map.setElement(element)
 
         booksProp.setCollection(map)
@@ -284,17 +284,19 @@ class MapSecondPassBinderSpec extends 
HibernateGormDatastoreSpec {
 }
 
 @grails.gorm.annotation.Entity
-class MSBAuthor {
+class MapSPBAuthor {
     Long id
-    Map<String, MSBBook> books
-    static hasMany = [books: MSBBook]
+    Map<String, MapSPBBook> books
+    static hasMany = [books: MapSPBBook]
     static mapping = {
-        books index: 'BOOK_TITLE'
+        books index: {
+            column 'books_idx'
+        }
     }
 }
 
 @grails.gorm.annotation.Entity
-class MSBBook {
+class MapSPBBook {
     Long id
     String title
 }

Reply via email to