From 61e4947a55c9f323d336e4676fae179281a86c79 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 4 Dec 2025 17:52:13 +0100 Subject: [PATCH] HHH-19976 Don't adopt AdjustableBasicType name to create derived type Also, store BasicTypeReferences by java type name and try finding a JavaType/JdbcType match when resolving a BasicType --- .../internal/InferredBasicValueResolver.java | 12 ++-- .../process/spi/MetadataBuildingProcess.java | 5 +- .../hibernate/type/AdjustableBasicType.java | 8 +-- .../org/hibernate/type/BasicTypeRegistry.java | 65 ++++++++++++++----- .../hibernate/type/StandardBasicTypes.java | 5 +- .../mapping/basic/DurationMappingTests.java | 3 + .../integration/basic/NationalizedTest.java | 65 +++++++++++++++++++ .../orm/junit/DialectFeatureChecks.java | 6 ++ 8 files changed, 136 insertions(+), 33 deletions(-) create mode 100644 hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/NationalizedTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java b/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java index e00feb694a43..7e0b38521d3d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/internal/InferredBasicValueResolver.java @@ -68,7 +68,7 @@ public static BasicValue.Resolution from( final var typeConfiguration = bootstrapContext.getTypeConfiguration(); final var basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); - final var reflectedJtd = reflectedJtdResolver.get(); + final JavaType reflectedJtd; // NOTE: the distinction that is made below wrt `explicitJavaType` and `reflectedJtd` // is needed temporarily to trigger "legacy resolution" versus "ORM6 resolution. @@ -110,7 +110,7 @@ else if ( explicitJdbcType != null ) { } } } - else if ( reflectedJtd != null ) { + else if ( ( reflectedJtd = reflectedJtdResolver.get() ) != null ) { // we were able to determine the "reflected java-type" // Use JTD if we know it to apply any specialized resolutions if ( reflectedJtd instanceof EnumJavaType enumJavaType ) { @@ -150,7 +150,7 @@ else if ( explicitJdbcType != null ) { if ( registeredType != null ) { // so here is the legacy resolution - jdbcMapping = resolveSqlTypeIndicators( stdIndicators, registeredType, reflectedJtd ); + jdbcMapping = resolveSqlTypeIndicators( stdIndicators, registeredType, registeredType.getJavaTypeDescriptor() ); } else { // there was not a "legacy" BasicType registration, @@ -311,7 +311,11 @@ private static BasicType pluralBasicType( pluralJavaType.resolveType( bootstrapContext.getTypeConfiguration(), dialect, - resolveSqlTypeIndicators( stdIndicators, registeredElementType, elementJavaType ), + resolveSqlTypeIndicators( + stdIndicators, + registeredElementType, + registeredElementType.getJavaTypeDescriptor() + ), selectable instanceof ColumnTypeInformation information ? information : null, stdIndicators ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java index af8a90295a7a..a8b3a9443efe 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java @@ -762,7 +762,7 @@ public void contributeType(CompositeUserType type) { } final int preferredSqlTypeCodeForDuration = getPreferredSqlTypeCodeForDuration( serviceRegistry ); - if ( preferredSqlTypeCodeForDuration != SqlTypes.INTERVAL_SECOND ) { + if ( preferredSqlTypeCodeForDuration != SqlTypes.DURATION ) { adaptToPreferredSqlTypeCode( typeConfiguration, jdbcTypeRegistry, @@ -772,9 +772,6 @@ public void contributeType(CompositeUserType type) { "org.hibernate.type.DurationType" ); } - else { - addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INTERVAL_SECOND, SqlTypes.DURATION ); - } addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.INET, SqlTypes.VARBINARY ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.GEOMETRY, SqlTypes.VARBINARY ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/AdjustableBasicType.java b/hibernate-core/src/main/java/org/hibernate/type/AdjustableBasicType.java index c456e995651f..da9f9c0a38e3 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AdjustableBasicType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AdjustableBasicType.java @@ -25,16 +25,16 @@ default BasicType resolveIndicatedType(JdbcTypeIndicators indicators, Jav indicators, domainJtd ); - if ( resolvedJdbcType != jdbcType ) { + if ( getJavaTypeDescriptor() != domainJtd || resolvedJdbcType != jdbcType ) { return indicators.getTypeConfiguration().getBasicTypeRegistry() - .resolve( domainJtd, resolvedJdbcType, getName() ); + .resolve( domainJtd, resolvedJdbcType ); } } else { final int resolvedJdbcTypeCode = indicators.resolveJdbcTypeCode( jdbcType.getDefaultSqlTypeCode() ); - if ( resolvedJdbcTypeCode != jdbcType.getDefaultSqlTypeCode() ) { + if ( getJavaTypeDescriptor() != domainJtd || resolvedJdbcTypeCode != jdbcType.getDefaultSqlTypeCode() ) { return indicators.getTypeConfiguration().getBasicTypeRegistry() - .resolve( domainJtd, indicators.getJdbcType( resolvedJdbcTypeCode ), getName() ); + .resolve( domainJtd, indicators.getJdbcType( resolvedJdbcTypeCode ) ); } } return (BasicType) this; diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java index 4b437c0184f4..9e9070864372 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java @@ -5,6 +5,8 @@ package org.hibernate.type; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -49,6 +51,7 @@ public class BasicTypeRegistry implements Serializable { private final Map> typesByName = new ConcurrentHashMap<>(); private final Map> typeReferencesByName = new ConcurrentHashMap<>(); + private final Map>> typeReferencesByJavaTypeName = new ConcurrentHashMap<>(); public BasicTypeRegistry(TypeConfiguration typeConfiguration){ this.typeConfiguration = typeConfiguration; @@ -256,14 +259,28 @@ private BasicType createIfUnregistered( if ( registeredTypeMatches( javaType, jdbcType, registeredType ) ) { return castNonNull( registeredType ); } - else { - final var createdType = creator.get(); - register( javaType, jdbcType, createdType ); - return createdType; + // Create an ad-hoc type since the java type doesn't come from the registry and is probably explicitly defined + else if ( typeConfiguration.getJavaTypeRegistry().resolveDescriptor( javaType.getJavaType() ) == javaType ) { + final var basicTypeReferences = typeReferencesByJavaTypeName.get( javaType.getTypeName() ); + if ( basicTypeReferences != null && !basicTypeReferences.isEmpty() ) { + final var jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + for ( var typeReference : basicTypeReferences ) { + if ( jdbcTypeRegistry.getDescriptor( typeReference.getSqlTypeCode() ) == jdbcType ) { + final var basicType = typesByName.get( typeReference.getName() ); + //noinspection unchecked + return registeredTypeMatches( javaType, jdbcType, basicType ) + ? (BasicType) basicType + : (BasicType) createBasicType( typeReference.getName(), typeReference ); + } + } + } } + final var createdType = creator.get(); + register( javaType, jdbcType, createdType ); + return createdType; } - private static boolean registeredTypeMatches(JavaType javaType, JdbcType jdbcType, BasicType registeredType) { + private static boolean registeredTypeMatches(JavaType javaType, JdbcType jdbcType, @Nullable BasicType registeredType) { return registeredType != null && registeredType.getJdbcType() == jdbcType && registeredType.getMappedJavaType() == javaType; @@ -334,7 +351,7 @@ public void addTypeReferenceRegistrationKey(String typeReferenceKey, String... a throw new IllegalArgumentException( "Couldn't find type reference with name: " + typeReferenceKey ); } for ( String additionalTypeReferenceKey : additionalTypeReferenceKeys ) { - typeReferencesByName.put( additionalTypeReferenceKey, basicTypeReference ); + addTypeReference( additionalTypeReferenceKey, basicTypeReference ); } } @@ -384,7 +401,7 @@ public void addPrimeEntry(BasicTypeReference type, String legacyTypeClassName // Legacy name registration if ( isNotEmpty( legacyTypeClassName ) ) { - typeReferencesByName.put( legacyTypeClassName, type ); + addTypeReference( legacyTypeClassName, type ); } // explicit registration keys @@ -429,19 +446,31 @@ private void applyRegistrationKeys(BasicTypeReference type, String[] keys) { // Incidentally, this might also help with map lookup efficiency. key = key.intern(); - // Incredibly verbose logging disabled -// LOG.tracef( "Adding type registration %s -> %s", key, type ); + addTypeReference( key, type ); + } + } + } + + private void addTypeReference(String name, BasicTypeReference typeReference) { + // Incredibly verbose logging disabled +// LOG.tracef( "Adding type registration %s -> %s", key, type ); // final BasicTypeReference old = - typeReferencesByName.put( key, type ); -// if ( old != null && old != type ) { -// LOG.tracef( -// "Type registration key [%s] overrode previous entry : `%s`", -// key, -// old -// ); -// } - } + typeReferencesByName.put( name, typeReference ); +// if ( old != null && old != type ) { +// LOG.tracef( +// "Type registration key [%s] overrode previous entry : `%s`", +// key, +// old +// ); +// } + + final var basicTypeReferences = typeReferencesByJavaTypeName.computeIfAbsent( + typeReference.getJavaType().getTypeName(), + s -> new ArrayList<>() + ); + if ( !basicTypeReferences.contains( typeReference ) ) { + basicTypeReferences.add( typeReference ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java b/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java index 8c16770b3caf..7966eced3ece 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java @@ -345,13 +345,12 @@ private StandardBasicTypes() { // Date / time data /** - * The standard Hibernate type for mapping {@link Duration} to JDBC {@link org.hibernate.type.SqlTypes#INTERVAL_SECOND INTERVAL_SECOND} - * or {@link org.hibernate.type.SqlTypes#NUMERIC NUMERIC} as a fallback. + * The standard Hibernate type for mapping {@link Duration} to JDBC {@link org.hibernate.type.SqlTypes#DURATION DURATION}. */ public static final BasicTypeReference DURATION = new BasicTypeReference<>( "Duration", Duration.class, - SqlTypes.INTERVAL_SECOND + SqlTypes.DURATION ); /** diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/DurationMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/DurationMappingTests.java index 650fa4d87004..cd048bd123ab 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/DurationMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/DurationMappingTests.java @@ -16,6 +16,8 @@ import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.AdjustableJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; @@ -44,6 +46,7 @@ * 2.2.21. Duration * By default, Hibernate maps Duration to the NUMERIC SQL type. */ +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsIntervalSecondType.class) @ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_DURATION_JDBC_TYPE, value = "INTERVAL_SECOND")) public class DurationMappingTests { diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/NationalizedTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/NationalizedTest.java new file mode 100644 index 000000000000..75a3406de224 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/NationalizedTest.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.Nationalized; +import org.hibernate.community.dialect.DerbyDialect; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.HANADialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.envers.Audited; +import org.hibernate.mapping.Table; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.type.StandardBasicTypes; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import static org.hibernate.boot.model.naming.Identifier.toIdentifier; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@JiraKey(value = "HHH-19976") +@DomainModel(annotatedClasses = {NationalizedTest.NationalizedEntity.class}) +@SessionFactory +@SkipForDialect(dialectClass = OracleDialect.class) +@SkipForDialect(dialectClass = PostgreSQLDialect.class, matchSubTypes = true, reason = "@Lob field in HQL predicate fails with error about text = bigint") +@SkipForDialect(dialectClass = HANADialect.class, matchSubTypes = true, reason = "HANA doesn't support comparing LOBs with the = operator") +@SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase doesn't support comparing LOBs with the = operator") +@SkipForDialect(dialectClass = DB2Dialect.class, matchSubTypes = true, reason = "DB2 jdbc driver doesn't support setNString") +@SkipForDialect(dialectClass = DerbyDialect.class, matchSubTypes = true, reason = "Derby jdbc driver doesn't support setNString") +public class NationalizedTest { + + @Test + public void testMetadataBindings(DomainModelScope scope) { + final var domainModel = scope.getDomainModel(); + + assertThrows( AssertionFailedError.class, () -> { + final Table auditTable = domainModel.getEntityBinding( NationalizedEntity.class.getName() + "_AUD" ) + .getTable(); + + final org.hibernate.mapping.Column colDef = auditTable.getColumn( toIdentifier( "nationalizedString" ) ); + assertEquals( StandardBasicTypes.NSTRING.getName(), colDef.getTypeName() ); + } ); + } + + @Entity(name = "NationalizedEntity") + @Audited + public static class NationalizedEntity { + @Id + @GeneratedValue + private Integer id; + @Nationalized + private String nationalizedString; + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index b6e415441cbd..b76ceb011792 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -1113,6 +1113,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsIntervalSecondType implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesDdlType( dialect, SqlTypes.INTERVAL_SECOND ); + } + } + public static class SupportsVectorType implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return definesDdlType( dialect, SqlTypes.VECTOR );