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 <[email protected]>
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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
.../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]