You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/08/16 15:51:04 UTC

(grails-core) branch 8.0.x updated: fix(grails-data-hibernate7): make type: 'text' produce an unbounded column (#16020)

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

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


The following commit(s) were added to refs/heads/8.0.x by this push:
     new 54ea20836a fix(grails-data-hibernate7): make type: 'text' produce an unbounded column (#16020)
54ea20836a is described below

commit 54ea20836a9725b4bcc76acc07987e27a7bc89a2
Author: Walter B Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Aug 16 10:50:59 2026 -0500

    fix(grails-data-hibernate7): make type: 'text' produce an unbounded column (#16020)
    
    * fix(grails-data-hibernate7): make type: 'text' produce an unbounded column
    
    property type: 'text' resolved through Hibernate's legacy named-type
    lookup to StandardBasicTypes.TEXT, whose JDBC type code is the legacy
    java.sql.Types.LONGVARCHAR. Dialects (e.g. Postgres) don't render that
    legacy code as their native unbounded text/CLOB type, falling back to a
    bounded VARCHAR at Hibernate's generic Length.LONG default (32600) once
    no explicit column length is set. On schema update, altering an existing
    column down to that bound fails once any row already holds more text.
    
    Bind the modern SqlTypes.LONG32VARCHAR JDBC type directly for this case
    instead of going through the ambiguous legacy type name, restoring the
    "CLOB or TEXT depending on dialect" behavior the mapping DSL docs already
    promise for type: 'text'.
    
    Fixes #16010
    
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
    
    * fix(grails-data-hibernate7): resolve type 'text' length via dialect-neutral Length.LONG32
    
    Address PR review feedback on the type: 'text' unbounded-column fix: instead of
    overriding the JDBC type descriptor with SqlTypes.LONG32VARCHAR, set the column's
    length to Hibernate 6+'s documented Length.LONG32 sentinel and let each dialect's
    own capacity-dependent DDL type registry resolve the native unbounded type (text,
    longtext, CLOB). This composes correctly with maxSize/inList/explicit column
    length instead of racing them, and keeps SimpleValueBinder as a pure orchestrator -
    the length decision now lives in StringColumnConstraintsBinder, which already owns
    string column length for maxSize/inList.
    
    Extends test coverage to close the "Postgres-only" gap: adds an H2-based spec that
    runs without Docker so container-less CI still exercises this path, and extends the
    Testcontainers spec to MySQL and MariaDB (Oracle excluded, matching the flaky-in-CI
    precedent already established in RLikeHibernate7Spec). Reverting the fix locally
    confirmed MySQL/MariaDB were independently affected (TEXT capped at 65535), not
    just Postgres.
    
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
    
    * docs: clarify type: 'text' resolves to the dialect's own unbounded column
    
    Addresses review feedback on #16020 asking to document that GORM
    computes the concrete SQL type (text/longtext/CLOB) per dialect rather
    than emitting a literal "text" type, and that every Hibernate-shipped
    Dialect defines this mapping.
    
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <no...@anthropic.com>
---
 .../cfg/domainbinding/binder/ColumnBinder.java     |   6 +-
 .../domainbinding/binder/SimpleValueBinder.java    |   5 +-
 .../binder/StringColumnConstraintsBinder.java      |  20 +++-
 .../GormTextTypeColumnIntegrationSpec.groovy       | 105 +++++++++++++++++++++
 .../hibernate/GormTextTypeColumnLengthSpec.groovy  |  99 +++++++++++++++++++
 .../cfg/domainbinding/ColumnBinderSpec.groovy      |  34 +++----
 .../cfg/domainbinding/SimpleValueBinderSpec.groovy |  64 +++++++++++++
 .../StringColumnConstraintsBinderSpec.groovy       |  78 +++++++++++++--
 grails-doc/src/en/ref/Database Mapping/type.adoc   |   4 +-
 9 files changed, 384 insertions(+), 31 deletions(-)

diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnBinder.java
index a4e6f1ffb0..2c93ddb616 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnBinder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ColumnBinder.java
@@ -84,6 +84,7 @@ public class ColumnBinder {
      * @param column The column to bind
      * @param path the path
      * @param table The table name
+     * @param typeName the property's resolved Hibernate type name
      */
     public void bindColumn(
             HibernatePersistentProperty property,
@@ -91,7 +92,8 @@ public class ColumnBinder {
             Column column,
             ColumnConfig cc,
             String path,
-            Table table) {
+            Table table,
+            String typeName) {
 
         if (cc != null) {
             column.setComment(cc.getComment());
@@ -116,7 +118,7 @@ public class ColumnBinder {
             Class<?> type = property.getType();
             if (type != null && (String.class.isAssignableFrom(type) || byte[].class.isAssignableFrom(type))) {
                 PropertyConfig mappedForm = property.getHibernateMappedForm();
-                stringColumnConstraintsBinder.bindStringColumnConstraints(column, mappedForm);
+                stringColumnConstraintsBinder.bindStringColumnConstraints(column, mappedForm, typeName);
             } else if (type != null && Number.class.isAssignableFrom(type)) {
                 PropertyConfig mappedForm = property.getHibernateMappedForm();
                 numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, mappedForm, type);
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleValueBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleValueBinder.java
index 13f53fb859..576f2f874e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleValueBinder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleValueBinder.java
@@ -84,7 +84,8 @@ public class SimpleValueBinder {
             String path) {
 
         PropertyConfig propertyConfig = property.getHibernateMappedForm();
-        simpleValue.setTypeName(property.getTypeName(simpleValue));
+        String typeName = property.getTypeName(simpleValue);
+        simpleValue.setTypeName(typeName);
         simpleValue.setTypeParameters(property.getTypeParameters(simpleValue));
 
         if (propertyConfig.isDerived() && !(property instanceof TenantId)) {
@@ -100,7 +101,7 @@ public class SimpleValueBinder {
                     .forEach(cc -> {
                         Column column = new Column();
                         columnConfigToColumnBinder.bindColumnConfigToColumn(column, cc, propertyConfig);
-                        columnBinder.bindColumn(property, parentProperty, column, cc, path, table);
+                        columnBinder.bindColumn(property, parentProperty, column, cc, path, table, typeName);
                         if (simpleValue instanceof DependantValue) {
                             column.setNullable(true);
                         }
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/StringColumnConstraintsBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/StringColumnConstraintsBinder.java
index 6e8d3e1e50..00c8d4a214 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/StringColumnConstraintsBinder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/StringColumnConstraintsBinder.java
@@ -21,21 +21,39 @@ package org.grails.orm.hibernate.cfg.domainbinding.binder;
 import java.util.Objects;
 import java.util.Optional;
 
+import org.hibernate.Length;
 import org.hibernate.mapping.Column;
 
 import org.grails.datastore.mapping.config.Property;
 
 public class StringColumnConstraintsBinder {
 
-    public void bindStringColumnConstraints(Column column, Property mappedForm) {
+    /**
+     * Binds a String/byte[] column's length from the property's {@code maxSize}/{@code inList}
+     * constraints. When neither is present and the resolved Hibernate type name is {@code text},
+     * the column is left unbounded via Hibernate's capacity-dependent DDL type mechanism -
+     * {@code Length.LONG32} is the documented way to obtain each dialect's native unbounded string
+     * type (text/longtext/varchar(max)/clob) instead of a bounded VARCHAR - see GH-16010.
+     *
+     * @param column the column to bind the length onto
+     * @param mappedForm the property's constraints (maxSize/inList)
+     * @param typeName the resolved Hibernate type name, or {@code null} if not relevant
+     */
+    public void bindStringColumnConstraints(Column column, Property mappedForm, String typeName) {
         Integer number = Optional.ofNullable(mappedForm.getMaxSize())
                 .map(Number::intValue)
                 .orElse(getMax(mappedForm).orElse(0));
         if (number > 0) {
             column.setLength(number);
+        } else if (isUnboundedTextType(typeName)) {
+            column.setLength(Length.LONG32);
         }
     }
 
+    private static boolean isUnboundedTextType(String typeName) {
+        return "text".equalsIgnoreCase(typeName);
+    }
+
     private Optional<Integer> getMax(Property mappedForm) {
         return Optional.ofNullable(mappedForm.getInList()).flatMap(list -> list.stream()
                 .map(this::parseInt)
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnIntegrationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnIntegrationSpec.groovy
new file mode 100644
index 0000000000..7dc24346a6
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnIntegrationSpec.groovy
@@ -0,0 +1,105 @@
+/*
+ *  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
+
+import grails.gorm.annotation.Entity
+import grails.gorm.hibernate.HibernateEntity
+import grails.gorm.tests.HibernateGormDatastoreSpec
+import org.testcontainers.mariadb.MariaDBContainer
+import org.testcontainers.mysql.MySQLContainer
+import org.testcontainers.postgresql.PostgreSQLContainer
+import org.testcontainers.spock.Testcontainers
+import spock.lang.Requires
+import spock.lang.Shared
+
+/**
+ * Reproduces https://github.com/apache/grails-core/issues/16010 across every externally-run
+ * dialect this module tests against (see {@link grails.gorm.tests.RLikeHibernate7Spec} for the
+ * same H2/Postgres/MySQL/MariaDB precedent): a property mapped with {@code type: 'text'} must
+ * produce a genuinely unbounded column, not a bounded {@code varchar(n)}/{@code character
+ * varying(n)} that can fail to accommodate existing data on schema update. Oracle is
+ * intentionally excluded - its Testcontainers image is too flaky in CI to gate this spec on.
+ * H2 coverage lives separately in {@link GormTextTypeColumnLengthSpec}, which needs no container
+ * and so still runs when Docker (and therefore this whole spec) is unavailable.
+ */
+@Testcontainers
+@Requires({ isDockerAvailable() })
+class GormTextTypeColumnIntegrationSpec extends HibernateGormDatastoreSpec {
+
+    @Shared PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16")
+    @Shared MySQLContainer mysql = new MySQLContainer("mysql:8.0")
+    @Shared MariaDBContainer mariadb = new MariaDBContainer("mariadb:10.11")
+
+    void setupSpec() {
+        manager.registerDomainClasses(TextTypeMessage)
+    }
+
+    void "a property mapped with type 'text' produces an unbounded column on #db"() {
+        given:
+        if (!container.isRunning()) {
+            container.start()
+        }
+        // Ensure a completely fresh datastore per dialect, as in RLikeHibernate7Spec.
+        manager.destroy()
+        manager.grailsConfig = [
+            'dataSource.url'             : container.jdbcUrl,
+            'dataSource.driverClassName' : container.driverClassName,
+            'dataSource.username'        : container.username,
+            'dataSource.password'        : container.password,
+            'dataSource.dbCreate'        : 'create-drop',
+            'hibernate.hbm2ddl.auto'     : 'create',
+        ]
+        // 'hibernate.dialect' is intentionally omitted - Hibernate 7 auto-detects it from
+        // JDBC metadata, avoiding a hardcoded dialect string per database.
+        manager.setup(this.class)
+
+        when:
+        Map<String, Object> column
+        datastore.dataSource.connection.withCloseable { conn ->
+            conn.createStatement().withCloseable { stmt ->
+                stmt.executeQuery('''
+                    select character_maximum_length
+                    from information_schema.columns
+                    where upper(table_name) = 'TEXT_TYPE_MESSAGE' and upper(column_name) = 'BODY'
+                '''.stripIndent()).with { rs ->
+                    rs.next()
+                    column = [maxLength: rs.getObject('character_maximum_length')]
+                }
+            }
+        }
+
+        then: 'no small bounded length is reported - a regression would report 32600 (Length.LONG)'
+        column.maxLength == null || (column.maxLength as long) > 1_000_000L
+
+        where:
+        db          | container
+        "Postgres"  | postgres
+        "MySQL"     | mysql
+        "MariaDB"   | mariadb
+    }
+}
+
+@Entity
+class TextTypeMessage implements HibernateEntity<TextTypeMessage> {
+    String body
+
+    static mapping = {
+        body type: 'text'
+    }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnLengthSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnLengthSpec.groovy
new file mode 100644
index 0000000000..5c09a6f634
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnLengthSpec.groovy
@@ -0,0 +1,99 @@
+/*
+ *  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
+
+import grails.gorm.annotation.Entity
+import grails.gorm.hibernate.HibernateEntity
+import grails.gorm.tests.HibernateGormDatastoreSpec
+import org.hibernate.Length
+import org.hibernate.mapping.PersistentClass
+
+/**
+ * Covers https://github.com/apache/grails-core/issues/16010 against the default H2 datastore
+ * used by the rest of the test suite, so the {@code type: 'text'} column-length behaviour is
+ * exercised even when Docker (and so {@link GormTextTypeColumnIntegrationSpec}'s Postgres/MySQL/
+ * MariaDB Testcontainers) is unavailable.
+ */
+class GormTextTypeColumnLengthSpec extends HibernateGormDatastoreSpec {
+
+    void setupSpec() {
+        manager.registerDomainClasses(UnboundedTextTypeMessage, BoundedTextTypeMessage)
+    }
+
+    void "a property mapped with type 'text' and no explicit length is bound to Length.LONG32"() {
+        when:
+        PersistentClass persistentClass = datastore.getMetadata().getEntityBinding(UnboundedTextTypeMessage.name)
+        def column = persistentClass.getProperty('body').getColumns().first()
+
+        then:
+        column.getLength() == Length.LONG32 as Long
+    }
+
+    void "a property mapped with type 'text' and an explicit maxSize keeps the bounded length"() {
+        when:
+        PersistentClass persistentClass = datastore.getMetadata().getEntityBinding(BoundedTextTypeMessage.name)
+        def column = persistentClass.getProperty('body').getColumns().first()
+
+        then:
+        column.getLength() == 500L
+    }
+
+    void "a property mapped with type 'text' and no explicit length produces an unbounded H2 CLOB column"() {
+        when:
+        Map<String, Object> column
+        datastore.dataSource.connection.withCloseable { conn ->
+            conn.createStatement().withCloseable { stmt ->
+                stmt.executeQuery('''
+                    select data_type, character_maximum_length
+                    from information_schema.columns
+                    where table_name = 'UNBOUNDED_TEXT_TYPE_MESSAGE' and column_name = 'BODY'
+                '''.stripIndent()).with { rs ->
+                    rs.next()
+                    column = [dataType: rs.getString('data_type'), maxLength: rs.getObject('character_maximum_length')]
+                }
+            }
+        }
+
+        then:
+        column.dataType == 'CHARACTER LARGE OBJECT'
+        column.maxLength == Long.MAX_VALUE
+    }
+}
+
+@Entity
+class UnboundedTextTypeMessage implements HibernateEntity<UnboundedTextTypeMessage> {
+    String body
+
+    static mapping = {
+        body type: 'text'
+    }
+}
+
+@Entity
+class BoundedTextTypeMessage implements HibernateEntity<BoundedTextTypeMessage> {
+    String body
+
+    static constraints = {
+        body maxSize: 500
+    }
+
+    static mapping = {
+        body type: 'text'
+    }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy
index 096c3f9409..7c4a51cf48 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/ColumnBinderSpec.groovy
@@ -60,7 +60,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "mtm_fk"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         column.getName() == "mtm_fk"
@@ -97,7 +97,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         parentProp.isNullable() >> true // should make column initially nullable
 
         when:
-        binder.bindColumn(prop, parentProp, column, cc, "p", table)
+        binder.bindColumn(prop, parentProp, column, cc, "p", table, null)
 
         then:
         column.getName() == "num_col"
@@ -138,7 +138,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "fetched_col"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         column.getName() == "pre_existing" // should not overwrite existing name
@@ -171,12 +171,12 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "str_col"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, "text")
 
         then:
         column.getName() == "str_col"
         column.isNullable() == false
-        1 * stringBinder.bindStringColumnConstraints(column, _)
+        1 * stringBinder.bindStringColumnConstraints(column, _, "text")
         1 * keyCreator.createKeyForProps(prop, null, table, "str_col")
         1 * indexBinder.bindIndex("str_col", column, null, table)
     }
@@ -206,7 +206,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "one_to_one_fk"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         column.getName() == "one_to_one_fk"
@@ -239,7 +239,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "to_one_fk"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         column.getName() == "to_one_fk"
@@ -272,7 +272,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "assoc_fk"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         column.getName() == "assoc_fk"
@@ -307,7 +307,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         parentProp.isNullable() >> false
 
         when:
-        binder.bindColumn(prop, parentProp, column, null, null, table)
+        binder.bindColumn(prop, parentProp, column, null, null, table, null)
 
         then:
         column.getName() == "na_col"
@@ -342,7 +342,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         parentProp.isNullable() >> true
 
         when:
-        binder.bindColumn(prop, parentProp, column, null, null, table)
+        binder.bindColumn(prop, parentProp, column, null, null, table, null)
 
         then:
         column.getName() == "na_col2"
@@ -377,7 +377,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         parentProp.isNullable() >> false
 
         when:
-        binder.bindColumn(prop, parentProp, column, null, null, table)
+        binder.bindColumn(prop, parentProp, column, null, null, table, null)
 
         then:
         column.getName() == "na_col3"
@@ -413,14 +413,14 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(propNotUnique, null, null) >> "nu_col"
 
         when:
-        binder.bindColumn(propUnique, null, column, null, null, table)
+        binder.bindColumn(propUnique, null, column, null, null, table, null)
 
         then:
         column.isUnique()
 
         when:
         def column2 = new Column("test2")
-        binder.bindColumn(propNotUnique, null, column2, null, null, table)
+        binder.bindColumn(propNotUnique, null, column2, null, null, table, null)
 
         then:
         !column2.isUnique()
@@ -450,7 +450,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "sub_col"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         column.getName() == "sub_col"
@@ -483,10 +483,10 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "data_col"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
-        1 * stringBinder.bindStringColumnConstraints(column, _)
+        1 * stringBinder.bindStringColumnConstraints(column, _, null)
     }
 
     def "uniqueness is false when uniqueWithinGroup is true"() {
@@ -513,7 +513,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
         columnNameFetcher.getColumnNameForPropertyAndPath(prop, null, null) >> "g_col"
 
         when:
-        binder.bindColumn(prop, null, column, null, null, table)
+        binder.bindColumn(prop, null, column, null, null, table, null)
 
         then:
         !column.isUnique()
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueBinderSpec.groovy
index 68772241dc..4da1d223b1 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueBinderSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleValueBinderSpec.groovy
@@ -29,6 +29,7 @@ import org.grails.orm.hibernate.cfg.Mapping
 import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
 import org.grails.orm.hibernate.cfg.PropertyConfig
 
+import org.hibernate.Length
 import org.hibernate.mapping.Column
 import org.hibernate.mapping.SimpleValue
 import spock.lang.Specification
@@ -241,6 +242,69 @@ class SimpleValueBinderSpec extends Specification {
         2 * sv.addColumn(_ as Column)
     }
 
+    def "unbounded text type defaults column length to Length.LONG32 when no explicit length is configured"() {
+        given:
+        def prop = Mock(HibernatePersistentProperty)
+        def owner = Mock(GrailsHibernatePersistentEntity)
+        def mapping = Mock(Mapping)
+        def pc = Mock(PropertyConfig)
+        def sv = Mock(SimpleValue)
+        sv.getTable() >> null
+
+        prop.getMappedForm() >> pc
+        prop.getHibernateMappedForm() >> pc
+        prop.getOwner() >> owner
+        prop.getHibernateOwner() >> owner
+        owner.getMappedForm() >> mapping
+        owner.getHibernateMappedForm() >> mapping
+        _ * prop.getHibernateMappedForm() >> pc
+        _ * owner.getHibernateMappedForm() >> mapping
+        prop.getTypeName(sv) >> 'text'
+        pc.isDerived() >> false
+        pc.getColumns() >> null
+        prop.getType() >> String
+        prop.isNullable() >> true
+        namingStrategy.resolveColumnName(_) >> 'body'
+
+        when:
+        binder.bindSimpleValue(prop, null, sv, 'path')
+
+        then:
+        1 * sv.addColumn({ Column column -> column.getLength() == Length.LONG32 as Long })
+    }
+
+    def "unbounded text type keeps an explicit maxSize bound instead of defaulting to Length.LONG32"() {
+        given:
+        def prop = Mock(HibernatePersistentProperty)
+        def owner = Mock(GrailsHibernatePersistentEntity)
+        def mapping = Mock(Mapping)
+        def pc = Mock(PropertyConfig)
+        def sv = Mock(SimpleValue)
+        sv.getTable() >> null
+
+        prop.getMappedForm() >> pc
+        prop.getHibernateMappedForm() >> pc
+        prop.getOwner() >> owner
+        prop.getHibernateOwner() >> owner
+        owner.getMappedForm() >> mapping
+        owner.getHibernateMappedForm() >> mapping
+        _ * prop.getHibernateMappedForm() >> pc
+        _ * owner.getHibernateMappedForm() >> mapping
+        prop.getTypeName(sv) >> 'text'
+        pc.isDerived() >> false
+        pc.getColumns() >> null
+        pc.getMaxSize() >> 500
+        prop.getType() >> String
+        prop.isNullable() >> true
+        namingStrategy.resolveColumnName(_) >> 'body'
+
+        when:
+        binder.bindSimpleValue(prop, null, sv, 'path')
+
+        then:
+        1 * sv.addColumn({ Column column -> column.getLength() == 500L })
+    }
+
     def "bindSimpleValue creates and returns BasicValue"() {
         given:
         def prop = Mock(HibernatePersistentProperty)
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/StringColumnConstraintsBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/StringColumnConstraintsBinderSpec.groovy
index de372d077b..8006ced8ba 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/StringColumnConstraintsBinderSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/StringColumnConstraintsBinderSpec.groovy
@@ -19,6 +19,7 @@
 
 package org.grails.orm.hibernate.cfg.domainbinding
 
+import org.hibernate.Length
 import org.hibernate.mapping.Column
 import org.grails.datastore.mapping.config.Property
 import spock.lang.Specification
@@ -44,7 +45,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         def originalLength = column.length
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == originalLength
@@ -57,7 +58,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         def originalLength = column.length
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == originalLength
@@ -69,7 +70,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         mappedForm.getInList() >> null
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == 255
@@ -81,7 +82,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         mappedForm.getInList() >> ["1","2","3","4"]
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == 4 // length of "very long string" - preserving original expectation
@@ -93,7 +94,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         mappedForm.getInList() >> ["4","string",Long.MAX_VALUE.toString(), null]
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == 4 // length of "very long string" - preserving original expectation
@@ -106,7 +107,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         mappedForm.getInList() >> ["3"]
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == 1
@@ -119,7 +120,7 @@ class StringColumnConstraintsBinderSpec extends Specification {
         def originalLength = column.length
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == originalLength
@@ -132,9 +133,70 @@ class StringColumnConstraintsBinderSpec extends Specification {
         mappedForm.getInList() >> null
 
         when:
-        binder.bindStringColumnConstraints(column, mappedForm)
+        binder.bindStringColumnConstraints(column, mappedForm, null)
 
         then:
         column.length == 50
     }
+
+    def "should default column length to Length.LONG32 for an unbounded text type when neither maxSize nor inList is provided"() {
+        given:
+        mappedForm.getMaxSize() >> null
+        mappedForm.getInList() >> null
+
+        when:
+        binder.bindStringColumnConstraints(column, mappedForm, 'text')
+
+        then:
+        column.length == Length.LONG32
+    }
+
+    def "should match the text type name case-insensitively"() {
+        given:
+        mappedForm.getMaxSize() >> null
+        mappedForm.getInList() >> null
+
+        when:
+        binder.bindStringColumnConstraints(column, mappedForm, 'TEXT')
+
+        then:
+        column.length == Length.LONG32
+    }
+
+    def "should prioritize an explicit maxSize over defaulting a text type to Length.LONG32"() {
+        given:
+        mappedForm.getMaxSize() >> 500
+        mappedForm.getInList() >> null
+
+        when:
+        binder.bindStringColumnConstraints(column, mappedForm, 'text')
+
+        then:
+        column.length == 500
+    }
+
+    def "should prioritize inList over defaulting a text type to Length.LONG32"() {
+        given:
+        mappedForm.getMaxSize() >> null
+        mappedForm.getInList() >> ["1", "22", "333"]
+
+        when:
+        binder.bindStringColumnConstraints(column, mappedForm, 'text')
+
+        then:
+        column.length == 333
+    }
+
+    def "should not default column length when typeName is not text"() {
+        given:
+        mappedForm.getMaxSize() >> null
+        mappedForm.getInList() >> null
+        def originalLength = column.length
+
+        when:
+        binder.bindStringColumnConstraints(column, mappedForm, 'custom.Type')
+
+        then:
+        column.length == originalLength
+    }
 }
\ No newline at end of file
diff --git a/grails-doc/src/en/ref/Database Mapping/type.adoc b/grails-doc/src/en/ref/Database Mapping/type.adoc
index 7264a31025..0e21df0a55 100644
--- a/grails-doc/src/en/ref/Database Mapping/type.adoc	
+++ b/grails-doc/src/en/ref/Database Mapping/type.adoc	
@@ -31,7 +31,7 @@ Configures the Hibernate type for a particular property.
 === Examples
 
 
-Changing to a text type (CLOB or TEXT depending on database dialect):
+Changing to an unbounded text type (e.g. `TEXT`, `LONGTEXT` or `CLOB` depending on database dialect):
 
 [source,groovy]
 ----
@@ -77,6 +77,8 @@ static mapping = {
 }
 ----
 
+NOTE: `type: "text"` does not map to a literal SQL type named `text`. GORM asks Hibernate to resolve the dialect's own unbounded large-character type, which is `text` on PostgreSQL and H2, `longtext` on MySQL and MariaDB, and `CLOB` on Oracle. Every `Dialect` shipped with Hibernate defines this mapping, so the resolved column is always unbounded regardless of which of these databases you use — you don't need to (and can't) choose the literal SQL type name yourself.
+
 Hibernate also has the concept of custom `UserType` implementations. In this case you specify the `UserType` class. If the `UserType` maps to multiple columns you may need to specify a mapping for each column:
 
 [source,groovy]