diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java index dbeabf4328f3..d7dca4889247 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java @@ -43,6 +43,15 @@ public interface DialectSpecificSettings { */ String ORACLE_APPLICATION_CONTINUITY = "hibernate.dialect.oracle.application_continuity"; + /** + * Specifies whether the dialect should use the binary IEEE Oracle SQL types {@code binary_float}/{@code binary_double} + * over {@code float(p)}/{@code real}/{@code double precision} when generating DDL or SQL casts for float types. + * + * @settingDefault {@code true} + * @since 7.0 + */ + String ORACLE_USE_BINARY_FLOATS = "hibernate.dialect.oracle.use_binary_floats"; + /** * Specifies whether the {@code ansinull} setting is enabled on Sybase. *

diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 9bd34c047891..f5b027b74c52 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -4,17 +4,8 @@ */ package org.hibernate.dialect; -import java.sql.CallableStatement; -import java.sql.DatabaseMetaData; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; -import java.time.temporal.ChronoField; -import java.time.temporal.TemporalAccessor; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import jakarta.persistence.GenerationType; +import jakarta.persistence.TemporalType; import org.hibernate.Length; import org.hibernate.QueryTimeoutException; import org.hibernate.boot.model.FunctionContributions; @@ -34,6 +25,8 @@ import org.hibernate.dialect.temptable.TemporaryTableKind; import org.hibernate.dialect.unique.CreateTableUniqueDelegate; import org.hibernate.dialect.unique.UniqueDelegate; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; @@ -47,20 +40,20 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.config.ConfigurationHelper; import org.hibernate.mapping.AggregateColumn; +import org.hibernate.mapping.CheckConstraint; import org.hibernate.mapping.Table; import org.hibernate.mapping.UserDefinedType; -import org.hibernate.mapping.CheckConstraint; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.procedure.internal.OracleCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.SemanticException; +import org.hibernate.query.common.FetchClauseType; +import org.hibernate.query.common.TemporalUnit; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.CastType; -import org.hibernate.query.common.FetchClauseType; import org.hibernate.query.sqm.IntervalType; -import org.hibernate.query.common.TemporalUnit; import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableInsertStrategy; import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableMutationStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; @@ -92,19 +85,29 @@ import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.internal.NamedNativeEnumDdlTypeImpl; import org.hibernate.type.descriptor.sql.internal.NamedNativeOrdinalEnumDdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; -import jakarta.persistence.GenerationType; -import jakarta.persistence.TemporalType; +import java.sql.CallableStatement; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static org.hibernate.LockOptions.NO_WAIT; import static org.hibernate.LockOptions.SKIP_LOCKED; import static org.hibernate.LockOptions.WAIT_FOREVER; +import static org.hibernate.cfg.DialectSpecificSettings.ORACLE_USE_BINARY_FLOATS; import static org.hibernate.dialect.OracleJdbcHelper.getArrayJdbcTypeConstructor; import static org.hibernate.dialect.OracleJdbcHelper.getNestedTableJdbcTypeConstructor; import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; @@ -200,11 +203,9 @@ protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggr // Is the database accessed using a database service protected by Application Continuity. protected final boolean applicationContinuity; - protected final int driverMajorVersion; - protected final int driverMinorVersion; - + private boolean useBinaryFloat; public OracleDialect() { this( MINIMUM_VERSION ); @@ -770,11 +771,11 @@ protected String columnType(int sqlTypeCode) { return "number(19,0)"; case REAL: // Oracle's 'real' type is actually double precision - return "float(24)"; + return useBinaryFloat ? "binary_float" : "float(24)"; case DOUBLE: // Oracle's 'double precision' means float(126), and // we never need 126 bits (38 decimal digits) - return "float(53)"; + return useBinaryFloat ? "binary_double" : "float(53)"; case NUMERIC: case DECIMAL: @@ -959,6 +960,9 @@ public Exporter getUserDefinedTypeExporter() { @Override public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + final ConfigurationService configurationService = serviceRegistry.requireService( ConfigurationService.class ); + useBinaryFloat = configurationService.getSetting( ORACLE_USE_BINARY_FLOATS, StandardConverters.BOOLEAN, true ); + super.contributeTypes( typeContributions, serviceRegistry ); if ( ConfigurationHelper.getPreferredSqlTypeCodeForBoolean( serviceRegistry, this ) == BIT ) { typeContributions.contributeJdbcType( OracleBooleanJdbcType.INSTANCE ); @@ -972,6 +976,15 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry typeContributions.contributeJdbcType( OracleReflectionStructJdbcType.INSTANCE ); } + if ( useBinaryFloat ) { + // Override the descriptor for float to produce binary_float or binary_double based on precision + typeContributions.getTypeConfiguration().getDdlTypeRegistry().addDescriptor( + CapacityDependentDdlType.builder( FLOAT, columnType( DOUBLE ), this ) + .withTypeCapacity( getFloatPrecision(), columnType( REAL ) ) + .build() + ); + } + if ( getVersion().isSameOrAfter( 21 ) ) { typeContributions.contributeJdbcType( OracleJsonJdbcType.INSTANCE ); typeContributions.contributeJdbcTypeConstructor( OracleJsonArrayJdbcTypeConstructor.NATIVE_INSTANCE ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java index 60f2de99801d..71d96604798b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java @@ -11,8 +11,8 @@ import org.hibernate.ScrollableResults; import org.hibernate.TypeMismatchException; import org.hibernate.cfg.Environment; -import org.hibernate.community.dialect.InformixDialect; import org.hibernate.community.dialect.DerbyDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; @@ -63,6 +63,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; import java.sql.Time; import java.sql.Timestamp; import java.util.ArrayList; @@ -80,6 +82,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hibernate.testing.orm.junit.ExtraAssertions.assertClassAssignability; import static org.hibernate.testing.orm.junit.ExtraAssertions.assertTyping; +import static org.junit.Assert.assertEquals; /** * Tests the integration of the new AST parser into the loading of query results using @@ -2791,12 +2794,8 @@ public void testStr(SessionFactoryScope scope) { String str = (String) session.createQuery( "select str(an.bodyWeight) from Animal an where str(an.bodyWeight) like '%1%'" ) .uniqueResult(); - if ( session.getDialect() instanceof DB2Dialect ) { - assertThat( str ).startsWith( "1.234" ); - } - else { - assertThat( str ).startsWith( "123.4" ); - } + BigDecimal value = new BigDecimal( str, new MathContext( 4, RoundingMode.DOWN ) ); + assertEquals( new BigDecimal( "123.4" ), value ); String dateStr1 = (String) session.createQuery( "select str(current_date) from Animal" ) .uniqueResult(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java index 154e21bab536..be84f182a45b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java @@ -55,7 +55,9 @@ integrators = SharedDriverManagerTypeCacheClearingIntegrator.class ) // Don't reorder columns in the types here to avoid the need to rewrite the test -@ServiceRegistry(settings = @Setting(name = AvailableSettings.COLUMN_ORDERING_STRATEGY, value = "legacy")) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.COLUMN_ORDERING_STRATEGY, value = "legacy") +}) @DomainModel(annotatedClasses = JsonWithArrayEmbeddableTest.JsonHolder.class) @SessionFactory public class JsonWithArrayEmbeddableTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/formula/FormulaTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/formula/FormulaTests.java index 4a92a6dda6bd..3429ccb915d5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/formula/FormulaTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/formula/FormulaTests.java @@ -114,7 +114,7 @@ public static class Account { @DialectOverride.Formula(dialect = DB2Dialect.class, override = @Formula("varchar_format(rate * 100) || '%'")) @DialectOverride.Formula(dialect = OracleDialect.class, - override = @Formula("to_char(rate * 100) || '%'")) + override = @Formula("to_char(cast(rate * 100 as number(10,2))) || '%'")) @DialectOverride.Formula(dialect = SQLServerDialect.class, override = @Formula("ltrim(str(rate * 100, 10, 2)) + '%'")) @DialectOverride.Formula(dialect = SybaseDialect.class, diff --git a/migration-guide.adoc b/migration-guide.adoc index a2cb5710167f..821072e6efef 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -635,6 +635,26 @@ The default precision for SQL Server timestamps was changed to 7, i.e. 100 nanos Note that these changes only affect DDL generation. +[[float-mapping-changes-oracle]] +=== DDL type for Java `float` and `double` changed on Oracle + +Previous version of Hibernate ORM mapped Java `float` and `double` to Oracle `float(p)`, `real` or `double precision` +types, which are all internally implemented as `number`. To avoid potential misbehavior compared to Java execution +and match the expectations of the IEEE floating point semantics as requested by using Java `float`/`double`, +the default DDL types were changed to Oracles IEEE floating point types `binary_float` and `binary_double` respectively. + +Migration requires multiple steps because Oracle doesn't support online type changes: + +```sql +alter table TBL add (NEW_COLUMN binary_float); +update TBL set NEW_COLUMN=OLD_COLUMN; +alter table TBL drop column OLD_COLUMN; +alter table TBL rename column NEW_COLUMN to OLD_COLUMN; +``` + +Note that changing the schema is not required for Hibernate ORM to work correctly. +The previous behavior may be recovered by setting `hibernate.dialect.oracle.use_binary_floats` to `false`. + [[array-mapping-changes-on-db2-sap-hana-sql-server-and-sybase-ase]] === Array Mapping Changes