Skip to content
Open
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 @@ -119,7 +119,7 @@ public void bindColumn(
stringColumnConstraintsBinder.bindStringColumnConstraints(column, mappedForm);
} else if (type != null && Number.class.isAssignableFrom(type)) {
PropertyConfig mappedForm = property.getHibernateMappedForm();
numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, mappedForm);
numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, mappedForm, type);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.mapping.Column;

import org.grails.orm.hibernate.cfg.ColumnConfig;
Expand All @@ -44,7 +43,8 @@ public NumericColumnConstraintsBinder(Dialect dialect) {
this.dialect = dialect;
}

public void bindNumericColumnConstraints(Column column, ColumnConfig cc, PropertyConfig constrainedProperty) {
public void bindNumericColumnConstraints(
Column column, ColumnConfig cc, PropertyConfig constrainedProperty, Class<?> propertyType) {
int scale = determineScale(cc, constrainedProperty);
if (scale > -1) {
column.setScale(scale);
Expand All @@ -53,25 +53,33 @@ public void bindNumericColumnConstraints(Column column, ColumnConfig cc, Propert
}
if (cc != null && cc.getPrecision() > -1) {
column.setPrecision(cc.getPrecision());
} else {
} else if (!isApproximateFloatingPoint(propertyType)) {
int minConstraintValueLength = getConstraintValueLength(constrainedProperty.getMin(), scale);
int maxConstraintValueLength = getConstraintValueLength(constrainedProperty.getMax(), scale);

int defaultPrecision;
if (dialect instanceof OracleDialect) {
defaultPrecision = 126;
} else {
// Default to 15 decimal digits which maps to ~50-53 bits in Hibernate 7
// This avoids float(64) DDL errors in H2 and PostgreSQL
defaultPrecision = 15;
}

int precision = minConstraintValueLength > 0 && maxConstraintValueLength > 0 ?
Math.max(minConstraintValueLength, maxConstraintValueLength) :
DefaultGroovyMethods.max(
new Integer[] {defaultPrecision, minConstraintValueLength, maxConstraintValueLength});
DefaultGroovyMethods.max(new Integer[] {
dialect.getDefaultDecimalPrecision(), minConstraintValueLength, maxConstraintValueLength
});
column.setPrecision(precision);
}
// else: leave Float/Double precision unset. Hibernate renders FLOAT/DOUBLE DDL as
// float(precision) where precision is a *bit* count (IEEE-754), converted internally
// from whatever decimal-digit value column.setPrecision() is given (n * log2(10)). Any
// decimal-oriented default - Hibernate's own 19/38, or a dialect's getFloatPrecision()/
// getDoublePrecision(), which are already bit counts and would be converted a second
// time - overflows H2/PostgreSQL's 53-bit ceiling and produces DDL those dialects reject
// at execution time (silently: Hibernate only logs the failure, it doesn't throw, so the
// table is never created - see GH numeric-precision issue). Leaving precision unset lets
// Hibernate fall back to the dialect's own correct float/double DDL type directly.
}

private boolean isApproximateFloatingPoint(Class<?> propertyType) {
return Float.class.equals(propertyType) ||
float.class.equals(propertyType) ||
Double.class.equals(propertyType) ||
double.class.equals(propertyType);
}

private int getConstraintValueLength(Comparable<?> min, int scale) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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

/**
* Extends the H2-only coverage in {@link GormNumericTypeColumnPrecisionSpec} across every
* externally-run dialect this module tests against (see {@link grails.gorm.tests.RLikeHibernate7Spec}
* for the same H2/Postgres/MySQL/MariaDB precedent): a Float/Double/BigDecimal property with no
* explicit precision must produce creatable DDL, not a silently-dropped table. Oracle is
* intentionally excluded - its Testcontainers image is too flaky in CI to gate this spec on.
*/
@Testcontainers
@Requires({ isDockerAvailable() })
class GormNumericTypeColumnIntegrationSpec 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(NumericTypeReading)
}

void "a Float/Double/BigDecimal property with no explicit precision produces creatable DDL on #db"() {
given:
// 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> columns = [:]
datastore.dataSource.connection.withCloseable { conn ->
conn.createStatement().withCloseable { stmt ->
stmt.executeQuery('''
select column_name, numeric_precision
from information_schema.columns
where upper(table_name) = 'NUMERIC_TYPE_READING'
'''.stripIndent()).with { rs ->
while (rs.next()) {
columns[rs.getString('column_name').toUpperCase()] = rs.getInt('numeric_precision')
}
}
}
}

then: 'the table was not silently dropped, and every numeric column has a valid precision'
columns.keySet().containsAll(['FLOAT_VALUE', 'DOUBLE_VALUE', 'BIG_DECIMAL_VALUE'])
columns['FLOAT_VALUE'] > 0
columns['DOUBLE_VALUE'] > 0
columns['BIG_DECIMAL_VALUE'] > 0

where:
db | container
"Postgres" | postgres
"MySQL" | mysql
"MariaDB" | mariadb
}
}

@Entity
class NumericTypeReading implements HibernateEntity<NumericTypeReading> {
Float floatValue
Double doubleValue
BigDecimal bigDecimalValue
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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.dialect.H2Dialect
import org.hibernate.mapping.PersistentClass

/**
* Covers a regression where a Float/Double property with no explicit precision produced
* invalid DDL (Hibernate rendered {@code float(64)}, which H2/PostgreSQL reject since their
* FLOAT bit-precision ceiling is 53) that {@code hibernate.hbm2ddl.auto} logs but does not
* throw for - the table is silently never created. FLOAT/DOUBLE precision is a *bit* count,
* while NUMERIC/DECIMAL (BigDecimal) precision is a *decimal digit* count; applying one default
* to every Number subtype conflates the two, and Hibernate itself converts a decimal-digit
* precision into a bit count when rendering float(n) DDL - so NumericColumnConstraintsBinder now
* leaves Float/Double precision unset (letting Hibernate fall back to the dialect's own correct
* float/double DDL type directly) and only computes a decimal-digit default, from the dialect's
* Dialect.getDefaultDecimalPrecision(), for BigDecimal/BigInteger. No per-dialect (e.g. Oracle)
* special case is needed for either family.
*
* Runs against the default H2 datastore, so it needs no container - if schema creation in
* setupSpec() silently drops the table, the live-DDL test below fails to find its columns. The
* Testcontainers-backed multi-dialect DDL check lives separately in
* {@link GormNumericTypeColumnIntegrationSpec}.
*/
class GormNumericTypeColumnPrecisionSpec extends HibernateGormDatastoreSpec {

void setupSpec() {
manager.registerDomainClasses(NumericTypeMessage)
}

void "a Float property with no explicit precision is left unset for Hibernate's own dialect default"() {
when:
PersistentClass persistentClass = datastore.getMetadata().getEntityBinding(NumericTypeMessage.name)
def column = persistentClass.getProperty('floatValue').getColumns().first()

then:
column.getPrecision() == null
}

void "a Double property with no explicit precision is left unset for Hibernate's own dialect default"() {
when:
PersistentClass persistentClass = datastore.getMetadata().getEntityBinding(NumericTypeMessage.name)
def column = persistentClass.getProperty('doubleValue').getColumns().first()

then:
column.getPrecision() == null
}

void "a BigDecimal property with no explicit precision defaults to the dialect's decimal precision"() {
when:
PersistentClass persistentClass = datastore.getMetadata().getEntityBinding(NumericTypeMessage.name)
def column = persistentClass.getProperty('bigDecimalValue').getColumns().first()

then:
column.getPrecision() == new H2Dialect().getDefaultDecimalPrecision()
}

void "the numeric_type_message table is actually created with valid DDL on H2"() {
when:
Map<String, Object> columns = [:]
datastore.dataSource.connection.withCloseable { conn ->
conn.createStatement().withCloseable { stmt ->
stmt.executeQuery('''
select column_name, numeric_precision
from information_schema.columns
where upper(table_name) = 'NUMERIC_TYPE_MESSAGE'
'''.stripIndent()).with { rs ->
while (rs.next()) {
columns[rs.getString('column_name').toUpperCase()] = rs.getInt('numeric_precision')
}
}
}
}

then: 'the table was not silently dropped, and every numeric column has a valid precision'
columns.keySet().containsAll(['FLOAT_VALUE', 'DOUBLE_VALUE', 'BIG_DECIMAL_VALUE'])
columns['FLOAT_VALUE'] > 0
columns['DOUBLE_VALUE'] > 0
columns['BIG_DECIMAL_VALUE'] > 0
}
}

@Entity
class NumericTypeMessage implements HibernateEntity<NumericTypeMessage> {
Float floatValue
Double doubleValue
BigDecimal bigDecimalValue
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class ColumnBinderSpec extends HibernateGormDatastoreSpec {
column.getCustomRead() == "r"
column.getCustomWrite() == "w"

1 * numericBinder.bindNumericColumnConstraints(column, cc, _)
1 * numericBinder.bindNumericColumnConstraints(column, cc, _, Integer)
1 * keyCreator.createKeyForProps(prop, "p", table, "num_col")
1 * indexBinder.bindIndex("num_col", column, cc, table)
}
Expand Down
Loading
Loading