From 95a31e6b28f301d1559c535fc17ee9419cbc9dc9 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 10 Dec 2025 17:25:30 +0100 Subject: [PATCH 01/13] Use DateJavaType instead of JdbcXxxxJavaTypes to represent fields of type Date This fixes bugs in the metamodel where getJavaType() would return the wrong class even in the Persistence-standard metamodel. It's also a first step to fixing a bunch of other unsound things we do in the codebase. --- .../internal/InferredBasicValueResolver.java | 3 +- .../domain/internal/AbstractAttribute.java | 7 +- .../tree/select/SqmDynamicInstantiation.java | 24 +- .../DynamicInstantiationResultImpl.java | 14 +- .../hibernate/type/StandardBasicTypes.java | 65 +++++- .../java/AbstractTemporalJavaType.java | 2 +- .../descriptor/java/CalendarDateJavaType.java | 3 +- .../descriptor/java/CalendarTimeJavaType.java | 4 +- .../type/descriptor/java/DateJavaType.java | 211 +++++++++++------- .../descriptor/java/JdbcDateJavaType.java | 81 ++++--- .../descriptor/java/JdbcTimeJavaType.java | 83 +++---- .../java/JdbcTimestampJavaType.java | 117 +++++----- .../descriptor/jdbc/JdbcTypeIndicators.java | 4 +- .../hibernate/type/spi/TypeConfiguration.java | 4 +- 14 files changed, 371 insertions(+), 251 deletions(-) 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..029d94e4b75e 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 @@ -537,7 +537,7 @@ public static BasicValue.Resolution fromTemporal( ? reflectedJtd.resolveTypeForPrecision( requestedTemporalPrecision, typeConfiguration ) // Avoid using the DateJavaType and prefer the JdbcTimestampJavaType : reflectedJtd.resolveTypeForPrecision( reflectedJtd.getPrecision(), typeConfiguration ); - final BasicType jdbcMapping = basicTypeRegistry.resolve( temporalJavaType, explicitJdbcType ); + final var jdbcMapping = basicTypeRegistry.resolve( temporalJavaType, explicitJdbcType ); return new InferredBasicValueResolution<>( jdbcMapping, temporalJavaType, @@ -564,7 +564,6 @@ public static BasicValue.Resolution fromTemporal( } else { basicType = basicTypeRegistry.resolve( - // Avoid using the DateJavaType and prefer the JdbcTimestampJavaType reflectedJtd.resolveTypeForPrecision( reflectedJtd.getPrecision(), typeConfiguration ), reflectedJtd.getRecommendedJdbcType( stdIndicators ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java index 22b999de5b4c..55c45407641a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java @@ -63,8 +63,11 @@ public String getName() { @Override public Class getJavaType() { - return valueType instanceof BasicTypeImpl basicType - ? basicType.getJavaType() + // TODO: create a new method to abstract this logic + return valueType instanceof BasicTypeImpl basicType + // handles primitives in basic types + ? (Class) basicType.getJavaType() + // good for everything else : attributeJtd.getJavaTypeClass(); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmDynamicInstantiation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmDynamicInstantiation.java index 13b1675ae760..c8e79168c930 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmDynamicInstantiation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmDynamicInstantiation.java @@ -5,7 +5,6 @@ package org.hibernate.query.sqm.tree.select; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -23,11 +22,15 @@ import org.hibernate.query.sqm.tree.domain.SqmDomainType; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.jpa.AbstractJpaSelection; +import org.hibernate.type.descriptor.java.DateJavaType; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.TemporalJavaType; import org.hibernate.type.spi.TypeConfiguration; import org.jboss.logging.Logger; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.query.sqm.DynamicInstantiationNature.CLASS; @@ -157,7 +160,7 @@ public boolean checkInstantiation(TypeConfiguration typeConfiguration) { if ( isConstructorCompatible( javaType, argTypes, typeConfiguration ) ) { return true; } - final List> arguments = getArguments(); + final var arguments = getArguments(); final List aliases = new ArrayList<>( arguments.size() ); for ( var argument : arguments ) { final String alias = argument.getAlias(); @@ -182,9 +185,18 @@ private List> argumentTypes() { return getArguments().stream() .map( arg -> { final var expressible = arg.getExpressible(); - return expressible != null && expressible.getExpressibleJavaType() != null ? - expressible.getExpressibleJavaType().getJavaTypeClass() : - Void.class; + if ( expressible != null ) { + final var expressibleJavaType = expressible.getExpressibleJavaType(); + if ( expressibleJavaType != null ) { + return expressibleJavaType instanceof DateJavaType temporalJavaType + // Hack to accommodate a constructor with java.sql parameter + // types when the entity has java.util.Date as its field types. + // (This was requested in HHH-4179 and we fixed it by accident.) + ? TemporalJavaType.resolveJavaTypeClass( temporalJavaType.getPrecision() ) + : expressibleJavaType.getJavaTypeClass(); + } + } + return Void.class; } ).collect( toList() ); } @@ -227,7 +239,7 @@ public SqmDynamicInstantiationTarget getInstantiationTarget() { } public List> getArguments() { - return arguments == null ? Collections.emptyList() : Collections.unmodifiableList( arguments ); + return arguments == null ? emptyList() : unmodifiableList( arguments ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/instantiation/internal/DynamicInstantiationResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/instantiation/internal/DynamicInstantiationResultImpl.java index 58ac3dd808e1..58294dad65ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/instantiation/internal/DynamicInstantiationResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/instantiation/internal/DynamicInstantiationResultImpl.java @@ -17,8 +17,10 @@ import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.InitializerParent; import org.hibernate.sql.results.graph.instantiation.DynamicInstantiationResult; +import org.hibernate.type.descriptor.java.DateJavaType; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.TemporalJavaType; import org.jboss.logging.Logger; import static java.util.stream.Collectors.toList; @@ -157,7 +159,7 @@ private DomainResultAssembler assembler( final var constructor = findMatchingConstructor( javaType.getJavaTypeClass(), argumentReaders.stream() - .map( reader -> reader.getAssembledJavaType().getJavaTypeClass() ) + .map( reader -> argumentClass( reader ) ) .collect( toList() ), creationState.getSqlAstCreationContext() .getMappingMetamodel() @@ -194,6 +196,16 @@ private DomainResultAssembler assembler( return new DynamicInstantiationAssemblerInjectionImpl<>( javaType, argumentReaders ); } + private static Class argumentClass(ArgumentReader reader) { + final var assembledJavaType = reader.getAssembledJavaType(); + return assembledJavaType instanceof DateJavaType temporalJavaType + // Hack to accommodate a constructor with java.sql parameter + // types when the entity has java.util.Date as its field types. + // (This was requested in HHH-4179 and we fixed it by accident.) + ? TemporalJavaType.resolveJavaTypeClass( temporalJavaType.getPrecision() ) + : assembledJavaType.getJavaTypeClass(); + } + private List signature() { return argumentResults.stream() .map( adt -> adt.getResultJavaType().getTypeName() ) 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..cc138c1c5077 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java @@ -11,8 +11,6 @@ import java.sql.Blob; import java.sql.Clob; import java.sql.NClob; -import java.sql.Time; -import java.sql.Timestamp; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; @@ -494,7 +492,7 @@ private StandardBasicTypes() { */ public static final BasicTypeReference TIME = new BasicTypeReference<>( "time", - Time.class, + java.util.Date.class, SqlTypes.TIME ); @@ -504,7 +502,7 @@ private StandardBasicTypes() { */ public static final BasicTypeReference DATE = new BasicTypeReference<>( "date", - java.sql.Date.class, + java.util.Date.class, SqlTypes.DATE ); @@ -514,7 +512,37 @@ private StandardBasicTypes() { */ public static final BasicTypeReference TIMESTAMP = new BasicTypeReference<>( "timestamp", - Timestamp.class, + java.util.Date.class, + SqlTypes.TIMESTAMP + ); + + /** + * The standard Hibernate type for mapping {@link java.sql.Time} to JDBC + * {@link org.hibernate.type.SqlTypes#TIMESTAMP TIMESTAMP}. + */ + public static final BasicTypeReference SQL_TIME = new BasicTypeReference<>( + "sql_time", + java.sql.Time.class, + SqlTypes.TIME + ); + + /** + * The standard Hibernate type for mapping {@link java.sql.Date} to JDBC + * {@link org.hibernate.type.SqlTypes#DATE DATE}. + */ + public static final BasicTypeReference SQL_DATE = new BasicTypeReference<>( + "sql_date", + java.sql.Date.class, + SqlTypes.DATE + ); + + /** + * The standard Hibernate type for mapping {@link java.sql.Timestamp} to JDBC + * {@link org.hibernate.type.SqlTypes#TIMESTAMP TIMESTAMP}. + */ + public static final BasicTypeReference SQL_TIMESTAMP = new BasicTypeReference<>( + "sql_timestamp", + java.sql.Timestamp.class, SqlTypes.TIMESTAMP ); @@ -1174,21 +1202,42 @@ public static void prime(TypeConfiguration typeConfiguration) { DATE, "org.hibernate.type.DateType", basicTypeRegistry, - "date", java.sql.Date.class.getName() + "date" ); handle( TIME, "org.hibernate.type.TimeType", basicTypeRegistry, - "time", java.sql.Time.class.getName() + "time" ); handle( TIMESTAMP, "org.hibernate.type.TimestampType", basicTypeRegistry, - "timestamp", java.sql.Timestamp.class.getName(), Date.class.getName() + "timestamp", Date.class.getName() + ); + + handle( + SQL_DATE, + null, + basicTypeRegistry, + "sql_date", java.sql.Date.class.getName() + ); + + handle( + SQL_TIME, + null, + basicTypeRegistry, + "sql_time", java.sql.Time.class.getName() + ); + + handle( + SQL_TIMESTAMP, + null, + basicTypeRegistry, + "sql_timestamp", java.sql.Timestamp.class.getName() ); handle( diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractTemporalJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractTemporalJavaType.java index d1a0079326ce..914eb3df9d2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractTemporalJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractTemporalJavaType.java @@ -33,7 +33,7 @@ public AbstractTemporalJavaType( } @Override - public final TemporalJavaType resolveTypeForPrecision( + public TemporalJavaType resolveTypeForPrecision( TemporalType precision, TypeConfiguration typeConfiguration) { if ( precision == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java index 336041f99ba0..7e807a15dbbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java @@ -56,7 +56,8 @@ protected TemporalJavaType forTimePrecision(TypeConfiguration typeConfigu } public String toString(Calendar value) { - return JdbcDateJavaType.INSTANCE.toString( value.getTime() ); + return JdbcDateJavaType.INSTANCE.toString( + new java.sql.Date( value.getTime().getTime() ) ); } public Calendar fromString(CharSequence string) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarTimeJavaType.java index e544c30f2e2c..5c7acd663c3a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarTimeJavaType.java @@ -4,6 +4,7 @@ */ package org.hibernate.type.descriptor.java; +import java.sql.Time; import java.sql.Types; import java.util.Calendar; import java.util.Date; @@ -56,7 +57,8 @@ protected TemporalJavaType forDatePrecision(TypeConfiguration typeConfigu } public String toString(Calendar value) { - return JdbcTimeJavaType.INSTANCE.toString( value.getTime() ); + return JdbcTimeJavaType.INSTANCE.toString( + new Time( value.getTime().getTime() ) ); } public Calendar fromString(CharSequence string) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/DateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/DateJavaType.java index bed568b2643b..a8b2b9eccf8d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/DateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/DateJavaType.java @@ -4,18 +4,14 @@ */ package org.hibernate.type.descriptor.java; +import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; -import java.time.format.DateTimeParseException; -import java.time.temporal.ChronoField; -import java.time.temporal.TemporalAccessor; import java.util.Calendar; import java.util.Date; -import java.util.GregorianCalendar; import jakarta.persistence.TemporalType; -import org.hibernate.HibernateException; import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.descriptor.WrapperOptions; @@ -23,6 +19,8 @@ import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.type.descriptor.java.JdbcDateJavaType.toDateEpoch; + /** * Descriptor for {@link Date} handling. * @@ -30,17 +28,45 @@ */ public class DateJavaType extends AbstractTemporalJavaType implements VersionJavaType { public static final DateJavaType INSTANCE = new DateJavaType(); + private final TemporalType precision; public static class DateMutabilityPlan extends MutableMutabilityPlan { - public static final DateMutabilityPlan INSTANCE = new DateMutabilityPlan(); + public static final DateMutabilityPlan INSTANCE = new DateMutabilityPlan( TemporalType.TIMESTAMP ); + private final TemporalType precision; + + public DateMutabilityPlan(TemporalType precision) { + this.precision = precision; + } + @Override public Date deepCopyNotNull(Date value) { - return new Date( value.getTime() ); + if ( value instanceof java.sql.Timestamp timestamp ) { + return JdbcTimestampJavaType.TimestampMutabilityPlan.INSTANCE.deepCopyNotNull( timestamp ); + } + else if ( value instanceof java.sql.Date date ) { + return JdbcDateJavaType.DateMutabilityPlan.INSTANCE.deepCopyNotNull( date ); + } + else if ( value instanceof java.sql.Time time ) { + return JdbcTimeJavaType.TimeMutabilityPlan.INSTANCE.deepCopyNotNull( time ); + } + else { + return switch ( precision ) { + case TIMESTAMP -> wrapSqlTimestamp( value ); + case DATE -> wrapSqlDate( value ); + case TIME -> wrapSqlTime( value ); + }; + } } } public DateJavaType() { super( Date.class, DateMutabilityPlan.INSTANCE ); + this.precision = TemporalType.TIMESTAMP; + } + + private DateJavaType(TemporalType precision) { + super( Date.class, new DateMutabilityPlan(precision) ); + this.precision = precision; } @Override @@ -55,18 +81,30 @@ public Date cast(Object value) { @Override public TemporalType getPrecision() { - return TemporalType.TIMESTAMP; + return precision; + } + + @Override + public TemporalJavaType resolveTypeForPrecision(TemporalType precision, TypeConfiguration typeConfiguration) { + return (TemporalJavaType) new DateJavaType( precision ); } @Override public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) { - // this "Date" is really a timestamp - return dialect.getDefaultTimestampPrecision(); + return switch ( precision ) { + case TIMESTAMP -> JdbcTimestampJavaType.INSTANCE.getDefaultSqlPrecision( dialect, jdbcType ); + case DATE -> JdbcDateJavaType.INSTANCE.getDefaultSqlPrecision( dialect, jdbcType ); + case TIME -> JdbcTimeJavaType.INSTANCE.getDefaultSqlPrecision( dialect, jdbcType ); + }; } @Override public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { - return context.getJdbcType( Types.TIMESTAMP ); + return context.getJdbcType( switch ( precision ) { + case TIMESTAMP -> Types.TIMESTAMP; + case DATE -> Types.DATE; + case TIME -> Types.TIME; + } ); } @Override @SuppressWarnings("unchecked") @@ -86,21 +124,31 @@ protected TemporalJavaType forTimePrecision(TypeConfiguration typeConfigu @Override public String toString(Date value) { - return JdbcTimestampJavaType.LITERAL_FORMATTER.format( value.toInstant() ); +// return JdbcTimestampJavaType.LITERAL_FORMATTER.format( value.toInstant() ); + return switch ( precision ) { + case TIMESTAMP -> JdbcTimestampJavaType.INSTANCE.toString( wrapSqlTimestamp( value ) ); + case DATE -> JdbcDateJavaType.INSTANCE.toString( wrapSqlDate( value ) ); + case TIME -> JdbcTimeJavaType.INSTANCE.toString( wrapSqlTime( value ) ); + }; } @Override public Date fromString(CharSequence string) { - try { - final TemporalAccessor accessor = JdbcTimestampJavaType.LITERAL_FORMATTER.parse( string ); - return new Date( - accessor.getLong( ChronoField.INSTANT_SECONDS ) * 1000L - + accessor.get( ChronoField.NANO_OF_SECOND ) / 1_000_000 - ); - } - catch ( DateTimeParseException pe) { - throw new HibernateException( "could not parse timestamp string" + string, pe ); - } + return switch ( precision ) { + case TIMESTAMP -> JdbcTimestampJavaType.INSTANCE.fromString( string ); + case DATE -> JdbcDateJavaType.INSTANCE.fromString( string ); + case TIME -> JdbcTimeJavaType.INSTANCE.fromString( string ); + }; +// try { +// final var accessor = JdbcTimestampJavaType.LITERAL_FORMATTER.parse( string ); +// return new Date( +// accessor.getLong( ChronoField.INSTANT_SECONDS ) * 1000L +// + accessor.get( ChronoField.NANO_OF_SECOND ) / 1_000_000 +// ); +// } +// catch ( DateTimeParseException pe) { +// throw new HibernateException( "could not parse timestamp string" + string, pe ); +// } } @Override @@ -108,78 +156,67 @@ public boolean areEqual(Date one, Date another) { if ( one == another) { return true; } - return !( one == null || another == null ) && one.getTime() == another.getTime(); - + return one != null && another != null + && switch ( precision ) { + case DATE -> JdbcDateJavaType.INSTANCE.areEqual( wrapSqlDate( one ), wrapSqlDate( another ) ); + case TIME -> JdbcTimeJavaType.INSTANCE.areEqual( wrapSqlTime( one ), wrapSqlTime( another ) ); + case TIMESTAMP -> + one instanceof Timestamp timestamp && another instanceof Timestamp anotherTimestamp + ? JdbcTimestampJavaType.INSTANCE.areEqual( timestamp, anotherTimestamp ) + : one.getTime() == another.getTime(); + }; } @Override public int extractHashCode(Date value) { - Calendar calendar = Calendar.getInstance(); + var calendar = Calendar.getInstance(); calendar.setTime( value ); - return CalendarJavaType.INSTANCE.extractHashCode( calendar ); + int hashCode = 1; + if ( precision == TemporalType.TIMESTAMP ) { + hashCode = 31 * hashCode + calendar.get(Calendar.MILLISECOND); + } + if ( precision != TemporalType.DATE ) { + hashCode = 31 * hashCode + calendar.get(Calendar.SECOND); + hashCode = 31 * hashCode + calendar.get(Calendar.MINUTE); + hashCode = 31 * hashCode + calendar.get(Calendar.HOUR_OF_DAY); + } + if ( precision != TemporalType.TIME ) { + hashCode = 31 * hashCode + calendar.get(Calendar.DAY_OF_MONTH); + hashCode = 31 * hashCode + calendar.get(Calendar.MONTH); + hashCode = 31 * hashCode + calendar.get(Calendar.YEAR); + } + return hashCode; } @Override public X unwrap(Date value, Class type, WrapperOptions options) { - if ( value == null ) { - return null; - } - if ( java.sql.Date.class.isAssignableFrom( type ) ) { - final java.sql.Date rtn = value instanceof java.sql.Date - ? ( java.sql.Date ) value - : new java.sql.Date( value.getTime() ); - return type.cast( rtn ); - } - if ( java.sql.Time.class.isAssignableFrom( type ) ) { - final java.sql.Time rtn = value instanceof java.sql.Time - ? ( java.sql.Time ) value - : new java.sql.Time( value.getTime() % 86_400_000 ); - return type.cast( rtn ); - } - if ( java.sql.Timestamp.class.isAssignableFrom( type ) ) { - final java.sql.Timestamp rtn = value instanceof Timestamp - ? ( java.sql.Timestamp ) value - : new java.sql.Timestamp( value.getTime() ); - return type.cast( rtn ); - } - if ( Date.class.isAssignableFrom( type ) ) { - return type.cast( value ); - } - if ( Calendar.class.isAssignableFrom( type ) ) { - final GregorianCalendar cal = new GregorianCalendar(); - cal.setTimeInMillis( value.getTime() ); - return type.cast( cal ); - } - if ( Long.class.isAssignableFrom( type ) ) { - return type.cast( value.getTime() ); - } - throw unknownUnwrap( type ); + return switch ( precision ) { + case TIMESTAMP -> JdbcTimestampJavaType.INSTANCE.unwrap( wrapSqlTimestamp( value ), type, options ); + case DATE -> JdbcDateJavaType.INSTANCE.unwrap( wrapSqlDate( value ), type, options ); + case TIME -> JdbcTimeJavaType.INSTANCE.unwrap( wrapSqlTime( value ), type, options ); + }; } + @Override public Date wrap(X value, WrapperOptions options) { - if ( value == null ) { - return null; - } - if (value instanceof Date date) { - return date; - } - - if (value instanceof Long longValue) { - return new Date( longValue ); - } - - if (value instanceof Calendar calendar) { - return new Date( calendar.getTimeInMillis() ); - } + return switch ( precision ) { + case TIMESTAMP -> JdbcTimestampJavaType.INSTANCE.wrap( value, options ); + case DATE -> JdbcDateJavaType.INSTANCE.wrap( value, options ); + case TIME -> JdbcTimeJavaType.INSTANCE.wrap( value, options ); + }; + } - throw unknownWrap( value.getClass() ); + @Override + public Object coerce(Object value) { + return wrap( value, null ); } @Override public boolean isWider(JavaType javaType) { - return switch ( javaType.getTypeName() ) { - case "java.sql.Date", "java.sql.Timestamp", "java.util.Calendar" -> true; - default -> false; + return switch ( precision ) { + case TIMESTAMP -> JdbcTimestampJavaType.INSTANCE.isWider( javaType ); + case DATE -> JdbcDateJavaType.INSTANCE.isWider( javaType ); + case TIME -> JdbcTimeJavaType.INSTANCE.isWider( javaType ); }; } @@ -199,4 +236,24 @@ public Date seed( Integer precision, Integer scale, SharedSessionContractImplementor session) { return Timestamp.from( ClockHelper.forPrecision( precision, session ).instant() ); } + + static Timestamp wrapSqlTimestamp(Date date) { + return date instanceof Timestamp timestamp ? timestamp : new Timestamp( date.getTime() ); + } + + static Time wrapSqlTime(Date date) { + return date instanceof Time time ? time : new Time( date.getTime() % 86_400_000 ); + } + + static java.sql.Date wrapSqlDate(java.util.Date value) { + if ( value instanceof java.sql.Date date ) { + final long millis = date.getTime(); + final long dateEpoch = toDateEpoch( millis ); + return dateEpoch == millis ? date : new java.sql.Date( dateEpoch ); + } + else { + return new java.sql.Date( toDateEpoch( value ) ); + } + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java index 9258833af50a..d74bff5d8a6f 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java @@ -12,7 +12,7 @@ import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; import java.util.Calendar; -import java.util.Date; +import java.sql.Date; import java.util.GregorianCalendar; import org.hibernate.HibernateException; @@ -62,15 +62,13 @@ public TemporalType getPrecision() { @Override public Class getJavaType() { - // wrong, but needed for backward compatibility - //noinspection unchecked, rawtypes - return (Class) java.sql.Date.class; + return java.sql.Date.class; } @Override public boolean isInstance(Object value) { // this check holds true for java.sql.Date as well - return value instanceof Date + return value instanceof java.util.Date && !( value instanceof java.sql.Time ); } @@ -130,7 +128,7 @@ public X unwrap(Date value, Class type, WrapperOptions options) { } if ( java.sql.Date.class.isAssignableFrom( type ) ) { - return type.cast( unwrapSqlDate( value ) ); + return type.cast( value ); } if ( java.util.Date.class.isAssignableFrom( type ) ) { @@ -138,7 +136,7 @@ public X unwrap(Date value, Class type, WrapperOptions options) { } if ( Long.class.isAssignableFrom( type ) ) { - return type.cast( unwrapDateEpoch( value ) ); + return type.cast( toDateEpoch( value ) ); } if ( String.class.isAssignableFrom( type ) ) { @@ -147,49 +145,25 @@ public X unwrap(Date value, Class type, WrapperOptions options) { if ( Calendar.class.isAssignableFrom( type ) ) { final var gregorianCalendar = new GregorianCalendar(); - gregorianCalendar.setTimeInMillis( unwrapDateEpoch( value ) ); + gregorianCalendar.setTimeInMillis( toDateEpoch( value ) ); return type.cast( gregorianCalendar ); } if ( java.sql.Timestamp.class.isAssignableFrom( type ) ) { - return type.cast( new java.sql.Timestamp( unwrapDateEpoch( value ) ) ); + return type.cast( new java.sql.Timestamp( toDateEpoch( value ) ) ); } if ( java.sql.Time.class.isAssignableFrom( type ) ) { - throw new IllegalArgumentException( "Illegal attempt to treat `java.sql.Date` as `java.sql.Time`" ); + throw new IllegalArgumentException( "Illegal attempt to treat 'java.sql.Date' as 'java.sql.Time'" ); } throw unknownUnwrap( type ); } - private LocalDate unwrapLocalDate(Date value) { + private LocalDate unwrapLocalDate(java.util.Date value) { return value instanceof java.sql.Date date ? date.toLocalDate() - : new java.sql.Date( unwrapDateEpoch( value ) ).toLocalDate(); - } - - private java.sql.Date unwrapSqlDate(Date value) { - if ( value instanceof java.sql.Date date ) { - final long dateEpoch = toDateEpoch( date.getTime() ); - return dateEpoch == date.getTime() ? date : new java.sql.Date( dateEpoch ); - } - else { - return new java.sql.Date( unwrapDateEpoch( value ) ); - } - } - - private static long unwrapDateEpoch(Date value) { - return toDateEpoch( value.getTime() ); - } - - private static long toDateEpoch(long value) { - final var calendar = Calendar.getInstance(); - calendar.setTimeInMillis( value ); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.clear(Calendar.MINUTE); - calendar.clear(Calendar.SECOND); - calendar.clear(Calendar.MILLISECOND); - return calendar.getTimeInMillis(); + : new java.sql.Date( toDateEpoch( value ) ).toLocalDate(); } @Override @@ -210,8 +184,8 @@ public Date wrap(Object value, WrapperOptions options) { return new java.sql.Date( toDateEpoch( calendar.getTimeInMillis() ) ); } - if ( value instanceof Date date ) { - return unwrapSqlDate( date ); + if ( value instanceof java.util.Date date ) { + return wrapSqlDate( date ); } if ( value instanceof LocalDate localDate ) { @@ -221,7 +195,32 @@ public Date wrap(Object value, WrapperOptions options) { throw unknownWrap( value.getClass() ); } - private static TemporalAccessor fromDate(Date value) { + static java.sql.Date wrapSqlDate(java.util.Date value) { + if ( value instanceof java.sql.Date date ) { + final long millis = date.getTime(); + final long dateEpoch = toDateEpoch( millis ); + return dateEpoch == millis ? date : new java.sql.Date( dateEpoch ); + } + else { + return new java.sql.Date( toDateEpoch( value ) ); + } + } + + static long toDateEpoch(java.util.Date value) { + return toDateEpoch( value.getTime() ); + } + + static long toDateEpoch(long value) { + final var calendar = Calendar.getInstance(); + calendar.setTimeInMillis( value ); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.clear(Calendar.MINUTE); + calendar.clear(Calendar.SECOND); + calendar.clear(Calendar.MILLISECOND); + return calendar.getTimeInMillis(); + } + + private static TemporalAccessor fromDate(java.util.Date value) { return value instanceof java.sql.Date date ? date.toLocalDate() : LocalDate.ofInstant( value.toInstant(), ZoneOffset.systemDefault() ); @@ -275,9 +274,7 @@ public static class DateMutabilityPlan extends MutableMutabilityPlan { @Override public Date deepCopyNotNull(Date value) { - return value instanceof java.sql.Date - ? value - : new java.sql.Date( value.getTime() ); + return new java.sql.Date( value.getTime() ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimeJavaType.java index af0af15fe7f0..b94a20f2c845 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimeJavaType.java @@ -37,7 +37,7 @@ * to {@link Time} values. This capability is shared with * {@link JdbcDateJavaType} and {@link JdbcTimestampJavaType}. */ -public class JdbcTimeJavaType extends AbstractTemporalJavaType { +public class JdbcTimeJavaType extends AbstractTemporalJavaType