Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,16 @@ namingStrategy, new DefaultColumnNameFetcher(namingStrategy), new BackticksRemov
* @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,
HibernatePersistentProperty parentProperty,
Column column,
ColumnConfig cc,
String path,
Table table) {
Table table,
String typeName) {

if (cc != null) {
column.setComment(cc.getComment());
Expand All @@ -116,7 +118,7 @@ public void bindColumn(
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public SimpleValue bindSimpleValue(
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)) {
Expand All @@ -100,7 +101,7 @@ public SimpleValue bindSimpleValue(
.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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,39 @@
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
}
}
Original file line number Diff line number Diff line change
@@ -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'
}
}
Loading
Loading