This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch merge-hibernate6
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 9e326ae8b9f1082f4be97edca18fca7b4d2cde04
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sun Jul 13 21:22:06 2025 -0500

    refactor UniqueNamesGenerator
---
 .../orm/hibernate/cfg/GrailsDomainBinder.java      |  38 +-----
 .../cfg/domainbinding/UniqueNameGenerator.java     |  44 +++++++
 .../domainbinding/UniqueNameGeneratorSpec.groovy   | 139 +++++++++++++++++++++
 3 files changed, 186 insertions(+), 35 deletions(-)

diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
index a4fcc87818..7ab62ed2c9 100644
--- 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java
@@ -42,6 +42,7 @@ import 
org.grails.orm.hibernate.cfg.domainbinding.NumericColumnConstraintsBinder
 import org.grails.orm.hibernate.cfg.domainbinding.SimpleValueBinder;
 import 
org.grails.orm.hibernate.cfg.domainbinding.StringColumnConstraintsBinder;
 import org.grails.orm.hibernate.cfg.domainbinding.TypeNameProvider;
+import org.grails.orm.hibernate.cfg.domainbinding.UniqueNameGenerator;
 import org.hibernate.FetchMode;
 import org.hibernate.MappingException;
 import org.hibernate.boot.internal.MetadataBuildingContextRootImpl;
@@ -97,11 +98,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.util.StringUtils;
 
-import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Method;
-import java.math.BigInteger;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -1988,40 +1985,11 @@ public class GrailsDomainBinder implements 
MetadataContributor {
             uk.addColumns(property.getValue());
         }
 
-        setGeneratedUniqueName(uk);
+        new UniqueNameGenerator().setGeneratedUniqueName(uk);
 
         table.addUniqueKey(uk);
     }
 
-    private void setGeneratedUniqueName(UniqueKey uk) {
-        StringBuilder sb = new 
StringBuilder(uk.getTable().getName()).append('_');
-        for (Object col : uk.getColumns()) {
-            sb.append(((Column) col).getName()).append('_');
-        }
-
-        MessageDigest md;
-        try {
-            md = MessageDigest.getInstance("MD5");
-        }
-        catch (NoSuchAlgorithmException e) {
-            throw new RuntimeException(e);
-        }
-        try {
-            md.update(sb.toString().getBytes("UTF-8"));
-        }
-        catch (UnsupportedEncodingException e) {
-            throw new RuntimeException(e);
-        }
-
-        String name = "UK" + new BigInteger(1, md.digest()).toString(16);
-        if (name.length() > 30) {
-            // Oracle has a 30-char limit
-            name = name.substring(0, 30);
-        }
-
-        uk.setName(name);
-    }
-
     private boolean canBindOneToOneWithSingleColumnAndForeignKey(Association 
currentGrailsProp) {
         if (currentGrailsProp.isBidirectional()) {
             final Association otherSide = currentGrailsProp.getInverseSide();
@@ -2989,7 +2957,7 @@ public class GrailsDomainBinder implements 
MetadataContributor {
         if(LOG.isDebugEnabled()) {
             LOG.debug("create unique key for " + table.getName() + " columns = 
" + columns);
         }
-        setGeneratedUniqueName(uk);
+        new UniqueNameGenerator().setGeneratedUniqueName(uk);
         table.addUniqueKey(uk);
     }
 
diff --git 
a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGenerator.java
 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGenerator.java
new file mode 100644
index 0000000000..ddab2c679c
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGenerator.java
@@ -0,0 +1,44 @@
+package org.grails.orm.hibernate.cfg.domainbinding;
+
+import io.micrometer.common.util.StringUtils;
+import jakarta.validation.constraints.NotNull;
+import org.hibernate.MappingException;
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.UniqueKey;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UniqueNameGenerator {
+
+    public void setGeneratedUniqueName(@NotNull  UniqueKey uk) {
+        if(uk.getTable() == null) {
+            throw new MappingException(String.format("Unique Key %s does not 
have a table associated with it", uk.getName()));
+        }
+
+        try {
+            var fields = new ArrayList<>(List.of(uk.getTable().getName()));
+            uk.getColumns()
+                    .stream()
+                    .map(Column::getName)
+                    .filter(StringUtils::isNotBlank)
+                    .forEach(fields::add);
+            var ukString = String.join("_", fields);
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            md.update(ukString.getBytes(StandardCharsets.UTF_8));
+            String name = "UK" + new BigInteger(1, md.digest()).toString(16);
+            if (name.length() > 30) {
+                name = name.substring(0, 30);
+            }
+            uk.setName(name);
+        }
+        catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+}
diff --git 
a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGeneratorSpec.groovy
 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGeneratorSpec.groovy
new file mode 100644
index 0000000000..45ba1572fa
--- /dev/null
+++ 
b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGeneratorSpec.groovy
@@ -0,0 +1,139 @@
+package org.grails.orm.hibernate.cfg.domainbinding
+
+import org.hibernate.MappingException
+import org.hibernate.mapping.Column
+import org.hibernate.mapping.Table
+import org.hibernate.mapping.UniqueKey
+import spock.lang.Specification
+import spock.lang.Subject
+import spock.lang.Unroll
+
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+
+class UniqueNameGeneratorSpec extends Specification {
+
+    @Subject
+    UniqueNameGenerator generator = new UniqueNameGenerator()
+
+    @Unroll
+    def "should generate a unique name based on table and column names and 
truncate it"() {
+        given: "A unique key with a table and several columns"
+        def table = Mock(Table)
+        def column1 = Mock(Column)
+        def column2 = Mock(Column)
+        def uniqueKey = Mock(UniqueKey)
+
+        table.getName() >> "person"
+        column1.getName() >> "first_name"
+        column2.getName() >> "last_name"
+
+        uniqueKey.getTable() >> table
+        uniqueKey.getColumns() >> [column1, column2]
+
+        def expectedName = generateExpectedName("person", "first_name", 
"last_name")
+
+        when: "the unique name is generated"
+        generator.setGeneratedUniqueName(uniqueKey)
+
+        then: "the name is correctly calculated, prefixed, and truncated"
+        1 * uniqueKey.setName(expectedName)
+    }
+
+    @Unroll
+    def "should not truncate a generated name that is 30 characters or less"() 
{
+        given: "A unique key whose hash results in a short name"
+        def table = Mock(Table)
+        def column = Mock(Column)
+        def uniqueKey = Mock(UniqueKey)
+
+        table.getName() >> "short_table"
+        column.getName() >> "short_col"
+        uniqueKey.getTable() >> table
+        uniqueKey.getColumns() >> [column]
+
+        def expectedName = generateExpectedName("short_table", "short_col")
+
+        when: "the unique name is generated"
+        generator.setGeneratedUniqueName(uniqueKey)
+
+        then: "the name is not truncated because its length is not greater 
than 30"
+        1 * uniqueKey.setName(expectedName)
+    }
+
+    @Unroll
+    def "should throw MappingException if the unique key has no associated 
table"() {
+        given: "A unique key without a table"
+        def uniqueKey = Mock(UniqueKey)
+        uniqueKey.getTable() >> null
+        uniqueKey.getName() >> "my_uk" // For the exception message
+
+        when: "an attempt is made to generate the name"
+        generator.setGeneratedUniqueName(uniqueKey)
+
+        then: "a MappingException is thrown with a descriptive message"
+        def e = thrown(MappingException)
+        e.message == "Unique Key my_uk does not have a table associated with 
it"
+    }
+
+    @Unroll
+    def "should generate a name based only on the table if no columns are 
present"() {
+        given: "A unique key with a table but no columns"
+        def table = Mock(Table)
+        def uniqueKey = Mock(UniqueKey)
+
+        table.getName() >> "audit_log"
+        uniqueKey.getTable() >> table
+        uniqueKey.getColumns() >> []
+
+        def expectedName = generateExpectedName("audit_log")
+
+        when: "the unique name is generated"
+        generator.setGeneratedUniqueName(uniqueKey)
+
+        then: "the name is generated correctly using only the table name"
+        1 * uniqueKey.setName(expectedName)
+    }
+
+    @Unroll
+    def "should filter out columns with blank or null names"() {
+        given: "A unique key with valid, blank, and null column names"
+        def table = Mock(Table)
+        def column1 = Mock(Column)
+        def column2 = Mock(Column)
+        def column3 = Mock(Column)
+        def uniqueKey = Mock(UniqueKey)
+
+        table.getName() >> "product"
+        column1.getName() >> "sku"
+        column2.getName() >> ""      // Blank name
+        column3.getName() >> null    // Null name
+
+        uniqueKey.getTable() >> table
+        uniqueKey.getColumns() >> [column1, column2, column3]
+
+        // Only valid names should be part of the hash
+        def expectedName = generateExpectedName("product", "sku")
+
+        when: "the unique name is generated"
+        generator.setGeneratedUniqueName(uniqueKey)
+
+        then: "the blank and null column names are ignored in the calculation"
+        1 * uniqueKey.setName(expectedName)
+    }
+
+    /**
+     * Helper method that mirrors the core logic of UniqueNameGenerator to 
create
+     * a verifiable expected result without using hardcoded "magic" strings.
+     */
+    private String generateExpectedName(String... fields) {
+        def ukString = fields.join('_')
+        MessageDigest md = MessageDigest.getInstance("MD5")
+        md.update(ukString.getBytes(StandardCharsets.UTF_8))
+        String name = "UK" + new BigInteger(1, md.digest()).toString(16)
+        if (name.length() > 30) {
+            name = name.substring(0, 30)
+        }
+        return name
+    }
+}
\ No newline at end of file

Reply via email to