diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfa18d3f560f..e1bb14e41a3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,9 @@ name: GH Actions CI on: - push: - branches: - # Pattern order matters: the last matching inclusion/exclusion wins - - 'main' - # We don't want to run CI on branches for dependabot, just on the PR. - - '!dependabot/**' pull_request: branches: - - 'main' + - '7.2' # Ignore dependabot PRs that are not just about build dependencies or workflows; # we'll reject such PRs and send one ourselves. - '!dependabot/**' diff --git a/changelog.txt b/changelog.txt index eacba2aab168..918e028afeab 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,12 @@ Hibernate 7 Changelog ======================= +Changes in 7.2.0.Final (December 12, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36806 + + Changes in 7.2.0.CR4 (December 10, 2025) ------------------------------------------------------------------------------------------------------------------------ diff --git a/gradle/version.properties b/gradle/version.properties index 8f31599e762f..ff3bcd4c1cd5 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -hibernateVersion=7.2.0-SNAPSHOT \ No newline at end of file +hibernateVersion=7.2.1-SNAPSHOT \ No newline at end of file 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/boot/models/xml/internal/ManagedTypeProcessor.java b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/ManagedTypeProcessor.java index 831be5b28700..9a2e3fa34229 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/ManagedTypeProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/ManagedTypeProcessor.java @@ -209,7 +209,6 @@ private static void processEntityMetadata( XmlAnnotationHelper.applyTable( jaxbEntity.getTable(), classDetails, xmlDocumentContext ); XmlAnnotationHelper.applySecondaryTables( jaxbEntity.getSecondaryTables(), classDetails, xmlDocumentContext ); - final JaxbAttributesContainerImpl attributes = jaxbEntity.getAttributes(); if ( attributes != null ) { processIdMappings( @@ -234,6 +233,9 @@ private static void processEntityMetadata( xmlDocumentContext ); } + + XmlAnnotationHelper.applyConverts( jaxbEntity.getConverts(), classDetails, xmlDocumentContext ); + AttributeProcessor.processAttributeOverrides( jaxbEntity.getAttributeOverrides(), classDetails, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java index 54bb3cb56d32..e3d3ec32d4d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/xml/internal/XmlAnnotationHelper.java @@ -135,6 +135,7 @@ import static org.hibernate.boot.models.JpaAnnotations.CHECK_CONSTRAINT; import static org.hibernate.boot.models.JpaAnnotations.COLUMN; import static org.hibernate.boot.models.JpaAnnotations.CONVERT; +import static org.hibernate.boot.models.JpaAnnotations.CONVERTS; import static org.hibernate.boot.models.JpaAnnotations.EXCLUDE_DEFAULT_LISTENERS; import static org.hibernate.boot.models.JpaAnnotations.EXCLUDE_SUPERCLASS_LISTENERS; import static org.hibernate.boot.models.JpaAnnotations.INDEX; @@ -804,6 +805,28 @@ public static void applyConvert( transferConvertDetails( jaxbConvert, annotation, null, xmlDocumentContext ); } + public static void applyConverts( + List jaxbConverts, + MutableAnnotationTarget target, + XmlDocumentContext xmlDocumentContext){ + if ( isEmpty( jaxbConverts ) ) { + return; + } + + final ConvertsJpaAnnotation convertsUsage = (ConvertsJpaAnnotation) target.replaceAnnotationUsage( + CONVERT, + CONVERTS, + xmlDocumentContext.getModelBuildingContext() + ); + + final Convert[] convertUsages = new Convert[jaxbConverts.size()]; + convertsUsage.value( convertUsages ); + + for ( int i = 0; i < jaxbConverts.size(); i++ ) { + convertUsages[i] = transformConvert( jaxbConverts.get( i ), null, xmlDocumentContext ); + } + } + public static void applyConverts( List jaxbConverts, String namePrefix, @@ -815,7 +838,7 @@ public static void applyConverts( final ConvertsJpaAnnotation convertsUsage = (ConvertsJpaAnnotation) memberDetails.replaceAnnotationUsage( CONVERT, - JpaAnnotations.CONVERTS, + CONVERTS, xmlDocumentContext.getModelBuildingContext() ); final Convert[] convertUsages = new Convert[jaxbConverts.size()]; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java index 89dc933e087e..23f2ef4b5d9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java @@ -79,19 +79,31 @@ public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor f return Helper.getLockTimeout( "select current_setting('lock_timeout', true)", (resultSet) -> { - // even though lock_timeout is "in milliseconds", `current_setting` - // returns a String form which unfortunately varies depending on - // the actual value: - // * for zero (no timeout), "0" is returned - // * for non-zero, `{timeout-in-seconds}s` is returned (e.g. "4s") - // so we need to "parse" that form here - final String value = resultSet.getString( 1 ); + // Although lock_timeout is stored internally in milliseconds, + // `current_setting` returns a String in a canonical, human-readable form + // that varies depending on the value: + // * "0" is returned for no timeout (WAIT_FOREVER) + // * Non-zero values may be returned with units such as: + // - milliseconds: "500ms" + // - seconds: "3s" + // - minutes: "1min" + // - hours: "1h" + // Therefore, we need to parse this String carefully to reconstruct the correct Timeout. + String value = resultSet.getString( 1 ); if ( "0".equals( value ) ) { return Timeouts.WAIT_FOREVER; } - assert value.endsWith( "s" ); - final int secondsValue = Integer.parseInt( value.substring( 0, value.length() - 1 ) ); - return Timeout.seconds( secondsValue ); + final var unitStartIndex = findUnitStartIndex( value ); + final var amount = Integer.parseInt( value, 0, unitStartIndex, 10 ); + return switch ( unitStartIndex == -1 ? "ms" : value.substring( unitStartIndex ) ) { + case "ms" -> Timeout.milliseconds( amount ); + case "s" -> Timeout.seconds( amount ); + case "min" -> Timeout.seconds( amount * 60 ); + case "h" -> Timeout.seconds( amount * 3600 ); + case "d" -> Timeout.seconds( amount * 3600 * 24 ); + default -> throw new IllegalArgumentException( + "Unexpected PostgreSQL lock_timeout format: " + value ); + }; }, connection, factory @@ -120,4 +132,13 @@ public void setLockTimeout(Timeout timeout, Connection connection, SessionFactor factory ); } + + private static int findUnitStartIndex(String value) { + for ( int i = value.length() - 1; i >= 0; i-- ) { + if ( Character.isDigit( value.charAt( i ) ) ) { + return i + 1; + } + } + return -1; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java index 35ff129960b5..65170aba9361 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java +++ b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java @@ -54,6 +54,13 @@ public long millis() { return lastTimestamp.toEpochMilli(); } + /** + * Sub-miliseconds part of timestamp (micro- and nanoseconds) mapped to 12 bit integral value. + * Calculated as nanos / 1000000 * 4096 + * + * @param timestamp + * @return + */ private static long nanos(Instant timestamp) { return (long) ((timestamp.getNano() % 1_000_000L) * 0.004096); } @@ -64,9 +71,17 @@ public State getNextState() { return new State( now, randomSequence() ); } else { - final long nextSequence = lastSequence + Holder.numberGenerator.nextLong( 0xFFFF_FFFFL ); - return nextSequence > MAX_RANDOM_SEQUENCE - ? new State( lastTimestamp.plusNanos( 250 ), randomSequence() ) + final long nextSequence = randomSequence(); + /* + * If next random sequence is less or equal to last one sub-millisecond part + * should be incremented to preserve monotonicity of generated UUIDs. + * To do this smallest number of nanoseconds that will always increase + * sub-millisecond part mapped to 12 bits is + * 1_000_000 (nanons per milli) / 4096 (12 bits) = 244.14... + * So 245 is used as smallest integer larger than this value. + */ + return lastSequence >= nextSequence + ? new State( lastTimestamp.plusNanos( 245 ), nextSequence ) : new State( lastTimestamp, nextSequence ); } } @@ -77,7 +92,7 @@ private boolean lastTimestampEarlierThan(Instant now) { } private static long randomSequence() { - return Holder.numberGenerator.nextLong( MAX_RANDOM_SEQUENCE ); + return Holder.numberGenerator.nextLong( MAX_RANDOM_SEQUENCE + 1 ); } } @@ -118,7 +133,7 @@ public UUID generateUuid(final SharedSessionContractImplementor session) { | state.nanos() & 0xFFFL, // LSB bits 0-1 - variant = 4 0x8000_0000_0000_0000L - // LSB bits 2-15 - pseudorandom counter + // LSB bits 2-63 - pseudorandom counter | state.lastSequence ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 9f98cd259fca..0b7d8ed24318 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -7814,19 +7814,44 @@ public Predicate visitMemberOfPredicate(SqmMemberOfPredicate predicate) { this ) ); + subQuerySpec.applyPredicate( + new ComparisonPredicate( + toSingleExpression( subQuerySpec.getSelectClause().getSqlSelections(), lhs ), + ComparisonOperator.EQUAL, + lhs + ) + ); + subQuerySpec.getSelectClause().getSqlSelections().clear(); + subQuerySpec.getSelectClause().addSqlSelection( + new SqlSelectionImpl( new QueryLiteral<>( 1, basicType( Integer.class ) ) ) + ); } finally { popProcessingStateStack(); } - return new InSubQueryPredicate( - lhs, + return new ExistsPredicate( new SelectStatement( subQuerySpec ), predicate.isNegated(), getBooleanType() ); } + private Expression toSingleExpression(List sqlSelections, Expression inferenceSource) { + assert !sqlSelections.isEmpty(); + + if ( sqlSelections.size() == 1 ) { + return sqlSelections.get( 0 ).getExpression(); + } + else { + final var expressions = new ArrayList( sqlSelections.size() ); + for ( SqlSelection sqlSelection : sqlSelections ) { + expressions.add( sqlSelection.getExpression() ); + } + return new SqlTuple( expressions, (MappingModelExpressible) inferenceSource.getExpressionType() ); + } + } + @Override public NegatedPredicate visitNegatedPredicate(SqmNegatedPredicate predicate) { return new NegatedPredicate( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleBasicValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleBasicValuedModelPart.java index 40266a57e163..598022ff7f5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleBasicValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleBasicValuedModelPart.java @@ -65,7 +65,7 @@ public AnonymousTupleBasicValuedModelPart( new SelectableMappingImpl( "", selectionExpression, - new SelectablePath( partName ), + null, null, null, null, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEmbeddableValuedModelPart.java index 4e3e700380dd..c05647eb1f49 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEmbeddableValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tuple/internal/AnonymousTupleEmbeddableValuedModelPart.java @@ -128,7 +128,7 @@ private Map createModelParts( sqmExpressible, attributeType, sqlTypedMappings, - selectionIndex, + selectionIndex + index, selectionExpression + "_" + attribute.getName(), attribute.getName(), modelPartContainer.findSubPart( attribute.getName(), null ), diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java index 2df34e1b52f9..25a404c62ba1 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/AbstractSchemaValidator.java @@ -139,6 +139,7 @@ protected void validateTable( ); } validateColumnType( table, column, existingColumn, metadata, dialect ); + validateColumnNullability( table, column, existingColumn ); } } @@ -164,6 +165,22 @@ protected void validateColumnType( } } + private void validateColumnNullability(Table table, Column column, ColumnInformation existingColumn) { + if ( existingColumn.getNullable() == Boolean.FALSE ) { + // the existing schema column is defined as not-nullable + if ( column.isNullable() ) { + // but it is mapped in the model as nullable + throw new SchemaManagementException( + String.format( + "Schema validation: column defined as not-null in the database, but nullable in model - [%s] in table [%s]", + column.getName(), + table.getQualifiedTableName() + ) + ); + } + } + } + protected void validateSequence(Sequence sequence, SequenceInformation sequenceInformation) { if ( sequenceInformation == null ) { throw new SchemaManagementException( 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/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java index eceee2682a1b..73c8881f0379 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/AbstractJsonFormatMapper.java @@ -18,7 +18,7 @@ public abstract class AbstractJsonFormatMapper implements FormatMapper { @Override public final T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { final Type type = javaType.getJavaType(); - if ( type == String.class || type == Object.class ) { + if ( type == String.class ) { return (T) charSequence.toString(); } return fromString( charSequence, type ); @@ -27,7 +27,7 @@ public final T fromString(CharSequence charSequence, JavaType javaType, W @Override public final String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { final Type type = javaType.getJavaType(); - if ( type == String.class || type == Object.class ) { + if ( type == String.class ) { return (String) value; } return toString( value, type ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/internal/BindingTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/type/internal/BindingTypeHelper.java index 5f09df40d16a..a9ca925ca7bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/internal/BindingTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/internal/BindingTypeHelper.java @@ -97,10 +97,11 @@ public static JdbcMapping resolveBindType( final var javaType = value.getClass(); final var temporalJavaType = (TemporalJavaType) baseType.getJdbcJavaType(); final var bindableType = (BindableType) baseType; - return (JdbcMapping) switch ( temporalJavaType.getPrecision() ) { - case TIMESTAMP -> resolveTimestampTemporalTypeVariant( javaType, bindableType, typeConfiguration ); - case DATE -> resolveDateTemporalTypeVariant( javaType, bindableType, typeConfiguration ); - case TIME -> resolveTimeTemporalTypeVariant( javaType, bindableType, typeConfiguration ); + // Cast individual arms of the switch to avoid a JDK 17 javac bug + return switch ( temporalJavaType.getPrecision() ) { + case TIMESTAMP -> (JdbcMapping) resolveTimestampTemporalTypeVariant( javaType, bindableType, typeConfiguration ); + case DATE -> (JdbcMapping) resolveDateTemporalTypeVariant( javaType, bindableType, typeConfiguration ); + case TIME -> (JdbcMapping) resolveTimeTemporalTypeVariant( javaType, bindableType, typeConfiguration ); }; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableSameNestedNameSelectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableSameNestedNameSelectionTest.java new file mode 100644 index 000000000000..93e99dad023b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableSameNestedNameSelectionTest.java @@ -0,0 +1,222 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.embeddable; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + EmbeddableSameNestedNameSelectionTest.Material.class, + EmbeddableSameNestedNameSelectionTest.Weight.class, + EmbeddableSameNestedNameSelectionTest.Length.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19962") +@Jira("https://hibernate.atlassian.net/browse/HHH-19985") +public class EmbeddableSameNestedNameSelectionTest { + + @Test + void testPlainEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( "from Material", Material.class ) + .getSingleResult(); + assertThat( result.getWeight().getValue() ).isEqualTo( "10" ); + assertThat( result.getLength().getValue() ).isEqualTo( "2" ); + } ); + } + + @Test + void testSubqueryEmbedded(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select q.weight as weight, q.length as length from (select m.weight as weight, m.length as length from Material m) q", + Tuple.class + ).getSingleResult(); + assertEmbeddedTuple( result ); + } ); + } + + @Test + void testEmbedded(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select m.weight as weight, m.length as length from Material m", + Tuple.class + ).getSingleResult(); + assertEmbeddedTuple( result ); + } ); + } + + private void assertEmbeddedTuple(Tuple result) { + final var weight = result.get( "weight", Weight.class ); + assertThat( weight.getValue() ).isEqualTo( "10" ); + assertThat( weight.getUnit() ).isEqualTo( WeightUnit.KILOGRAM ); + final var length = result.get( "length", Length.class ); + assertThat( length.getValue() ).isEqualTo( "2" ); + assertThat( length.getUnit() ).isEqualTo( LengthUnit.METER ); + } + + @Test + void testSubqueryScalar(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select q.weight, q.weight_unit, q.length, q.length_unit from " + + "(select m.weight.value as weight, m.weight.unit as weight_unit, m.length.value as length, m.length.unit as length_unit from Material m) q", + Tuple.class + ).getSingleResult(); + assertScalarTuple( result ); + } ); + } + + @Test + void testScalar(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select m.weight.value, m.weight.unit, m.length.value, m.length.unit from Material m", + Tuple.class + ).getSingleResult(); + assertScalarTuple( result ); + } ); + } + + private void assertScalarTuple(Tuple result) { + assertThat( result.get( 0, String.class ) ).isEqualTo( "10" ); + assertThat( result.get( 1, WeightUnit.class ) ).isEqualTo( WeightUnit.KILOGRAM ); + assertThat( result.get( 2, String.class ) ).isEqualTo( "2" ); + assertThat( result.get( 3, LengthUnit.class ) ).isEqualTo( LengthUnit.METER ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( + new Material( 1L, new Weight( "10", WeightUnit.KILOGRAM ), new Length( "2", LengthUnit.METER ) ) + ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Material") + static class Material { + @Id + private Long id; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "weight_value")), + @AttributeOverride(name = "unit", column = @Column(name = "weight_unit")), + }) + private Weight weight; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "length_value")), + @AttributeOverride(name = "unit", column = @Column(name = "length_unit")), + }) + private Length length; + + public Material() { + } + + public Material(Long id, Weight weight, Length length) { + this.id = id; + this.weight = weight; + this.length = length; + } + + public Long getId() { + return id; + } + + public Weight getWeight() { + return weight; + } + + public void setWeight(Weight weight) { + this.weight = weight; + } + + public Length getLength() { + return length; + } + + public void setLength(Length length) { + this.length = length; + } + } + + @Embeddable + static class Weight { + private String value; + + private WeightUnit unit; + + public Weight() { + } + + public Weight(String value, WeightUnit unit) { + this.value = value; + this.unit = unit; + } + + public String getValue() { + return value; + } + + public WeightUnit getUnit() { + return unit; + } + } + + enum WeightUnit { + KILOGRAM, + POUND + } + + @Embeddable + static class Length { + private String value; + + private LengthUnit unit; + + public Length() { + } + + public Length(String value, LengthUnit unit) { + this.value = value; + this.unit = unit; + } + + public String getValue() { + return value; + } + + public LengthUnit getUnit() { + return unit; + } + } + + enum LengthUnit { + METER, + FOOT + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java index 87c1806eb87b..9946ef58303f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/SingleTableAndGenericsTest.java @@ -20,9 +20,12 @@ import jakarta.persistence.Inheritance; import jakarta.persistence.Table; +import java.util.Map; + import static jakarta.persistence.InheritanceType.SINGLE_TABLE; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hibernate.type.SqlTypes.JSON; +import static org.junit.jupiter.api.Assertions.assertTrue; @DomainModel( annotatedClasses = { @@ -37,7 +40,7 @@ public class SingleTableAndGenericsTest { @Test public void testIt(SessionFactoryScope scope) { - String payload = "{\"book\": \"1\"}"; + Map payload = Map.of("book", "1"); String aId = "1"; scope.inTransaction( @@ -53,9 +56,9 @@ public void testIt(SessionFactoryScope scope) { session -> { A a = session.find( A.class, aId ); assertThat( a ).isNotNull(); - String payload1 = a.getPayload(); + Map payload1 = a.getPayload(); assertThat( payload1 ).isNotNull(); - assertThat( payload1 ).contains( "book" ); + assertTrue(payload1.containsKey("book") ); } ); } @@ -93,6 +96,9 @@ public void setPayload(T payload) { @Entity(name = "C") @DiscriminatorValue("child") - public static class A extends B { + // Changed from to since the fix for HHH-19969; the restriction '|| type == Object.class' inside + // AbstractFormatMapper.fromString() was removed, so now no cast to String happens, but instead the json is serialized + // to either Map or List (depending on the json format) + public static class A extends B> { } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java index 146ccd40896d..05ed3ec5f9dc 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/ConnectionLockTimeoutTests.java @@ -19,6 +19,9 @@ import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; +import java.time.Duration; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; @@ -67,6 +70,61 @@ else if ( session.getDialect() instanceof GaussDBDialect ) { } ) ); } + /** + * Tests that lock_timeout values are correctly handled despite PostgreSQL + * canonical formatting (e.g., "60s" displayed as "1min"). + */ + @Test + void testCanonicalLockTimeoutFormat(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> session.doWork( (conn) -> { + final int expectedInitialValue; + if ( session.getDialect() instanceof MySQLDialect ) { + expectedInitialValue = 50; + } + else if ( session.getDialect() instanceof GaussDBDialect ) { + expectedInitialValue = 20 * 60 * 1000; + } + else { + expectedInitialValue = Timeouts.WAIT_FOREVER_MILLI; + } + + final LockingSupport lockingSupport = session.getDialect().getLockingSupport(); + final ConnectionLockTimeoutStrategy connectionStrategy = lockingSupport.getConnectionLockTimeoutStrategy(); + final Timeout initialLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() ); + assertThat( initialLockTimeout.milliseconds() ).isEqualTo( expectedInitialValue ); + + List durs = List.of( + Duration.ofMillis(1), + Duration.ofMillis(2), + Duration.ofMillis(999), + Duration.ofSeconds(1), + Duration.ofSeconds(2), + Duration.ofSeconds(59), + Duration.ofMinutes(1), + Duration.ofMinutes(2), + Duration.ofMinutes(59), + Duration.ofHours(1), + Duration.ofHours(2), + Duration.ofHours(23), + Duration.ofDays(1) + ); + + try { + for ( Duration dur : durs ) { + int timeoutInMillis = (int) dur.toMillis(); + Timeout timeout = Timeout.milliseconds( timeoutInMillis ); + connectionStrategy.setLockTimeout( timeout, conn, session.getFactory() ); + + Timeout adjustedLockTimeout = connectionStrategy.getLockTimeout( conn, session.getFactory() ); + assertThat( adjustedLockTimeout.milliseconds() ).isEqualTo( timeoutInMillis ); + } + } + finally { + connectionStrategy.setLockTimeout( Timeout.milliseconds( expectedInitialValue ), conn, session.getFactory() ); + } + } ) ); + } + @Test void testSkipLocked(SessionFactoryScope factoryScope) { // this is never supported 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-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java index 7f5ecf8be859..a6440785f92c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java @@ -11,6 +11,7 @@ import jakarta.persistence.Table; import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Root; +import org.assertj.core.api.AssertionsForClassTypes; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; @@ -42,8 +43,12 @@ import java.nio.charset.StandardCharsets; import java.sql.Blob; import java.sql.Clob; +import java.util.ArrayDeque; +import java.util.Dictionary; +import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; @@ -53,12 +58,14 @@ import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.hibernate.type.SqlTypes.JSON; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Christian Beikov * @author Yanming Zhou */ -@DomainModel(annotatedClasses = JsonMappingTests.EntityWithJson.class) +@DomainModel(annotatedClasses = {JsonMappingTests.EntityWithJson.class, JsonMappingTests.EntityWithObjectJson.class}) @SessionFactory public abstract class JsonMappingTests { @@ -76,6 +83,75 @@ public static class Jackson extends JsonMappingTests { public Jackson() { super( false ); } + + @Test + @JiraKey( "https://hibernate.atlassian.net/browse/HHH-19969" ) + public void jsonMappedToObjectTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var entity = new EntityWithObjectJson(); + entity.id = 1L; + entity.json = Map.of("a", 1, "b", 2); + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 2L; + entity.json = List.of("c", 11, 22, "d"); + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 3L; + entity.json = Set.of("s1", 2, "s3"); + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 4L; + Queue ad = new ArrayDeque<>(); + ad.add(2); + ad.add(1); + ad.add(3); + entity.json = ad; + session.persist(entity); + + entity = new EntityWithObjectJson(); + entity.id = 5L; + Dictionary ht = new Hashtable<>(); + ht.put(1, "one"); + ht.put(2, "two"); + ht.put(3, "three"); + entity.json = ht; + session.persist(entity); + } + ); + scope.inTransaction( + session -> { + var entity = session.find( EntityWithObjectJson.class, 1L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( Map.class ); + assertEquals( 2, ((Map)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 2L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( List.class ); + assertEquals( 4, ((List)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 3L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( List.class ); + assertEquals( 3, ((List)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 4L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( List.class ); + assertEquals( 3, ((List)entity.json).size() ); + + entity = session.find( EntityWithObjectJson.class, 5L ); + AssertionsForClassTypes.assertThat( entity ).isNotNull(); + AssertionsForClassTypes.assertThat( entity.json ).isInstanceOf( Map.class ); + assertEquals( 3, ((Map)entity.json).size() ); + } + ); + } } private final Map stringMap; @@ -228,15 +304,13 @@ public void verifyComparisonWorks(SessionFactoryScope scope) { .get( 0 ); final String jsonText; try { - if ( nativeJson instanceof Blob ) { - final Blob blob = (Blob) nativeJson; + if ( nativeJson instanceof Blob blob ) { jsonText = new String( blob.getBytes( 1L, (int) blob.length() ), StandardCharsets.UTF_8 ); } - else if ( nativeJson instanceof Clob ) { - final Clob jsonClob = (Clob) nativeJson; + else if ( nativeJson instanceof Clob jsonClob ) { jsonText = jsonClob.getSubString( 1L, (int) jsonClob.length() ); } else { @@ -363,4 +437,21 @@ public int hashCode() { return string != null ? string.hashCode() : 0; } } + + @Entity + public static class EntityWithObjectJson { + @Id + long id; + + @JdbcTypeCode(JSON) + Object json; + + public EntityWithObjectJson() { + } + + public EntityWithObjectJson(long id, Object json) { + this.id = id; + this.json = json; + } + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/SimpleXmlOverriddenTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/SimpleXmlOverriddenTest.java index f2461f5480f8..6b8f27602f26 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/SimpleXmlOverriddenTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/SimpleXmlOverriddenTest.java @@ -20,6 +20,7 @@ import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.StringJavaType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.junit.jupiter.api.Test; @@ -66,10 +67,27 @@ public void testDefinitionAtAttributeLevel(ServiceRegistryScope scope) { PersistentClass pc = metadata.getEntityBinding( TheEntity.class.getName() ); BasicType type = (BasicType) pc.getProperty( "it" ).getType(); + assertTyping( BasicTypeImpl.class, type ); // Should not be ConvertedBasicTypeImpl in particular assertTyping( StringJavaType.class, type.getJavaTypeDescriptor() ); assertTyping( jdbcTypeRegistry.getDescriptor( Types.VARCHAR ).getClass(), type.getJdbcType() ); } + /** + * A baseline test, with an explicit @Convert annotation at entity level that should be in effect + */ + @Test + public void baselineAtEntityLevel(ServiceRegistryScope scope) { + Metadata metadata = new MetadataSources( scope.getRegistry() ) + .addAnnotatedClass( TheEntity2.class ) + .buildMetadata(); + + PersistentClass pc = metadata.getEntityBinding( TheEntity2.class.getName() ); + Type type = pc.getProperty( "it" ).getType(); + ConvertedBasicTypeImpl adapter = assertTyping( ConvertedBasicTypeImpl.class, type ); + final JpaAttributeConverter converter = (JpaAttributeConverter) adapter.getValueConverter(); + assertTrue( SillyStringConverter.class.isAssignableFrom( converter.getConverterJavaType().getJavaTypeClass() ) ); + } + /** * Test outcome of applying overrides via orm.xml, specifically at the entity level */ @@ -85,6 +103,7 @@ public void testDefinitionAtEntityLevel(ServiceRegistryScope scope) { PersistentClass pc = metadata.getEntityBinding( TheEntity2.class.getName() ); BasicType type = (BasicType) pc.getProperty( "it" ).getType(); + assertTyping( BasicTypeImpl.class, type ); // Should not be ConvertedBasicTypeImpl in particular assertTyping( StringJavaType.class, type.getJavaTypeDescriptor() ); assertTyping( jdbcTypeRegistry.getDescriptor( Types.VARCHAR ).getClass(), type.getJdbcType() ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/mutable/MutableNaturalIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/mutable/MutableNaturalIdTest.java index 36a5cf0671dd..0bca803ce384 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/mutable/MutableNaturalIdTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/mutable/MutableNaturalIdTest.java @@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; /** * @author Gavin King @@ -58,7 +59,7 @@ public void testNaturalIdNullability(SessionFactoryScope scope) { @AfterEach public void dropTestData(SessionFactoryScope scope) { - scope.getSessionFactory().getSchemaManager().truncate(); + scope.dropData(); } @Test @@ -362,4 +363,42 @@ public void testEviction(SessionFactoryScope scope) { } ); } + + @Test + @JiraKey("HHH-7287") + public void testModificationInOtherSession(SessionFactoryScope factoryScope) { + var id = factoryScope.fromTransaction( (session) -> { + User u = new User( "gavin", "hb", "secret" ); + session.persist( u ); + return u.getId(); + } ); + + // Use transactionless session + factoryScope.inSession( (session) -> { + // this loads the state into this `session` + var byNaturalId = session.byNaturalId( User.class ).using( "name", "gavin" ).using( "org", "hb" ).load(); + assertNotNull( byNaturalId ); + + // CHANGE natural-id values in another session + factoryScope.inTransaction( (otherSession) -> { + var u = otherSession.find( User.class, id ); + u.setOrg( "zz" ); + } ); + // CHANGE APPLIED + + byNaturalId = session.byNaturalId( User.class ) + .using( "name", "gavin" ) + .using( "org", "hb" ).load(); + assertNotNull( byNaturalId ); + + // the internal query will 'see' the new values, because isolation level < SERIALIZABLE + var byNaturalId2 = session.byNaturalId( User.class ) + .using( "name", "gavin" ) + .using( "org", "zz" ).load(); + assertSame( byNaturalId, byNaturalId2 ); + + // this fails, that's the bug + assertNotNull( session.byNaturalId( User.class ).using( "name", "gavin" ).using( "org", "hb" ).load()); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schematools/PrimaryKeyColumnOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schematools/PrimaryKeyColumnOrderTest.java index 3d1483dd2d7b..b4291d23f880 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schematools/PrimaryKeyColumnOrderTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schematools/PrimaryKeyColumnOrderTest.java @@ -73,7 +73,9 @@ public void setUp(SessionFactoryScope scope) { @AfterEach public void tearDown(SessionFactoryScope scope) { - scope.dropData(); + scope.inTransaction( session -> + session.createNativeQuery( "drop table TEST_ENTITY " ).executeUpdate() + ); } @Test 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 ); diff --git a/migration-guide.adoc b/migration-guide.adoc index 8e5c7d93d5f4..9088e77bbc14 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -54,10 +54,12 @@ This section describes changes to contracts (classes, interfaces, methods, etc.) The method `contains(String,Object)` of `Session` was deprecated. Use `contains(Object)` instead. -[[noInterceptor]] -== SharedSessionBuilder.noInterceptor() -The behavior of `SharedSessionBuilder.noInterceptor()` was changed to reflect its documented semantics. +[[byMultipleIds]] +=== Session.byMultipleIds() deprecated + +`byMultipleIds()` of `Session` and `MultiIdentifierLoadAccess` have been deprecated in favor of `Session#findMultiple` + [[jpa]] === @Jpa (hibernate-testing)