diff --git a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/TimeZoneStorageMappingTests.java b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/TimeZoneStorageMappingTests.java index 5834f6d12b..2c9bcf23ee 100644 --- a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/TimeZoneStorageMappingTests.java +++ b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/TimeZoneStorageMappingTests.java @@ -8,7 +8,9 @@ package org.hibernate.userguide.mapping.basic; import java.time.Duration; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -18,6 +20,7 @@ import org.hibernate.annotations.TimeZoneColumn; import org.hibernate.annotations.TimeZoneStorage; import org.hibernate.annotations.TimeZoneStorageType; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.H2Dialect; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; @@ -47,6 +50,15 @@ import static org.hamcrest.MatcherAssert.assertThat; @ServiceRegistry(settings = @Setting( name = AvailableSettings.TIMEZONE_DEFAULT_STORAGE, value = "AUTO")) public class TimeZoneStorageMappingTests { + private static final ZoneOffset JVM_TIMEZONE_OFFSET = OffsetDateTime.now().getOffset(); + private static final OffsetTime OFFSET_TIME = OffsetTime.of( + LocalTime.of( + 12, + 0, + 0 + ), + ZoneOffset.ofHoursMinutes( 5, 45 ) + ); private static final OffsetDateTime OFFSET_DATE_TIME = OffsetDateTime.of( LocalDateTime.of( 2022, @@ -69,11 +81,12 @@ public class TimeZoneStorageMappingTests { ), ZoneOffset.ofHoursMinutes( 5, 45 ) ); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern( "HH:mm:ssxxx" ); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern( "dd/MM/yyyy 'at' HH:mm:ssxxx" ); @BeforeEach public void setup(SessionFactoryScope scope) { - scope.inTransaction( s -> s.persist( new TimeZoneStorageEntity( 1, OFFSET_DATE_TIME, ZONED_DATE_TIME ) ) ); + scope.inTransaction( s -> s.persist( new TimeZoneStorageEntity( 1, OFFSET_TIME, OFFSET_DATE_TIME, ZONED_DATE_TIME ) ) ); } @AfterEach @@ -109,26 +122,40 @@ public class TimeZoneStorageMappingTests { session -> { List resultList = session.createQuery( "select " + + "e.offsetTime" + suffix + ", " + "e.offsetDateTime" + suffix + ", " + "e.zonedDateTime" + suffix + ", " + + "extract(offset from e.offsetTime" + suffix + "), " + "extract(offset from e.offsetDateTime" + suffix + "), " + "extract(offset from e.zonedDateTime" + suffix + "), " + + "e.offsetTime" + suffix + " + 1 hour, " + "e.offsetDateTime" + suffix + " + 1 hour, " + "e.zonedDateTime" + suffix + " + 1 hour, " + + "e.offsetTime" + suffix + " + 1 hour - e.offsetTime" + suffix + ", " + "e.offsetDateTime" + suffix + " + 1 hour - e.offsetDateTime" + suffix + ", " + "e.zonedDateTime" + suffix + " + 1 hour - e.zonedDateTime" + suffix + ", " + "1 from TimeZoneStorageEntity e " + "where e.offsetDateTime" + suffix + " = e.offsetDateTime" + suffix, Tuple.class ).getResultList(); - assertThat( resultList.get( 0 ).get( 0, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME ) ); - assertThat( resultList.get( 0 ).get( 1, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME ) ); - assertThat( resultList.get( 0 ).get( 2, ZoneOffset.class ), Matchers.is( OFFSET_DATE_TIME.getOffset() ) ); - assertThat( resultList.get( 0 ).get( 3, ZoneOffset.class ), Matchers.is( ZONED_DATE_TIME.getOffset() ) ); - assertThat( resultList.get( 0 ).get( 4, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME.plusHours( 1L ) ) ); - assertThat( resultList.get( 0 ).get( 5, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME.plusHours( 1L ) ) ); - assertThat( resultList.get( 0 ).get( 6, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) ); - assertThat( resultList.get( 0 ).get( 7, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) ); + assertThat( resultList.get( 0 ).get( 0, OffsetTime.class ), Matchers.is( OFFSET_TIME ) ); + assertThat( resultList.get( 0 ).get( 1, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME ) ); + assertThat( resultList.get( 0 ).get( 2, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME ) ); + if ( !( scope.getSessionFactory().getJdbcServices().getDialect() instanceof H2Dialect) ) { + // H2 bug: https://github.com/h2database/h2database/issues/3757 + assertThat( + resultList.get( 0 ).get( 3, ZoneOffset.class ), + Matchers.is( OFFSET_TIME.getOffset() ) + ); + } + assertThat( resultList.get( 0 ).get( 4, ZoneOffset.class ), Matchers.is( OFFSET_DATE_TIME.getOffset() ) ); + assertThat( resultList.get( 0 ).get( 5, ZoneOffset.class ), Matchers.is( ZONED_DATE_TIME.getOffset() ) ); + assertThat( resultList.get( 0 ).get( 6, OffsetTime.class ), Matchers.is( OFFSET_TIME.plusHours( 1L ) ) ); + assertThat( resultList.get( 0 ).get( 7, OffsetDateTime.class ), Matchers.is( OFFSET_DATE_TIME.plusHours( 1L ) ) ); + assertThat( resultList.get( 0 ).get( 8, ZonedDateTime.class ), Matchers.is( ZONED_DATE_TIME.plusHours( 1L ) ) ); + assertThat( resultList.get( 0 ).get( 9, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) ); + assertThat( resultList.get( 0 ).get( 10, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) ); + assertThat( resultList.get( 0 ).get( 11, Duration.class ), Matchers.is( Duration.ofHours( 1L ) ) ); } ); } @@ -138,14 +165,22 @@ public class TimeZoneStorageMappingTests { session -> { List resultList = session.createQuery( "select " + + "format(e.offsetTime" + suffix + " as 'HH:mm:ssxxx'), " + "format(e.offsetDateTime" + suffix + " as 'dd/MM/yyyy ''at'' HH:mm:ssxxx'), " + "format(e.zonedDateTime" + suffix + " as 'dd/MM/yyyy ''at'' HH:mm:ssxxx'), " + "1 from TimeZoneStorageEntity e " + "where e.offsetDateTime" + suffix + " = e.offsetDateTime" + suffix, Tuple.class ).getResultList(); - assertThat( resultList.get( 0 ).get( 0, String.class ), Matchers.is( FORMATTER.format( OFFSET_DATE_TIME ) ) ); - assertThat( resultList.get( 0 ).get( 1, String.class ), Matchers.is( FORMATTER.format( ZONED_DATE_TIME ) ) ); + if ( !( scope.getSessionFactory().getJdbcServices().getDialect() instanceof H2Dialect) ) { + // H2 bug: https://github.com/h2database/h2database/issues/3757 + assertThat( + resultList.get( 0 ).get( 0, String.class ), + Matchers.is( TIME_FORMATTER.format( OFFSET_TIME ) ) + ); + } + assertThat( resultList.get( 0 ).get( 1, String.class ), Matchers.is( FORMATTER.format( OFFSET_DATE_TIME ) ) ); + assertThat( resultList.get( 0 ).get( 2, String.class ), Matchers.is( FORMATTER.format( ZONED_DATE_TIME ) ) ); } ); } @@ -155,29 +190,48 @@ public class TimeZoneStorageMappingTests { scope.inSession( session -> { List resultList = session.createQuery( - "select e.offsetDateTimeNormalized, e.zonedDateTimeNormalized, e.offsetDateTimeNormalizedUtc, e.zonedDateTimeNormalizedUtc from TimeZoneStorageEntity e", + "select " + + "e.offsetTimeNormalized, " + + "e.offsetDateTimeNormalized, " + + "e.zonedDateTimeNormalized, " + + "e.offsetTimeNormalizedUtc, " + + "e.offsetDateTimeNormalizedUtc, " + + "e.zonedDateTimeNormalizedUtc " + + "from TimeZoneStorageEntity e", Tuple.class ).getResultList(); - assertThat( resultList.get( 0 ).get( 0, OffsetDateTime.class ).toInstant(), Matchers.is( OFFSET_DATE_TIME.toInstant() ) ); - assertThat( resultList.get( 0 ).get( 1, ZonedDateTime.class ).toInstant(), Matchers.is( ZONED_DATE_TIME.toInstant() ) ); - assertThat( resultList.get( 0 ).get( 2, OffsetDateTime.class ).toInstant(), Matchers.is( OFFSET_DATE_TIME.toInstant() ) ); - assertThat( resultList.get( 0 ).get( 3, ZonedDateTime.class ).toInstant(), Matchers.is( ZONED_DATE_TIME.toInstant() ) ); + assertThat( resultList.get( 0 ).get( 0, OffsetTime.class ).toLocalTime(), Matchers.is( OFFSET_TIME.withOffsetSameInstant( JVM_TIMEZONE_OFFSET ).toLocalTime() ) ); + assertThat( resultList.get( 0 ).get( 0, OffsetTime.class ).getOffset(), Matchers.is( JVM_TIMEZONE_OFFSET ) ); + assertThat( resultList.get( 0 ).get( 1, OffsetDateTime.class ).toInstant(), Matchers.is( OFFSET_DATE_TIME.toInstant() ) ); + assertThat( resultList.get( 0 ).get( 2, ZonedDateTime.class ).toInstant(), Matchers.is( ZONED_DATE_TIME.toInstant() ) ); + assertThat( resultList.get( 0 ).get( 3, OffsetTime.class ).toLocalTime(), Matchers.is( OFFSET_TIME.withOffsetSameInstant( ZoneOffset.UTC ).toLocalTime() ) ); + assertThat( resultList.get( 0 ).get( 3, OffsetTime.class ).getOffset(), Matchers.is( ZoneOffset.UTC ) ); + assertThat( resultList.get( 0 ).get( 4, OffsetDateTime.class ).toInstant(), Matchers.is( OFFSET_DATE_TIME.toInstant() ) ); + assertThat( resultList.get( 0 ).get( 5, ZonedDateTime.class ).toInstant(), Matchers.is( ZONED_DATE_TIME.toInstant() ) ); } ); } @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFormat.class) - @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTimezoneTypes.class) + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTimezoneTypes.class, comment = "Extracting the offset usually only makes sense if the temporal retains the offset. On DBs that have native TZ support we test this anyway to make sure it's not broken'") public void testNormalizeOffset(SessionFactoryScope scope) { scope.inSession( session -> { List resultList = session.createQuery( - "select extract(offset from e.offsetDateTimeNormalizedUtc), extract(offset from e.zonedDateTimeNormalizedUtc) from TimeZoneStorageEntity e", + "select " + + "extract(offset from e.offsetTimeNormalizedUtc), " + + "extract(offset from e.offsetDateTimeNormalizedUtc), " + + "extract(offset from e.zonedDateTimeNormalizedUtc) " + + "from TimeZoneStorageEntity e", Tuple.class ).getResultList(); - assertThat( resultList.get( 0 ).get( 0, ZoneOffset.class ), Matchers.is( ZoneOffset.systemDefault().getRules().getOffset( OFFSET_DATE_TIME.toInstant() ) ) ); + if ( !( scope.getSessionFactory().getJdbcServices().getDialect() instanceof H2Dialect) ) { + // H2 bug: https://github.com/h2database/h2database/issues/3757 + assertThat( resultList.get( 0 ).get( 0, ZoneOffset.class ), Matchers.is( ZoneOffset.UTC ) ); + } assertThat( resultList.get( 0 ).get( 1, ZoneOffset.class ), Matchers.is( ZoneOffset.UTC ) ); + assertThat( resultList.get( 0 ).get( 2, ZoneOffset.class ), Matchers.is( ZoneOffset.UTC ) ); } ); } @@ -189,6 +243,11 @@ public class TimeZoneStorageMappingTests { private Integer id; //tag::time-zone-column-examples-mapping-example[] + @TimeZoneStorage(TimeZoneStorageType.COLUMN) + @TimeZoneColumn(name = "birthtime_offset_offset") + @Column(name = "birthtime_offset") + private OffsetTime offsetTimeColumn; + @TimeZoneStorage(TimeZoneStorageType.COLUMN) @TimeZoneColumn(name = "birthday_offset_offset") @Column(name = "birthday_offset") @@ -200,6 +259,10 @@ public class TimeZoneStorageMappingTests { private ZonedDateTime zonedDateTimeColumn; //end::time-zone-column-examples-mapping-example[] + @TimeZoneStorage + @Column(name = "birthtime_offset_auto") + private OffsetTime offsetTimeAuto; + @TimeZoneStorage @Column(name = "birthday_offset_auto") private OffsetDateTime offsetDateTimeAuto; @@ -208,6 +271,10 @@ public class TimeZoneStorageMappingTests { @Column(name = "birthday_zoned_auto") private ZonedDateTime zonedDateTimeAuto; + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE) + @Column(name = "birthtime_offset_normalized") + private OffsetTime offsetTimeNormalized; + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE) @Column(name = "birthday_offset_normalized") private OffsetDateTime offsetDateTimeNormalized; @@ -216,6 +283,10 @@ public class TimeZoneStorageMappingTests { @Column(name = "birthday_zoned_normalized") private ZonedDateTime zonedDateTimeNormalized; + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @Column(name = "birthtime_offset_normalized_utc") + private OffsetTime offsetTimeNormalizedUtc; + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) @Column(name = "birthday_offset_normalized_utc") private OffsetDateTime offsetDateTimeNormalizedUtc; @@ -227,14 +298,18 @@ public class TimeZoneStorageMappingTests { public TimeZoneStorageEntity() { } - public TimeZoneStorageEntity(Integer id, OffsetDateTime offsetDateTime, ZonedDateTime zonedDateTime) { + public TimeZoneStorageEntity(Integer id, OffsetTime offsetTime, OffsetDateTime offsetDateTime, ZonedDateTime zonedDateTime) { this.id = id; + this.offsetTimeColumn = offsetTime; this.offsetDateTimeColumn = offsetDateTime; this.zonedDateTimeColumn = zonedDateTime; + this.offsetTimeAuto = offsetTime; this.offsetDateTimeAuto = offsetDateTime; this.zonedDateTimeAuto = zonedDateTime; + this.offsetTimeNormalized = offsetTime; this.offsetDateTimeNormalized = offsetDateTime; this.zonedDateTimeNormalized = zonedDateTime; + this.offsetTimeNormalizedUtc = offsetTime; this.offsetDateTimeNormalizedUtc = offsetDateTime; this.zonedDateTimeNormalizedUtc = zonedDateTime; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java index 9dc7a943df..3fbf29131c 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java @@ -54,6 +54,7 @@ import static org.hibernate.type.SqlTypes.BLOB; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; @@ -83,6 +84,7 @@ public class CUBRIDDialect extends Dialect { //(always 3, millisecond precision) case TIMESTAMP: return "datetime"; + case TIME_WITH_TIMEZONE: case TIMESTAMP_WITH_TIMEZONE: return "datetimetz"; default: diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 1b0cb8c089..b0bfd8d03f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -72,7 +72,7 @@ import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantAsTimestampWithTimeZoneJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsOffsetDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @@ -195,6 +195,10 @@ public class CockroachLegacyDialect extends Dialect { case BLOB: return "bytes"; + // We do not use the time with timezone type because PG deprecated it and it lacks certain operations like subtraction +// case TIME_UTC: +// return columnType( TIME_WITH_TIMEZONE ); + case TIMESTAMP_UTC: return columnType( TIMESTAMP_WITH_TIMEZONE ); @@ -269,6 +273,12 @@ public class CockroachLegacyDialect extends Dialect { break; } break; + case TIME: + // The PostgreSQL JDBC driver reports TIME for timetz, but we use it only for mapping OffsetTime to UTC + if ( "timetz".equals( columnTypeName ) ) { + jdbcTypeCode = TIME_UTC; + } + break; case TIMESTAMP: // The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant if ( "timestamptz".equals( columnTypeName ) ) { @@ -324,7 +334,7 @@ public class CockroachLegacyDialect extends Dialect { protected void contributeCockroachTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); - jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, InstantAsTimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, TimestampUtcAsOffsetDateTimeJdbcType.INSTANCE ); if ( driverKind == PostgreSQLDriverKind.PG_JDBC ) { jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); if ( PgJdbcHelper.isUsable( serviceRegistry ) ) { @@ -423,7 +433,13 @@ public class CockroachLegacyDialect extends Dialect { functionContributions.getFunctionRegistry().register( "format", - new FormatFunction( "experimental_strftime", functionContributions.getTypeConfiguration() ) + new FormatFunction( + "experimental_strftime", + false, + true, + false, + functionContributions.getTypeConfiguration() + ) ); functionFactory.windowFunctions(); functionFactory.listagg_stringAgg( "string" ); @@ -757,22 +773,30 @@ public class CockroachLegacyDialect extends Dialect { if ( unit == null ) { return "(?3-?2)"; } - switch (unit) { - case YEAR: - return "(extract(year from ?3)-extract(year from ?2))"; - case QUARTER: - return "(extract(year from ?3)*4-extract(year from ?2)*4+extract(month from ?3)//3-extract(month from ?2)//3)"; - case MONTH: - return "(extract(year from ?3)*12-extract(year from ?2)*12+extract(month from ?3)-extract(month from ?2))"; - } - if ( toTemporalType != TemporalType.TIMESTAMP && fromTemporalType != TemporalType.TIMESTAMP ) { + if ( toTemporalType == TemporalType.DATE && fromTemporalType == TemporalType.DATE ) { // special case: subtraction of two dates // results in an integer number of days // instead of an INTERVAL - return "(?3-?2)" + DAY.conversionFactor( unit, this ); + switch ( unit ) { + case YEAR: + case MONTH: + case QUARTER: + // age only supports timestamptz, so we have to cast the date expressions + return "extract(" + translateDurationField( unit ) + " from age(cast(?3 as timestamptz),cast(?2 as timestamptz)))"; + default: + return "(?3-?2)" + DAY.conversionFactor( unit, this ); + } } else { switch (unit) { + case YEAR: + return "extract(year from ?3-?2)"; + case QUARTER: + return "(extract(year from ?3-?2)*4+extract(month from ?3-?2)//3)"; + case MONTH: + return "(extract(year from ?3-?2)*12+extract(month from ?3-?2))"; + // Prior to v20, Cockroach didn't support extracting from an interval/duration, + // so we use the extract_duration function case WEEK: return "extract_duration(hour from ?3-?2)/168"; case DAY: diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index e5743e81c1..97a56512ba 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -11,7 +11,11 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; import java.util.List; +import java.util.TimeZone; import org.hibernate.LockOptions; import org.hibernate.boot.model.FunctionContributions; @@ -96,11 +100,16 @@ import static org.hibernate.type.SqlTypes.CLOB; import static org.hibernate.type.SqlTypes.DECIMAL; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.SQLXML; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; /** * A {@linkplain Dialect SQL dialect} for DB2. @@ -187,6 +196,7 @@ public class DB2LegacyDialect extends Dialect { return "clob"; case TIMESTAMP_WITH_TIMEZONE: return "timestamp($p)"; + case TIME: case TIME_WITH_TIMEZONE: return "time"; case BINARY: @@ -416,9 +426,37 @@ public class DB2LegacyDialect extends Dialect { if ( getDB2Version().isBefore( 11 ) ) { return DB2Dialect.timestampdiffPatternV10( unit, fromTemporalType, toTemporalType ); } - StringBuilder pattern = new StringBuilder(); - boolean castFrom = fromTemporalType != TemporalType.TIMESTAMP && !unit.isDateUnit(); - boolean castTo = toTemporalType != TemporalType.TIMESTAMP && !unit.isDateUnit(); + final StringBuilder pattern = new StringBuilder(); + final String fromExpression; + final String toExpression; + if ( unit.isDateUnit() ) { + fromExpression = "?2"; + toExpression = "?3"; + } + else { + switch ( fromTemporalType ) { + case DATE: + fromExpression = "cast(?2 as timestamp)"; + break; + case TIME: + fromExpression = "timestamp('1970-01-01',?2)"; + break; + default: + fromExpression = "?2"; + break; + } + switch ( toTemporalType ) { + case DATE: + toExpression = "cast(?3 as timestamp)"; + break; + case TIME: + toExpression = "timestamp('1970-01-01',?3)"; + break; + default: + toExpression = "?3"; + break; + } + } switch ( unit ) { case NATIVE: case NANOSECOND: @@ -434,26 +472,24 @@ public class DB2LegacyDialect extends Dialect { default: pattern.append( "?1s_between(" ); } - if ( castTo ) { - pattern.append( "cast(?3 as timestamp)" ); - } - else { - pattern.append( "?3" ); - } + pattern.append( toExpression ); pattern.append( ',' ); - if ( castFrom ) { - pattern.append( "cast(?2 as timestamp)" ); - } - else { - pattern.append( "?2" ); - } + pattern.append( fromExpression ); pattern.append( ')' ); switch ( unit ) { case NATIVE: - pattern.append( "+(microsecond(?3)-microsecond(?2))/1e6)" ); + pattern.append( "+(microsecond("); + pattern.append( toExpression ); + pattern.append(")-microsecond("); + pattern.append( fromExpression ); + pattern.append("))/1e6)" ); break; case NANOSECOND: - pattern.append( "*1e9+(microsecond(?3)-microsecond(?2))*1e3)" ); + pattern.append( "*1e9+(microsecond("); + pattern.append( toExpression ); + pattern.append(")-microsecond("); + pattern.append( fromExpression ); + pattern.append("))*1e3)" ); break; case MONTH: pattern.append( ')' ); @@ -468,19 +504,24 @@ public class DB2LegacyDialect extends Dialect { @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { final StringBuilder pattern = new StringBuilder(); - final boolean castTo; + final String timestampExpression; if ( unit.isDateUnit() ) { - castTo = temporalType == TemporalType.TIME; + if ( temporalType == TemporalType.TIME ) { + timestampExpression = "timestamp('1970-01-01',?3)"; + } + else { + timestampExpression = "?3"; + } } else { - castTo = temporalType == TemporalType.DATE; - } - if (castTo) { - pattern.append("cast(?3 as timestamp)"); - } - else { - pattern.append("?3"); + if ( temporalType == TemporalType.DATE ) { + timestampExpression = "cast(?3 as timestamp)"; + } + else { + timestampExpression = "?3"; + } } + pattern.append(timestampExpression); pattern.append("+("); // DB2 supports temporal arithmetic. See https://www.ibm.com/support/knowledgecenter/en/SSEPGG_9.7.0/com.ibm.db2.luw.sql.ref.doc/doc/r0023457.html switch (unit) { @@ -503,6 +544,83 @@ public class DB2LegacyDialect extends Dialect { return pattern.toString(); } + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsLocalTime( appender, temporalAccessor ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanos( appender, temporalAccessor, false, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDate( appender, date ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsLocalTime( appender, date ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanos( appender, date, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + Calendar calendar, + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDate( appender, calendar ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsLocalTime( appender, calendar ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + @Override public String getLowercaseFunction() { return getDB2Version().isBefore( 9, 7 ) ? "lcase" : super.getLowercaseFunction(); @@ -894,6 +1012,12 @@ public class DB2LegacyDialect extends Dialect { return "dayofweek(?2)"; case QUARTER: return "quarter(?2)"; + case EPOCH: + if ( getDB2Version().isBefore( 11 ) ) { + return timestampdiffPattern( TemporalUnit.SECOND, TemporalType.TIMESTAMP, TemporalType.TIMESTAMP ) + .replace( "?2", "'1970-01-01 00:00:00'" ) + .replace( "?3", "?2" ); + } } return super.extractPattern( unit ); } @@ -939,4 +1063,20 @@ public class DB2LegacyDialect extends Dialect { public String getCreateUserDefinedTypeExtensionsString() { return " instantiable mode db2sql"; } + + /** + * The more "standard" syntax is {@code rid_bit(alias)} but here we use {@code alias.rowid}. + *

+ * There is also an alternative {@code rid()} of type {@code bigint}, but it cannot be used + * with partitioning. + */ + @Override + public String rowId(String rowId) { + return "rowid"; + } + + @Override + public int rowIdSqlType() { + return VARBINARY; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java index 9a805b1d88..8adadb9aba 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java @@ -37,7 +37,9 @@ import jakarta.persistence.TemporalType; import java.util.List; +import static org.hibernate.type.SqlTypes.ROWID; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; /** * An SQL dialect for DB2 for z/OS, previously known as known as Db2 UDB for z/OS and Db2 UDB for z/OS and OS/390. @@ -74,9 +76,13 @@ public class DB2zLegacyDialect extends DB2LegacyDialect { @Override protected String columnType(int sqlTypeCode) { - if ( sqlTypeCode == TIMESTAMP_WITH_TIMEZONE && getVersion().isAfter( 10 ) ) { - // See https://www.ibm.com/support/knowledgecenter/SSEPEK_10.0.0/wnew/src/tpc/db2z_10_timestamptimezone.html - return "timestamp with time zone"; + if ( getVersion().isAfter( 10 ) ) { + switch ( sqlTypeCode ) { + case TIME_WITH_TIMEZONE: + case TIMESTAMP_WITH_TIMEZONE: + // See https://www.ibm.com/support/knowledgecenter/SSEPEK_10.0.0/wnew/src/tpc/db2z_10_timestamptimezone.html + return "timestamp with time zone"; + } } return super.columnType( sqlTypeCode ); } @@ -160,14 +166,7 @@ public class DB2zLegacyDialect extends DB2LegacyDialect { @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { - StringBuilder pattern = new StringBuilder(); - final boolean castTo; - if ( unit.isDateUnit() ) { - castTo = temporalType == TemporalType.TIME; - } - else { - castTo = temporalType == TemporalType.DATE; - } + final StringBuilder pattern = new StringBuilder(); pattern.append("add_"); switch (unit) { case NATIVE: @@ -185,12 +184,24 @@ public class DB2zLegacyDialect extends DB2LegacyDialect { pattern.append("?1"); } pattern.append("s("); - if (castTo) { - pattern.append("cast(?3 as timestamp)"); + final String timestampExpression; + if ( unit.isDateUnit() ) { + if ( temporalType == TemporalType.TIME ) { + timestampExpression = "timestamp('1970-01-01',?3)"; + } + else { + timestampExpression = "?3"; + } } else { - pattern.append("?3"); + if ( temporalType == TemporalType.DATE ) { + timestampExpression = "cast(?3 as timestamp)"; + } + else { + timestampExpression = "?3"; + } } + pattern.append(timestampExpression); pattern.append(","); switch (unit) { case NANOSECOND: @@ -219,4 +230,23 @@ public class DB2zLegacyDialect extends DB2LegacyDialect { } }; } + + // I speculate that this is a correct implementation of rowids for DB2 for z/OS, + // just on the basis of the DB2 docs, but I currently have no way to test it + // Note that the implementation inherited from DB2Dialect for LUW will not work! + + @Override + public String rowId(String rowId) { + return rowId.isEmpty() ? "rowid_" : rowId; + } + + @Override + public int rowIdSqlType() { + return ROWID; + } + + @Override + public String getRowIdColumnString(String rowId) { + return rowId( rowId ) + " rowid not null generated always"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java index 1cf8dc2406..034b7f1be9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java @@ -92,8 +92,10 @@ import static org.hibernate.type.SqlTypes.NCHAR; import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; @@ -162,6 +164,10 @@ public class DerbyLegacyDialect extends Dialect { case NCLOB: return "clob"; + case TIME: + case TIME_WITH_TIMEZONE: + return "time"; + case TIMESTAMP: case TIMESTAMP_WITH_TIMEZONE: return "timestamp"; @@ -383,6 +389,8 @@ public class DerbyLegacyDialect extends Dialect { return "(({fn timestampdiff(sql_tsi_day,date(char(year(?2),4)||'-01-01'),{fn timestampadd(sql_tsi_day,{fn timestampdiff(sql_tsi_day,{d '1753-01-01'},?2)}/7*7,{d '1753-01-04'})})}+7)/7)"; case QUARTER: return "((month(?2)+2)/3)"; + case EPOCH: + return "{fn timestampdiff(sql_tsi_second,{ts '1970-01-01 00:00:00'},?2)}"; default: return "?1(?2)"; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java index 26cdce8790..eec4b49a84 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java @@ -150,7 +150,7 @@ public class FirebirdDialect extends Dialect { case TIMESTAMP: return "timestamp"; case TIME_WITH_TIMEZONE: - return getVersion().isBefore( 4, 0 ) ? "time" : super.columnType( sqlTypeCode ); + return getVersion().isBefore( 4, 0 ) ? "time" : "time with time zone"; case TIMESTAMP_WITH_TIMEZONE: return getVersion().isBefore( 4, 0 ) ? "timestamp" : "timestamp with time zone"; case BINARY: diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 6b7eaa08c0..e0a0e37ada 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -76,8 +76,11 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLe import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.TimeUtcAsJdbcTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsInstantJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampWithTimeZoneJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -88,6 +91,7 @@ import jakarta.persistence.TemporalType; import static org.hibernate.query.sqm.TemporalUnit.SECOND; import static org.hibernate.type.SqlTypes.ARRAY; +import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.CHAR; import static org.hibernate.type.SqlTypes.DECIMAL; @@ -104,6 +108,8 @@ import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.OTHER; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; @@ -112,7 +118,6 @@ import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; /** * A legacy {@linkplain Dialect SQL dialect} for H2. @@ -208,6 +213,9 @@ public class H2LegacyDialect extends Dialect { // which caused problems for schema update tool case NUMERIC: return getVersion().isBefore( 2 ) ? columnType( DECIMAL ) : super.columnType( sqlTypeCode ); + // Support was only added in 2.0 + case TIME_WITH_TIMEZONE: + return getVersion().isBefore( 2 ) ? columnType( TIMESTAMP_WITH_TIMEZONE ) : super.columnType( sqlTypeCode ); case NCHAR: return columnType( CHAR ); case NVARCHAR: @@ -263,7 +271,15 @@ public class H2LegacyDialect extends Dialect { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); - jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, InstantJdbcType.INSTANCE ); + if ( getVersion().isBefore( 2 ) ) { + // Support for TIME_WITH_TIMEZONE was only added in 2.0 + jdbcTypeRegistry.addDescriptor( TIME_WITH_TIMEZONE, TimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimeUtcAsJdbcTimeJdbcType.INSTANCE ); + } + else { + jdbcTypeRegistry.addDescriptor( TimeUtcAsOffsetTimeJdbcType.INSTANCE ); + } + jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, TimestampUtcAsInstantJdbcType.INSTANCE ); if ( getVersion().isSameOrAfter( 1, 4, 197 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); } @@ -878,4 +894,14 @@ public class H2LegacyDialect extends Dialect { public UniqueDelegate getUniqueDelegate() { return uniqueDelegate; } + + @Override + public String rowId(String rowId) { + return "_rowid_"; + } + + @Override + public int rowIdSqlType() { + return BIGINT; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 615634a848..cdbe977fad 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -11,7 +11,10 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; import java.util.Locale; +import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -135,6 +138,7 @@ import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; /** * A {@linkplain Dialect SQL dialect} for Oracle 8i and above. @@ -153,6 +157,13 @@ public class OracleLegacyDialect extends Dialect { public static final String PREFER_LONG_RAW = "hibernate.dialect.oracle.prefer_long_raw"; + private static final String yqmSelect = + "( TRUNC(%2$s, 'MONTH') + NUMTOYMINTERVAL(%1$s, 'MONTH') + ( LEAST( EXTRACT( DAY FROM %2$s ), EXTRACT( DAY FROM LAST_DAY( TRUNC(%2$s, 'MONTH') + NUMTOYMINTERVAL(%1$s, 'MONTH') ) ) ) - 1 ) )"; + + private static final String ADD_YEAR_EXPRESSION = String.format( yqmSelect, "?2*12", "?3" ); + private static final String ADD_QUARTER_EXPRESSION = String.format( yqmSelect, "?2*3", "?3" ); + private static final String ADD_MONTH_EXPRESSION = String.format( yqmSelect, "?2", "?3" ); + private final LimitHandler limitHandler = supportsFetchClause( FetchClauseType.ROWS_ONLY ) ? Oracle12LimitHandler.INSTANCE : new LegacyOracleLimitHandler( getVersion() ); @@ -316,6 +327,11 @@ public class OracleLegacyDialect extends Dialect { return getVersion().isBefore( 9 ) ? currentTimestamp() : "current_timestamp"; } + @Override + public boolean supportsInsertReturningGeneratedKeys() { + return getVersion().isSameOrAfter( 12 ); + } + /** * Oracle doesn't have any sort of {@link Types#BOOLEAN} @@ -448,6 +464,8 @@ public class OracleLegacyDialect extends Dialect { return "to_number(to_char(?2,'MI'))"; case SECOND: return "to_number(to_char(?2,'SS'))"; + case EPOCH: + return "trunc((cast(?2 at time zone 'UTC' as date) - date '1970-1-1')*86400)"; default: return super.extractPattern(unit); } @@ -455,150 +473,119 @@ public class OracleLegacyDialect extends Dialect { @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + StringBuilder pattern = new StringBuilder(); - pattern.append("(?3+"); switch ( unit ) { case YEAR: + pattern.append( ADD_YEAR_EXPRESSION ); + break; case QUARTER: + pattern.append( ADD_QUARTER_EXPRESSION ); + break; case MONTH: - pattern.append("numtoyminterval"); + pattern.append( ADD_MONTH_EXPRESSION ); break; case WEEK: + pattern.append("(?3+numtodsinterval((?2)*7,'day'))"); + break; case DAY: case HOUR: case MINUTE: case SECOND: + pattern.append("(?3+numtodsinterval(?2,'?1'))"); + break; case NANOSECOND: + pattern.append("(?3+numtodsinterval((?2)/1e9,'second'))"); + break; case NATIVE: - pattern.append("numtodsinterval"); + pattern.append("(?3+numtodsinterval(?2,'second'))"); break; default: throw new SemanticException(unit + " is not a legal field"); } - pattern.append("("); - switch ( unit ) { - case NANOSECOND: - case QUARTER: - case WEEK: - pattern.append("("); - break; - } - pattern.append("?2"); - switch ( unit ) { - case QUARTER: - pattern.append(")*3"); - break; - case WEEK: - pattern.append(")*7"); - break; - case NANOSECOND: - pattern.append(")/1e9"); - break; - } - pattern.append(",'"); - switch ( unit ) { - case QUARTER: - pattern.append("month"); - break; - case WEEK: - pattern.append("day"); - break; - case NANOSECOND: - case NATIVE: - pattern.append("second"); - break; - default: - pattern.append("?1"); - } - pattern.append("')"); - pattern.append(")"); return pattern.toString(); } @Override - public String timestampdiffPattern( - TemporalUnit unit, - TemporalType fromTemporalType, TemporalType toTemporalType) { - StringBuilder pattern = new StringBuilder(); - boolean timestamp = toTemporalType == TemporalType.TIMESTAMP || fromTemporalType == TemporalType.TIMESTAMP; - switch (unit) { + public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { + final StringBuilder pattern = new StringBuilder(); + final boolean hasTimePart = toTemporalType != TemporalType.DATE || fromTemporalType != TemporalType.DATE; + switch ( unit ) { case YEAR: - extractField(pattern, YEAR, unit); + extractField( pattern, YEAR, unit ); break; case QUARTER: case MONTH: - pattern.append("("); - extractField(pattern, YEAR, unit); - pattern.append("+"); - extractField(pattern, MONTH, unit); - pattern.append(")"); + pattern.append( "(" ); + extractField( pattern, YEAR, unit ); + pattern.append( "+" ); + extractField( pattern, MONTH, unit ); + pattern.append( ")" ); break; case WEEK: case DAY: - extractField(pattern, DAY, unit); + extractField( pattern, DAY, unit ); break; case HOUR: - pattern.append("("); - extractField(pattern, DAY, unit); - if (timestamp) { - pattern.append("+"); - extractField(pattern, HOUR, unit); + pattern.append( "(" ); + extractField( pattern, DAY, unit ); + if ( hasTimePart ) { + pattern.append( "+" ); + extractField( pattern, HOUR, unit ); } - pattern.append(")"); + pattern.append( ")" ); break; case MINUTE: - pattern.append("("); - extractField(pattern, DAY, unit); - if (timestamp) { - pattern.append("+"); - extractField(pattern, HOUR, unit); - pattern.append("+"); - extractField(pattern, MINUTE, unit); + pattern.append( "(" ); + extractField( pattern, DAY, unit ); + if ( hasTimePart ) { + pattern.append( "+" ); + extractField( pattern, HOUR, unit ); + pattern.append( "+" ); + extractField( pattern, MINUTE, unit ); } - pattern.append(")"); + pattern.append( ")" ); break; case NATIVE: case NANOSECOND: case SECOND: - pattern.append("("); - extractField(pattern, DAY, unit); - if (timestamp) { - pattern.append("+"); - extractField(pattern, HOUR, unit); - pattern.append("+"); - extractField(pattern, MINUTE, unit); - pattern.append("+"); - extractField(pattern, SECOND, unit); + pattern.append( "(" ); + extractField( pattern, DAY, unit ); + if ( hasTimePart ) { + pattern.append( "+" ); + extractField( pattern, HOUR, unit ); + pattern.append( "+" ); + extractField( pattern, MINUTE, unit ); + pattern.append( "+" ); + extractField( pattern, SECOND, unit ); } - pattern.append(")"); + pattern.append( ")" ); break; default: - throw new SemanticException("unrecognized field: " + unit); + throw new SemanticException( "unrecognized field: " + unit ); } return pattern.toString(); } - private void extractField( - StringBuilder pattern, - TemporalUnit unit, TemporalUnit toUnit) { - pattern.append("extract("); - pattern.append( translateExtractField(unit) ); - pattern.append(" from (?3-?2) "); - switch (unit) { + private void extractField(StringBuilder pattern, TemporalUnit unit, TemporalUnit toUnit) { + pattern.append( "extract(" ); + pattern.append( translateExtractField( unit ) ); + pattern.append( " from (?3-?2) " ); + switch ( unit ) { case YEAR: case MONTH: - pattern.append("year to month"); + pattern.append( "year to month" ); break; case DAY: case HOUR: case MINUTE: case SECOND: - pattern.append("day to second"); + pattern.append( "day to second" ); break; default: - throw new SemanticException(unit + " is not a legal field"); + throw new SemanticException( unit + " is not a legal field" ); } - pattern.append(")"); + pattern.append( ")" ); pattern.append( unit.conversionFactor( toUnit, this ) ); } @@ -627,8 +614,9 @@ public class OracleLegacyDialect extends Dialect { return "number($p,$s)"; case DATE: - case TIME: return "date"; + case TIME: + return getVersion().isBefore( 9 ) ? "date" : super.columnType( sqlTypeCode ); case TIMESTAMP: // the only difference between date and timestamp // on Oracle is that date has no fractional seconds @@ -811,7 +799,7 @@ public class OracleLegacyDialect extends Dialect { typeContributions.contributeJdbcType( OracleJdbcHelper.getArrayJdbcType( serviceRegistry ) ); } else { - typeContributions.contributeJdbcType( ArrayJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( OracleReflectionStructJdbcType.INSTANCE ); } // Oracle requires a custom binder for binding untyped nulls with the NULL type typeContributions.contributeJdbcType( NullJdbcType.INSTANCE ); @@ -1107,24 +1095,15 @@ public class OracleLegacyDialect extends Dialect { @Override public String getQueryHintString(String sql, String hints) { - String statementType = statementType(sql); - - final int pos = sql.indexOf( statementType ); - if ( pos > -1 ) { - final StringBuilder buffer = new StringBuilder( sql.length() + hints.length() + 8 ); - if ( pos > 0 ) { - buffer.append( sql, 0, pos ); - } - buffer - .append( statementType ) - .append( " /*+ " ) - .append( hints ) - .append( " */" ) - .append( sql.substring( pos + statementType.length() ) ); - sql = buffer.toString(); + final String statementType = statementType( sql ); + final int start = sql.indexOf( statementType ); + if ( start < 0 ) { + return sql; + } + else { + int end = start + statementType.length(); + return sql.substring( 0, end ) + " /*+ " + hints + " */" + sql.substring( end ); } - - return sql; } @Override @@ -1161,14 +1140,15 @@ public class OracleLegacyDialect extends Dialect { return true; } - private String statementType(String sql) { - Matcher matcher = SQL_STATEMENT_TYPE_PATTERN.matcher( sql ); + private String statementType(String sql) { + final Matcher matcher = SQL_STATEMENT_TYPE_PATTERN.matcher( sql ); if ( matcher.matches() && matcher.groupCount() == 1 ) { return matcher.group(1); } - - throw new IllegalArgumentException( "Can't determine SQL statement type for statement: " + sql ); + else { + throw new IllegalArgumentException( "Can't determine SQL statement type for statement: " + sql ); + } } @Override @@ -1276,6 +1256,32 @@ public class OracleLegacyDialect extends Dialect { return getWriteLockString( aliases, timeout ); } + @Override + public boolean supportsTemporalLiteralOffset() { + // Oracle *does* support offsets, but only + // in the ANSI syntax, not in the JDBC + // escape-based syntax, which we use in + // almost all circumstances (see below) + return false; + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, TemporalAccessor temporalAccessor, TemporalType precision, TimeZone jdbcTimeZone) { + // we usually use the JDBC escape-based syntax + // because we want to let the JDBC driver handle + // TIME (a concept which does not exist in Oracle) + // but for the special case of timestamps with an + // offset we need to use the ANSI syntax + if ( precision == TemporalType.TIMESTAMP && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanos( appender, temporalAccessor, true, jdbcTimeZone, false ); + appender.appendSql( '\'' ); + } + else { + super.appendDateTimeLiteral( appender, temporalAccessor, precision, jdbcTimeZone ); + } + } + @Override public void appendDatetimeFormat(SqlAppender appender, String format) { // Unlike other databases, Oracle requires an explicit reset for the fm modifier, @@ -1432,4 +1438,9 @@ public class OracleLegacyDialect extends Dialect { public String getCreateUserDefinedTypeKindString() { return "object"; } + + @Override + public String rowId(String rowId) { + return "rowid"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index fec96ae927..3c154f06ee 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -91,7 +91,7 @@ import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantAsTimestampWithTimeZoneJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsOffsetDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @@ -130,9 +130,11 @@ import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.OTHER; import static org.hibernate.type.SqlTypes.SQLXML; import static org.hibernate.type.SqlTypes.STRUCT; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; @@ -209,6 +211,10 @@ public class PostgreSQLLegacyDialect extends Dialect { case LONG32VARBINARY: return "bytea"; + // We do not use the time with timezone type because PG deprecated it and it lacks certain operations like subtraction +// case TIME_UTC: +// return columnType( TIME_WITH_TIMEZONE ); + case TIMESTAMP_UTC: return columnType( TIMESTAMP_WITH_TIMEZONE ); @@ -323,6 +329,12 @@ public class PostgreSQLLegacyDialect extends Dialect { break; } break; + case TIME: + // The PostgreSQL JDBC driver reports TIME for timetz, but we use it only for mapping OffsetTime to UTC + if ( "timetz".equals( columnTypeName ) ) { + jdbcTypeCode = TIME_UTC; + } + break; case TIMESTAMP: // The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant if ( "timestamptz".equals( columnTypeName ) ) { @@ -443,36 +455,31 @@ public class PostgreSQLLegacyDialect extends Dialect { if ( unit == null ) { return "(?3-?2)"; } - if ( toTemporalType != TemporalType.TIMESTAMP && fromTemporalType != TemporalType.TIMESTAMP && unit == DAY ) { + if ( toTemporalType == TemporalType.DATE && fromTemporalType == TemporalType.DATE ) { // special case: subtraction of two dates // results in an integer number of days // instead of an INTERVAL - return "(?3-?2)"; - } - else { - StringBuilder pattern = new StringBuilder(); switch ( unit ) { case YEAR: - extractField( pattern, YEAR, fromTemporalType, toTemporalType, unit ); - break; - case QUARTER: - pattern.append( "(" ); - extractField( pattern, YEAR, fromTemporalType, toTemporalType, unit ); - pattern.append( "+" ); - extractField( pattern, QUARTER, fromTemporalType, toTemporalType, unit ); - pattern.append( ")" ); - break; case MONTH: - pattern.append( "(" ); - extractField( pattern, YEAR, fromTemporalType, toTemporalType, unit ); - pattern.append( "+" ); - extractField( pattern, MONTH, fromTemporalType, toTemporalType, unit ); - pattern.append( ")" ); - break; + case QUARTER: + return "extract(" + translateDurationField( unit ) + " from age(?3,?2))"; + default: + return "(?3-?2)" + DAY.conversionFactor( unit, this ); + } + } + else { + switch ( unit ) { + case YEAR: + return "extract(year from ?3-?2)"; + case QUARTER: + return "(extract(year from ?3-?2)*4+extract(month from ?3-?2)/3)"; + case MONTH: + return "(extract(year from ?3-?2)*12+extract(month from ?3-?2))"; case WEEK: //week is not supported by extract() when the argument is a duration + return "(extract(day from ?3-?2)/7)"; case DAY: - extractField( pattern, DAY, fromTemporalType, toTemporalType, unit ); - break; + return "extract(day from ?3-?2)"; //in order to avoid multiple calls to extract(), //we use extract(epoch from x - y) * factor for //all the following units: @@ -481,15 +488,14 @@ public class PostgreSQLLegacyDialect extends Dialect { case SECOND: case NANOSECOND: case NATIVE: - extractField( pattern, EPOCH, fromTemporalType, toTemporalType, unit ); - break; + return "extract(epoch from ?3-?2)" + EPOCH.conversionFactor( unit, this ); default: throw new SemanticException( "unrecognized field: " + unit ); } - return pattern.toString(); } } + @Deprecated protected void extractField( StringBuilder pattern, TemporalUnit unit, @@ -499,7 +505,7 @@ public class PostgreSQLLegacyDialect extends Dialect { pattern.append( "extract(" ); pattern.append( translateDurationField( unit ) ); pattern.append( " from " ); - if ( toTimestamp != TemporalType.TIMESTAMP && fromTimestamp != TemporalType.TIMESTAMP ) { + if ( toTimestamp == TemporalType.DATE && fromTimestamp == TemporalType.DATE ) { // special case subtraction of two // dates results in an integer not // an Interval @@ -1325,7 +1331,7 @@ public class PostgreSQLLegacyDialect extends Dialect { // dialect uses oid for Blobs, byte arrays cannot be used. jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.BLOB_BINDING ); jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.CLOB_BINDING ); - jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, InstantAsTimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, TimestampUtcAsOffsetDateTimeJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( XmlJdbcType.INSTANCE ); if ( driverKind == PostgreSQLDriverKind.PG_JDBC ) { @@ -1411,23 +1417,13 @@ public class PostgreSQLLegacyDialect extends Dialect { // disabled foreign key constraints still prevent 'truncate table' // (these would help if we used 'delete' instead of 'truncate') -// @Override -// public String getDisableConstraintsStatement() { -// return "set constraints all deferred"; -// } -// -// @Override -// public String getEnableConstraintsStatement() { -// return "set constraints all immediate"; -// } -// -// @Override -// public String getDisableConstraintStatement(String tableName, String name) { -// return "alter table " + tableName + " alter constraint " + name + " deferrable"; -// } -// -// @Override -// public String getEnableConstraintStatement(String tableName, String name) { -// return "alter table " + tableName + " alter constraint " + name + " deferrable"; -// } + @Override + public String rowId(String rowId) { + return "ctid"; + } + + @Override + public int rowIdSqlType() { + return OTHER; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java index ae23685a8d..4e0ce28492 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java @@ -20,8 +20,6 @@ import org.hibernate.query.sqm.TemporalUnit; import jakarta.persistence.TemporalType; -import static org.hibernate.query.sqm.TemporalUnit.DAY; - /** * An SQL dialect for Postgres Plus * @@ -84,12 +82,10 @@ public class PostgresPlusLegacyDialect extends PostgreSQLLegacyDialect { @Override public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { - if ( toTemporalType != TemporalType.TIMESTAMP && fromTemporalType != TemporalType.TIMESTAMP && unit == DAY ) { + if ( toTemporalType == TemporalType.DATE && fromTemporalType == TemporalType.DATE ) { // special case: subtraction of two dates results in an INTERVAL on Postgres Plus // because there is no date type i.e. without time for Oracle compatibility - final StringBuilder pattern = new StringBuilder(); - extractField( pattern, DAY, fromTemporalType, toTemporalType, unit ); - return pattern.toString(); + return super.timestampdiffPattern( unit, TemporalType.TIMESTAMP, TemporalType.TIMESTAMP ); } return super.timestampdiffPattern( unit, fromTemporalType, toTemporalType ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index 2373246a83..84a6501b22 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -169,6 +169,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { return getVersion().isSameOrAfter( 10 ) ? "time" : super.columnType( sqlTypeCode ); case TIMESTAMP: return getVersion().isSameOrAfter( 10 ) ? "datetime2($p)" : super.columnType( sqlTypeCode ); + case TIME_WITH_TIMEZONE: case TIMESTAMP_WITH_TIMEZONE: return getVersion().isSameOrAfter( 10 ) ? "datetimeoffset($p)" : super.columnType( sqlTypeCode ); } @@ -780,6 +781,8 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { case SECOND: //this should evaluate to a floating point type return "(datepart(second,?2)+datepart(nanosecond,?2)/1e9)"; + case EPOCH: + return "datediff_big(second, '1970-01-01', ?2)"; case WEEK: // Thanks https://www.sqlservercentral.com/articles/a-simple-formula-to-calculate-the-iso-week-number if ( getVersion().isBefore( 10 ) ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java index 40c4c46abd..cbb18d2305 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java @@ -37,6 +37,7 @@ import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; /** * SQL Dialect for Sybase/SQL Anywhere @@ -65,6 +66,7 @@ public class SybaseAnywhereDialect extends SybaseDialect { return "time"; case TIMESTAMP: return "timestamp"; + case TIME_WITH_TIMEZONE: case TIMESTAMP_WITH_TIMEZONE: return "timestamp with time zone"; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java index 36824fa76a..1b1875ae34 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AbstractPropertyHolder.java @@ -30,6 +30,7 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.util.StringHelper; import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType; +import org.hibernate.usertype.internal.OffsetTimeCompositeUserType; import org.jboss.logging.Logger; @@ -45,6 +46,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.MappedSuperclass; +import static org.hibernate.boot.model.internal.TimeZoneStorageHelper.isOffsetTimeClass; import static org.hibernate.boot.model.internal.TimeZoneStorageHelper.useColumnForTimeZoneStorage; /** @@ -484,11 +486,19 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { } } else if ( useColumnForTimeZoneStorage( element, context ) ) { - final Column column = createTimestampColumn( element, path, context ); - columnOverride.put( - path + "." + AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME, - new Column[]{ column } - ); + final Column column = createTemporalColumn( element, path, context ); + if ( isOffsetTimeClass( element ) ) { + columnOverride.put( + path + "." + OffsetTimeCompositeUserType.LOCAL_TIME_NAME, + new Column[] { column } + ); + } + else { + columnOverride.put( + path + "." + AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME, + new Column[] { column } + ); + } final Column offsetColumn = createTimeZoneColumn( element, column ); columnOverride.put( path + "." + AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME, @@ -527,7 +537,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { } } - private static Column createTimestampColumn(XAnnotatedElement element, String path, MetadataBuildingContext context) { + private static Column createTemporalColumn(XAnnotatedElement element, String path, MetadataBuildingContext context) { int precision; final Column annotatedColumn = element.getAnnotation( Column.class ); if ( annotatedColumn != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TimeZoneStorageHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TimeZoneStorageHelper.java index d054d08161..89a1bc7589 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TimeZoneStorageHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/TimeZoneStorageHelper.java @@ -13,9 +13,11 @@ import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.internal.OffsetDateTimeCompositeUserType; +import org.hibernate.usertype.internal.OffsetTimeCompositeUserType; import org.hibernate.usertype.internal.ZonedDateTimeCompositeUserType; import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.time.ZonedDateTime; import static org.hibernate.TimeZoneStorageStrategy.COLUMN; @@ -23,6 +25,7 @@ import static org.hibernate.dialect.TimeZoneSupport.NATIVE; public class TimeZoneStorageHelper { + private static final String OFFSET_TIME_CLASS = OffsetTime.class.getName(); private static final String OFFSET_DATETIME_CLASS = OffsetDateTime.class.getName(); private static final String ZONED_DATETIME_CLASS = ZonedDateTime.class.getName(); @@ -38,13 +41,29 @@ public class TimeZoneStorageHelper { else if ( ZONED_DATETIME_CLASS.equals( returnedClassName ) ) { return ZonedDateTimeCompositeUserType.class; } + else if ( OFFSET_TIME_CLASS.equals( returnedClassName ) ) { + return OffsetTimeCompositeUserType.class; + } } return null; } - private static boolean isZonedDateTimeClass(String returnedClassName) { + private static boolean isTemporalWithTimeZoneClass(String returnedClassName) { return OFFSET_DATETIME_CLASS.equals( returnedClassName ) - || ZONED_DATETIME_CLASS.equals( returnedClassName ); + || ZONED_DATETIME_CLASS.equals( returnedClassName ) + || isOffsetTimeClass( returnedClassName ); + } + + public static boolean isOffsetTimeClass(XAnnotatedElement element) { + if ( element instanceof XProperty ) { + XProperty property = (XProperty) element; + return isOffsetTimeClass( property.getType().getName() ); + } + return false; + } + + private static boolean isOffsetTimeClass(String returnedClassName) { + return OFFSET_TIME_CLASS.equals( returnedClassName ); } static boolean useColumnForTimeZoneStorage(XAnnotatedElement element, MetadataBuildingContext context) { @@ -52,7 +71,7 @@ public class TimeZoneStorageHelper { if ( timeZoneStorage == null ) { if ( element instanceof XProperty ) { XProperty property = (XProperty) element; - return isZonedDateTimeClass( property.getType().getName() ) + return isTemporalWithTimeZoneClass( property.getType().getName() ) //no @TimeZoneStorage annotation, so we need to use the default storage strategy && context.getBuildingOptions().getDefaultTimeZoneStorage() == COLUMN; } 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 5f45248b13..39e4fd795e 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 @@ -10,6 +10,7 @@ import java.io.InputStream; import java.sql.Types; import java.time.Instant; import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; @@ -65,7 +66,6 @@ import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonAsStringJdbcType; -import org.hibernate.type.descriptor.jdbc.JsonJdbcType; import org.hibernate.type.descriptor.jdbc.XmlAsStringJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.DdlType; @@ -687,7 +687,11 @@ public class MetadataBuildingProcess { final JdbcType timestampWithTimeZoneOverride = getTimestampWithTimeZoneOverride( options, jdbcTypeRegistry ); if ( timestampWithTimeZoneOverride != null ) { - adaptToDefaultTimeZoneStorage( typeConfiguration, timestampWithTimeZoneOverride ); + adaptTimestampTypesToDefaultTimeZoneStorage( typeConfiguration, timestampWithTimeZoneOverride ); + } + final JdbcType timeWithTimeZoneOverride = getTimeWithTimeZoneOverride( options, jdbcTypeRegistry ); + if ( timeWithTimeZoneOverride != null ) { + adaptTimeTypesToDefaultTimeZoneStorage( typeConfiguration, timeWithTimeZoneOverride ); } final int preferredSqlTypeCodeForInstant = getPreferredSqlTypeCodeForInstant( serviceRegistry ); if ( preferredSqlTypeCodeForInstant != SqlTypes.TIMESTAMP_UTC ) { @@ -728,7 +732,25 @@ public class MetadataBuildingProcess { ); } - private static void adaptToDefaultTimeZoneStorage( + private static void adaptTimeTypesToDefaultTimeZoneStorage( + TypeConfiguration typeConfiguration, + JdbcType timestampWithTimeZoneOverride) { + final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); + final BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); + final BasicType offsetDateTimeType = new NamedBasicTypeImpl<>( + javaTypeRegistry.getDescriptor( OffsetTime.class ), + timestampWithTimeZoneOverride, + "OffsetTime" + ); + basicTypeRegistry.register( + offsetDateTimeType, + "org.hibernate.type.OffsetTimeType", + OffsetTime.class.getSimpleName(), + OffsetTime.class.getName() + ); + } + + private static void adaptTimestampTypesToDefaultTimeZoneStorage( TypeConfiguration typeConfiguration, JdbcType timestampWithTimeZoneOverride) { final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); @@ -757,6 +779,19 @@ public class MetadataBuildingProcess { ); } + private static JdbcType getTimeWithTimeZoneOverride(MetadataBuildingOptions options, JdbcTypeRegistry jdbcTypeRegistry) { + switch ( options.getDefaultTimeZoneStorage() ) { + case NORMALIZE: + // For NORMALIZE, we replace the standard types that use TIME_WITH_TIMEZONE to use TIME + return jdbcTypeRegistry.getDescriptor( Types.TIME ); + case NORMALIZE_UTC: + // For NORMALIZE_UTC, we replace the standard types that use TIME_WITH_TIMEZONE to use TIME_UTC + return jdbcTypeRegistry.getDescriptor( SqlTypes.TIME_UTC ); + default: + return null; + } + } + private static JdbcType getTimestampWithTimeZoneOverride(MetadataBuildingOptions options, JdbcTypeRegistry jdbcTypeRegistry) { switch ( options.getDefaultTimeZoneStorage() ) { case NORMALIZE: diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/relational/ColumnOrderingStrategyStandard.java b/hibernate-core/src/main/java/org/hibernate/boot/model/relational/ColumnOrderingStrategyStandard.java index b03508ec36..e85f5be4db 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/relational/ColumnOrderingStrategyStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/relational/ColumnOrderingStrategyStandard.java @@ -195,6 +195,7 @@ public class ColumnOrderingStrategyStandard implements ColumnOrderingStrategy { return (int) length; case DATE: case TIME: + case TIME_UTC: case TIME_WITH_TIMEZONE: return 4; case TIMESTAMP: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java index b9c29fb825..cb4b189edc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -137,8 +137,10 @@ import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.POINT; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARCHAR; import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_END; @@ -254,6 +256,9 @@ public abstract class AbstractHANADialect extends Dialect { case DOUBLE: return "double"; //no explicit precision + case TIME: + case TIME_WITH_TIMEZONE: + return "time"; case TIMESTAMP: case TIMESTAMP_WITH_TIMEZONE: return "timestamp"; @@ -1196,19 +1201,19 @@ public abstract class AbstractHANADialect extends Dialect { public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { switch (unit) { case NANOSECOND: -// if ( temporalType == TemporalType.TIME ) { -// return "nano100_between(cast(?3 as timestamp), cast(?2 as timestamp))*100"; -// } -// else { - return "nano100_between(?2,?3)*100"; -// } + if ( fromTemporalType == TemporalType.TIME && toTemporalType == TemporalType.TIME ) { + return "seconds_between(?2,?3)*1000000000"; + } + else { + return "nano100_between(?2,?3)*100"; + } case NATIVE: -// if ( temporalType == TemporalType.TIME ) { -// return "nano100_between(cast(?3 as timestamp), cast(?2 as timestamp))"; -// } -// else { - return "nano100_between(?2,?3)"; -// } + if ( fromTemporalType == TemporalType.TIME && toTemporalType == TemporalType.TIME ) { + return "seconds_between(?2,?3)*10000000"; + } + else { + return "nano100_between(?2,?3)"; + } case QUARTER: return "months_between(?2,?3)/3"; case WEEK: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java index 01570ef28b..e1b442d739 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java @@ -322,6 +322,7 @@ public abstract class AbstractPostgreSQLStructJdbcType implements AggregateJdbcT break; case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: values[column] = fromRawObject( jdbcMapping, parseTime( @@ -804,6 +805,7 @@ public abstract class AbstractPostgreSQLStructJdbcType implements AggregateJdbcT case SqlTypes.DATE: case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: case SqlTypes.TIMESTAMP: case SqlTypes.TIMESTAMP_WITH_TIMEZONE: case SqlTypes.TIMESTAMP_UTC: @@ -940,6 +942,8 @@ public abstract class AbstractPostgreSQLStructJdbcType implements AggregateJdbcT } break; case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: if ( value instanceof java.util.Date ) { appendAsTime( appender, (java.util.Date) value, jdbcTimeZone ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 0d9283b740..8b99b9d43f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -46,6 +46,7 @@ import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.query.SemanticException; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.NullOrdering; import org.hibernate.query.sqm.TemporalUnit; @@ -58,7 +59,7 @@ import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantAsTimestampWithTimeZoneJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsOffsetDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @@ -76,7 +77,9 @@ import jakarta.persistence.TemporalType; import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.query.sqm.TemporalUnit.DAY; +import static org.hibernate.query.sqm.TemporalUnit.EPOCH; import static org.hibernate.query.sqm.TemporalUnit.NATIVE; +import static org.hibernate.query.sqm.TemporalUnit.YEAR; import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BLOB; @@ -94,9 +97,11 @@ import static org.hibernate.type.SqlTypes.NCHAR; import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.OTHER; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; @@ -212,6 +217,10 @@ public class CockroachDialect extends Dialect { case BLOB: return "bytes"; + // We do not use the time with timezone type because PG deprecated it and it lacks certain operations like subtraction +// case TIME_UTC: +// return columnType( TIME_WITH_TIMEZONE ); + case TIMESTAMP_UTC: return columnType( TIMESTAMP_WITH_TIMEZONE ); @@ -284,6 +293,12 @@ public class CockroachDialect extends Dialect { break; } break; + case TIME: + // The PostgreSQL JDBC driver reports TIME for timetz, but we use it only for mapping OffsetTime to UTC + if ( "timetz".equals( columnTypeName ) ) { + jdbcTypeCode = TIME_UTC; + } + break; case TIMESTAMP: // The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant if ( "timestamptz".equals( columnTypeName ) ) { @@ -339,7 +354,7 @@ public class CockroachDialect extends Dialect { protected void contributeCockroachTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); - jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, InstantAsTimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, TimestampUtcAsOffsetDateTimeJdbcType.INSTANCE ); if ( driverKind == PostgreSQLDriverKind.PG_JDBC ) { jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); if ( PgJdbcHelper.isUsable( serviceRegistry ) ) { @@ -422,7 +437,13 @@ public class CockroachDialect extends Dialect { functionContributions.getFunctionRegistry().register( "format", - new FormatFunction( "experimental_strftime", functionContributions.getTypeConfiguration() ) + new FormatFunction( + "experimental_strftime", + false, + true, + false, + functionContributions.getTypeConfiguration() + ) ); functionFactory.windowFunctions(); functionFactory.listagg_stringAgg( "string" ); @@ -756,30 +777,46 @@ public class CockroachDialect extends Dialect { if ( unit == null ) { return "(?3-?2)"; } - switch (unit) { - case YEAR: - return "(extract(year from ?3)-extract(year from ?2))"; - case QUARTER: - return "(extract(year from ?3)*4-extract(year from ?2)*4+extract(month from ?3)//3-extract(month from ?2)//3)"; - case MONTH: - return "(extract(year from ?3)*12-extract(year from ?2)*12+extract(month from ?3)-extract(month from ?2))"; - } - if ( toTemporalType != TemporalType.TIMESTAMP && fromTemporalType != TemporalType.TIMESTAMP ) { + if ( toTemporalType == TemporalType.DATE && fromTemporalType == TemporalType.DATE ) { // special case: subtraction of two dates // results in an integer number of days // instead of an INTERVAL - return "(?3-?2)" + DAY.conversionFactor( unit, this ); + switch ( unit ) { + case YEAR: + case MONTH: + case QUARTER: + // age only supports timestamptz, so we have to cast the date expressions + return "extract(" + translateDurationField( unit ) + " from age(cast(?3 as timestamptz),cast(?2 as timestamptz)))"; + default: + return "(?3-?2)" + DAY.conversionFactor( unit, this ); + } } else { switch (unit) { - case WEEK: - return "extract_duration(hour from ?3-?2)/168"; + case YEAR: + return "extract(year from ?3-?2)"; + case QUARTER: + return "(extract(year from ?3-?2)*4+extract(month from ?3-?2)//3)"; + case MONTH: + return "(extract(year from ?3-?2)*12+extract(month from ?3-?2))"; + case WEEK: //week is not supported by extract() when the argument is a duration + return "(extract(day from ?3-?2)/7)"; case DAY: - return "extract_duration(hour from ?3-?2)/24"; + return "extract(day from ?3-?2)"; + //in order to avoid multiple calls to extract(), + //we use extract(epoch from x - y) * factor for + //all the following units: + + // Note that CockroachDB also has an extract_duration function which returns an int, + // but we don't use that here because it is deprecated since v20 + case HOUR: + case MINUTE: + case SECOND: case NANOSECOND: - return "extract_duration(microsecond from ?3-?2)*1e3"; + case NATIVE: + return "cast(extract(epoch from ?3-?2)" + EPOCH.conversionFactor( unit, this ) + " as int)"; default: - return "extract_duration(?1 from ?3-?2)"; + throw new SemanticException( "unrecognized field: " + unit ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index eb303e3def..63c05ab1c0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -93,6 +93,7 @@ import static org.hibernate.type.SqlTypes.CLOB; import static org.hibernate.type.SqlTypes.DECIMAL; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.SQLXML; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; @@ -189,6 +190,7 @@ public class DB2Dialect extends Dialect { return "clob"; case TIMESTAMP_WITH_TIMEZONE: return "timestamp($p)"; + case TIME: case TIME_WITH_TIMEZONE: return "time"; case BINARY: @@ -408,9 +410,37 @@ public class DB2Dialect extends Dialect { if ( getDB2Version().isBefore( 11 ) ) { return timestampdiffPatternV10( unit, fromTemporalType, toTemporalType ); } - StringBuilder pattern = new StringBuilder(); - boolean castFrom = fromTemporalType != TemporalType.TIMESTAMP && !unit.isDateUnit(); - boolean castTo = toTemporalType != TemporalType.TIMESTAMP && !unit.isDateUnit(); + final StringBuilder pattern = new StringBuilder(); + final String fromExpression; + final String toExpression; + if ( unit.isDateUnit() ) { + fromExpression = "?2"; + toExpression = "?3"; + } + else { + switch ( fromTemporalType ) { + case DATE: + fromExpression = "cast(?2 as timestamp)"; + break; + case TIME: + fromExpression = "timestamp('1970-01-01',?2)"; + break; + default: + fromExpression = "?2"; + break; + } + switch ( toTemporalType ) { + case DATE: + toExpression = "cast(?3 as timestamp)"; + break; + case TIME: + toExpression = "timestamp('1970-01-01',?3)"; + break; + default: + toExpression = "?3"; + break; + } + } switch ( unit ) { case NATIVE: case NANOSECOND: @@ -426,26 +456,24 @@ public class DB2Dialect extends Dialect { default: pattern.append( "?1s_between(" ); } - if ( castTo ) { - pattern.append( "cast(?3 as timestamp)" ); - } - else { - pattern.append( "?3" ); - } + pattern.append( toExpression ); pattern.append( ',' ); - if ( castFrom ) { - pattern.append( "cast(?2 as timestamp)" ); - } - else { - pattern.append( "?2" ); - } + pattern.append( fromExpression ); pattern.append( ')' ); switch ( unit ) { case NATIVE: - pattern.append( "+(microsecond(?3)-microsecond(?2))/1e6)" ); + pattern.append( "+(microsecond("); + pattern.append( toExpression ); + pattern.append(")-microsecond("); + pattern.append( fromExpression ); + pattern.append("))/1e6)" ); break; case NANOSECOND: - pattern.append( "*1e9+(microsecond(?3)-microsecond(?2))*1e3)" ); + pattern.append( "*1e9+(microsecond("); + pattern.append( toExpression ); + pattern.append(")-microsecond("); + pattern.append( fromExpression ); + pattern.append("))*1e3)" ); break; case MONTH: pattern.append( ')' ); @@ -458,10 +486,36 @@ public class DB2Dialect extends Dialect { } public static String timestampdiffPatternV10(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { - final boolean castFrom = fromTemporalType != TemporalType.TIMESTAMP && !unit.isDateUnit(); - final boolean castTo = toTemporalType != TemporalType.TIMESTAMP && !unit.isDateUnit(); - final String fromExpression = castFrom ? "cast(?2 as timestamp)" : "?2"; - final String toExpression = castTo ? "cast(?3 as timestamp)" : "?3"; + final String fromExpression; + final String toExpression; + if ( unit.isDateUnit() ) { + if ( fromTemporalType == TemporalType.TIME ) { + fromExpression = "timestamp('1970-01-01',?2)"; + } + else { + fromExpression = "?2"; + } + if ( toTemporalType == TemporalType.TIME ) { + toExpression = "timestamp('1970-01-01',?3)"; + } + else { + toExpression = "?3"; + } + } + else { + if ( fromTemporalType == TemporalType.DATE ) { + fromExpression = "cast(?2 as timestamp)"; + } + else { + fromExpression = "?2"; + } + if ( toTemporalType == TemporalType.DATE ) { + toExpression = "cast(?3 as timestamp)"; + } + else { + toExpression = "?3"; + } + } switch ( unit ) { case NATIVE: return "(select (days(t2)-days(t1))*86400+(midnight_seconds(t2)-midnight_seconds(t1))+(microsecond(t2)-microsecond(t1))/1e6 " + @@ -498,19 +552,24 @@ public class DB2Dialect extends Dialect { @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { final StringBuilder pattern = new StringBuilder(); - final boolean castTo; + final String timestampExpression; if ( unit.isDateUnit() ) { - castTo = temporalType == TemporalType.TIME; + if ( temporalType == TemporalType.TIME ) { + timestampExpression = "timestamp('1970-01-01',?3)"; + } + else { + timestampExpression = "?3"; + } } else { - castTo = temporalType == TemporalType.DATE; - } - if (castTo) { - pattern.append("cast(?3 as timestamp)"); - } - else { - pattern.append("?3"); + if ( temporalType == TemporalType.DATE ) { + timestampExpression = "cast(?3 as timestamp)"; + } + else { + timestampExpression = "?3"; + } } + pattern.append(timestampExpression); pattern.append("+("); // DB2 supports temporal arithmetic. See https://www.ibm.com/support/knowledgecenter/en/SSEPGG_9.7.0/com.ibm.db2.luw.sql.ref.doc/doc/r0023457.html switch (unit) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java index f3edb98029..747ea39e16 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java @@ -32,6 +32,7 @@ import java.util.List; import static org.hibernate.type.SqlTypes.ROWID; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; /** * A SQL dialect for DB2 for z/OS version 12.1 and above, previously known as: @@ -78,9 +79,13 @@ public class DB2zDialect extends DB2Dialect { @Override protected String columnType(int sqlTypeCode) { - if ( sqlTypeCode == TIMESTAMP_WITH_TIMEZONE && getVersion().isAfter( 10 ) ) { - // See https://www.ibm.com/support/knowledgecenter/SSEPEK_10.0.0/wnew/src/tpc/db2z_10_timestamptimezone.html - return "timestamp with time zone"; + if ( getVersion().isAfter( 10 ) ) { + switch ( sqlTypeCode ) { + case TIME_WITH_TIMEZONE: + case TIMESTAMP_WITH_TIMEZONE: + // See https://www.ibm.com/support/knowledgecenter/SSEPEK_10.0.0/wnew/src/tpc/db2z_10_timestamptimezone.html + return "timestamp with time zone"; + } } return super.columnType( sqlTypeCode ); } @@ -150,14 +155,7 @@ public class DB2zDialect extends DB2Dialect { @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { - StringBuilder pattern = new StringBuilder(); - final boolean castTo; - if ( unit.isDateUnit() ) { - castTo = temporalType == TemporalType.TIME; - } - else { - castTo = temporalType == TemporalType.DATE; - } + final StringBuilder pattern = new StringBuilder(); pattern.append("add_"); switch (unit) { case NATIVE: @@ -175,12 +173,24 @@ public class DB2zDialect extends DB2Dialect { pattern.append("?1"); } pattern.append("s("); - if (castTo) { - pattern.append("cast(?3 as timestamp)"); + final String timestampExpression; + if ( unit.isDateUnit() ) { + if ( temporalType == TemporalType.TIME ) { + timestampExpression = "timestamp('1970-01-01',?3)"; + } + else { + timestampExpression = "?3"; + } } else { - pattern.append("?3"); + if ( temporalType == TemporalType.DATE ) { + timestampExpression = "cast(?3 as timestamp)"; + } + else { + timestampExpression = "?3"; + } } + pattern.append(timestampExpression); pattern.append(","); switch (unit) { case NANOSECOND: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java index 314f9020b9..75c7c92426 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java @@ -85,8 +85,10 @@ import static org.hibernate.type.SqlTypes.NCHAR; import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; @@ -154,6 +156,10 @@ public class DerbyDialect extends Dialect { case NCLOB: return "clob"; + case TIME: + case TIME_WITH_TIMEZONE: + return "time"; + case TIMESTAMP: case TIMESTAMP_WITH_TIMEZONE: return "timestamp"; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 64bc4c28f8..2fda34e821 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -178,14 +178,16 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantAsTimestampJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantAsTimestampWithTimeZoneJdbcType; +import org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsOffsetDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.LongNVarcharJdbcType; import org.hibernate.type.descriptor.jdbc.NCharJdbcType; import org.hibernate.type.descriptor.jdbc.NClobJdbcType; import org.hibernate.type.descriptor.jdbc.NVarcharJdbcType; +import org.hibernate.type.descriptor.jdbc.TimeUtcAsJdbcTimeJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -229,6 +231,7 @@ import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; @@ -404,6 +407,7 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun ddlTypeRegistry.addDescriptor( simpleSqlType( DATE ) ); ddlTypeRegistry.addDescriptor( simpleSqlType( TIME ) ); ddlTypeRegistry.addDescriptor( simpleSqlType( TIME_WITH_TIMEZONE ) ); + ddlTypeRegistry.addDescriptor( simpleSqlType( TIME_UTC ) ); ddlTypeRegistry.addDescriptor( simpleSqlType( TIMESTAMP ) ); ddlTypeRegistry.addDescriptor( simpleSqlType( TIMESTAMP_WITH_TIMEZONE ) ); ddlTypeRegistry.addDescriptor( simpleSqlType( TIMESTAMP_UTC ) ); @@ -534,17 +538,21 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun case DATE: return "date"; case TIME: - return "time"; + return "time($p)"; case TIME_WITH_TIMEZONE: // type included here for completeness but note that // very few databases support it, and the general // advice is to caution against its use (for reasons, // check the comments in the Postgres documentation). - return "time with time zone"; + return "time($p) with time zone"; case TIMESTAMP: return "timestamp($p)"; case TIMESTAMP_WITH_TIMEZONE: return "timestamp($p) with time zone"; + case TIME_UTC: + return getTimeZoneSupport() == TimeZoneSupport.NATIVE + ? columnType( TIME_WITH_TIMEZONE ) + : columnType( TIME ); case TIMESTAMP_UTC: return getTimeZoneSupport() == TimeZoneSupport.NATIVE ? columnType( TIMESTAMP_WITH_TIMEZONE ) @@ -1555,10 +1563,12 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun } if ( getTimeZoneSupport() == TimeZoneSupport.NATIVE ) { - jdbcTypeRegistry.addDescriptor( InstantAsTimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimestampUtcAsOffsetDateTimeJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimeUtcAsOffsetTimeJdbcType.INSTANCE ); } else { - jdbcTypeRegistry.addDescriptor( InstantAsTimestampJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimestampUtcAsJdbcTimestampJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimeUtcAsJdbcTimeJdbcType.INSTANCE ); } if ( supportsStandardArrays() ) { @@ -4899,6 +4909,9 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun precision = (int) ceil( precision * LOG_BASE2OF10 ); } break; + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: case SqlTypes.TIMESTAMP: case SqlTypes.TIMESTAMP_WITH_TIMEZONE: case SqlTypes.TIMESTAMP_UTC: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 3c3f969c3f..6d1b4c67bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -69,8 +69,11 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2 import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.descriptor.jdbc.H2FormatJsonJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantJdbcType; +import org.hibernate.type.descriptor.jdbc.TimeUtcAsJdbcTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsInstantJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampWithTimeZoneJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -98,6 +101,9 @@ import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.OTHER; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; @@ -190,6 +196,9 @@ public class H2Dialect extends Dialect { // which caused problems for schema update tool case NUMERIC: return getVersion().isBefore( 2 ) ? columnType( DECIMAL ) : super.columnType( sqlTypeCode ); + // Support was only added in 2.0 + case TIME_WITH_TIMEZONE: + return getVersion().isBefore( 2 ) ? columnType( TIMESTAMP_WITH_TIMEZONE ) : super.columnType( sqlTypeCode ); case NCHAR: return columnType( CHAR ); case NVARCHAR: @@ -243,7 +252,15 @@ public class H2Dialect extends Dialect { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); - jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, InstantJdbcType.INSTANCE ); + if ( getVersion().isBefore( 2 ) ) { + // Support for TIME_WITH_TIMEZONE was only added in 2.0 + jdbcTypeRegistry.addDescriptor( TIME_WITH_TIMEZONE, TimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TimeUtcAsJdbcTimeJdbcType.INSTANCE ); + } + else { + jdbcTypeRegistry.addDescriptor( TimeUtcAsOffsetTimeJdbcType.INSTANCE ); + } + jdbcTypeRegistry.addDescriptor( TimestampUtcAsInstantJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); if ( getVersion().isSameOrAfter( 1, 4, 198 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java index 64dc0e58fc..27944bb15f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java @@ -164,6 +164,7 @@ public class JsonHelper { break; case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: appender.append( '"' ); JdbcTimeJavaType.INSTANCE.appendEncodedString( appender, @@ -798,6 +799,7 @@ public class JsonHelper { ); case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: return jdbcMapping.getJdbcJavaType().wrap( JdbcTimeJavaType.INSTANCE.fromEncodedString( string, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index d8d055ff2e..088a0b609d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -529,86 +529,84 @@ public class OracleDialect extends Dialect { @Override public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { - StringBuilder pattern = new StringBuilder(); - boolean timestamp = toTemporalType == TemporalType.TIMESTAMP || fromTemporalType == TemporalType.TIMESTAMP; - switch (unit) { + final StringBuilder pattern = new StringBuilder(); + final boolean hasTimePart = toTemporalType != TemporalType.DATE || fromTemporalType != TemporalType.DATE; + switch ( unit ) { case YEAR: - extractField(pattern, YEAR, unit); + extractField( pattern, YEAR, unit ); break; case QUARTER: case MONTH: - pattern.append("("); - extractField(pattern, YEAR, unit); - pattern.append("+"); - extractField(pattern, MONTH, unit); - pattern.append(")"); + pattern.append( "(" ); + extractField( pattern, YEAR, unit ); + pattern.append( "+" ); + extractField( pattern, MONTH, unit ); + pattern.append( ")" ); break; case WEEK: case DAY: - extractField(pattern, DAY, unit); + extractField( pattern, DAY, unit ); break; case HOUR: - pattern.append("("); - extractField(pattern, DAY, unit); - if (timestamp) { - pattern.append("+"); - extractField(pattern, HOUR, unit); + pattern.append( "(" ); + extractField( pattern, DAY, unit ); + if ( hasTimePart ) { + pattern.append( "+" ); + extractField( pattern, HOUR, unit ); } - pattern.append(")"); + pattern.append( ")" ); break; case MINUTE: - pattern.append("("); - extractField(pattern, DAY, unit); - if (timestamp) { - pattern.append("+"); - extractField(pattern, HOUR, unit); - pattern.append("+"); - extractField(pattern, MINUTE, unit); + pattern.append( "(" ); + extractField( pattern, DAY, unit ); + if ( hasTimePart ) { + pattern.append( "+" ); + extractField( pattern, HOUR, unit ); + pattern.append( "+" ); + extractField( pattern, MINUTE, unit ); } - pattern.append(")"); + pattern.append( ")" ); break; case NATIVE: case NANOSECOND: case SECOND: - pattern.append("("); - extractField(pattern, DAY, unit); - if (timestamp) { - pattern.append("+"); - extractField(pattern, HOUR, unit); - pattern.append("+"); - extractField(pattern, MINUTE, unit); - pattern.append("+"); - extractField(pattern, SECOND, unit); + pattern.append( "(" ); + extractField( pattern, DAY, unit ); + if ( hasTimePart ) { + pattern.append( "+" ); + extractField( pattern, HOUR, unit ); + pattern.append( "+" ); + extractField( pattern, MINUTE, unit ); + pattern.append( "+" ); + extractField( pattern, SECOND, unit ); } - pattern.append(")"); + pattern.append( ")" ); break; default: - throw new SemanticException("unrecognized field: " + unit); + throw new SemanticException( "unrecognized field: " + unit ); } return pattern.toString(); } - private void extractField( - StringBuilder pattern, - TemporalUnit unit, TemporalUnit toUnit) { - pattern.append("extract("); - pattern.append( translateExtractField(unit) ); - pattern.append(" from (?3-?2) "); - switch (unit) { + private void extractField(StringBuilder pattern, TemporalUnit unit, TemporalUnit toUnit) { + pattern.append( "extract(" ); + pattern.append( translateExtractField( unit ) ); + pattern.append( " from (?3-?2) " ); + switch ( unit ) { case YEAR: case MONTH: - pattern.append("year to month"); + pattern.append( "year to month" ); break; case DAY: case HOUR: case MINUTE: case SECOND: - pattern.append("day to second"); + pattern.append( "day to second" ); break; default: - throw new SemanticException(unit + " is not a legal field"); + throw new SemanticException( unit + " is not a legal field" ); } - pattern.append(")"); + pattern.append( ")" ); pattern.append( unit.conversionFactor( toUnit, this ) ); } @@ -637,8 +635,9 @@ public class OracleDialect extends Dialect { return "number($p,$s)"; case DATE: - case TIME: return "date"; + case TIME: + return "timestamp($p)"; // the only difference between date and timestamp // on Oracle is that date has no fractional seconds case TIME_WITH_TIMEZONE: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index aa264980fc..12c0fa85f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -80,7 +80,7 @@ import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; -import org.hibernate.type.descriptor.jdbc.InstantAsTimestampWithTimeZoneJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsOffsetDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @@ -119,9 +119,11 @@ import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.OTHER; import static org.hibernate.type.SqlTypes.SQLXML; import static org.hibernate.type.SqlTypes.STRUCT; +import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; @@ -220,6 +222,10 @@ public class PostgreSQLDialect extends Dialect { case LONG32VARBINARY: return "bytea"; + // We do not use the time with timezone type because PG deprecated it and it lacks certain operations like subtraction +// case TIME_UTC: +// return columnType( TIME_WITH_TIMEZONE ); + case TIMESTAMP_UTC: return columnType( TIMESTAMP_WITH_TIMEZONE ); @@ -325,6 +331,12 @@ public class PostgreSQLDialect extends Dialect { break; } break; + case TIME: + // The PostgreSQL JDBC driver reports TIME for timetz, but we use it only for mapping OffsetTime to UTC + if ( "timetz".equals( columnTypeName ) ) { + jdbcTypeCode = TIME_UTC; + } + break; case TIMESTAMP: // The PostgreSQL JDBC driver reports TIMESTAMP for timestamptz, but we use it only for mapping Instant if ( "timestamptz".equals( columnTypeName ) ) { @@ -445,36 +457,31 @@ public class PostgreSQLDialect extends Dialect { if ( unit == null ) { return "(?3-?2)"; } - if ( toTemporalType != TemporalType.TIMESTAMP && fromTemporalType != TemporalType.TIMESTAMP && unit == DAY ) { + if ( toTemporalType == TemporalType.DATE && fromTemporalType == TemporalType.DATE ) { // special case: subtraction of two dates // results in an integer number of days // instead of an INTERVAL - return "(?3-?2)"; - } - else { - StringBuilder pattern = new StringBuilder(); switch ( unit ) { case YEAR: - extractField( pattern, YEAR, fromTemporalType, toTemporalType, unit ); - break; - case QUARTER: - pattern.append( "(" ); - extractField( pattern, YEAR, fromTemporalType, toTemporalType, unit ); - pattern.append( "+" ); - extractField( pattern, QUARTER, fromTemporalType, toTemporalType, unit ); - pattern.append( ")" ); - break; case MONTH: - pattern.append( "(" ); - extractField( pattern, YEAR, fromTemporalType, toTemporalType, unit ); - pattern.append( "+" ); - extractField( pattern, MONTH, fromTemporalType, toTemporalType, unit ); - pattern.append( ")" ); - break; + case QUARTER: + return "extract(" + translateDurationField( unit ) + " from age(?3,?2))"; + default: + return "(?3-?2)" + DAY.conversionFactor( unit, this ); + } + } + else { + switch ( unit ) { + case YEAR: + return "extract(year from ?3-?2)"; + case QUARTER: + return "(extract(year from ?3-?2)*4+extract(month from ?3-?2)/3)"; + case MONTH: + return "(extract(year from ?3-?2)*12+extract(month from ?3-?2))"; case WEEK: //week is not supported by extract() when the argument is a duration + return "(extract(day from ?3-?2)/7)"; case DAY: - extractField( pattern, DAY, fromTemporalType, toTemporalType, unit ); - break; + return "extract(day from ?3-?2)"; //in order to avoid multiple calls to extract(), //we use extract(epoch from x - y) * factor for //all the following units: @@ -483,15 +490,14 @@ public class PostgreSQLDialect extends Dialect { case SECOND: case NANOSECOND: case NATIVE: - extractField( pattern, EPOCH, fromTemporalType, toTemporalType, unit ); - break; + return "extract(epoch from ?3-?2)" + EPOCH.conversionFactor( unit, this ); default: throw new SemanticException( "unrecognized field: " + unit ); } - return pattern.toString(); } } + @Deprecated protected void extractField( StringBuilder pattern, TemporalUnit unit, @@ -501,7 +507,7 @@ public class PostgreSQLDialect extends Dialect { pattern.append( "extract(" ); pattern.append( translateDurationField( unit ) ); pattern.append( " from " ); - if ( toTimestamp != TemporalType.TIMESTAMP && fromTimestamp != TemporalType.TIMESTAMP ) { + if ( toTimestamp == TemporalType.DATE && fromTimestamp == TemporalType.DATE ) { // special case subtraction of two // dates results in an integer not // an Interval @@ -1333,7 +1339,7 @@ public class PostgreSQLDialect extends Dialect { // dialect uses oid for Blobs, byte arrays cannot be used. jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.BLOB_BINDING ); jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.CLOB_BINDING ); - jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, InstantAsTimestampWithTimeZoneJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, TimestampUtcAsOffsetDateTimeJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( XmlJdbcType.INSTANCE ); if ( driverKind == PostgreSQLDriverKind.PG_JDBC ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java index 00e2c6f11b..2769656690 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java @@ -24,8 +24,6 @@ import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; import jakarta.persistence.TemporalType; -import static org.hibernate.query.sqm.TemporalUnit.DAY; - /** * An SQL dialect for Postgres Plus * @@ -88,12 +86,10 @@ public class PostgresPlusDialect extends PostgreSQLDialect { @Override public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { - if ( toTemporalType != TemporalType.TIMESTAMP && fromTemporalType != TemporalType.TIMESTAMP && unit == DAY ) { + if ( toTemporalType == TemporalType.DATE && fromTemporalType == TemporalType.DATE ) { // special case: subtraction of two dates results in an INTERVAL on Postgres Plus // because there is no date type i.e. without time for Oracle compatibility - final StringBuilder pattern = new StringBuilder(); - extractField( pattern, DAY, fromTemporalType, toTemporalType, unit ); - return pattern.toString(); + return super.timestampdiffPattern( unit, TemporalType.TIMESTAMP, TemporalType.TIMESTAMP ); } return super.timestampdiffPattern( unit, fromTemporalType, toTemporalType ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index bcf1a84d88..d4d8bd30f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -100,6 +100,7 @@ import static org.hibernate.type.SqlTypes.SQLXML; import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; @@ -179,6 +180,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { return "time"; case TIMESTAMP: return "datetime2($p)"; + case TIME_WITH_TIMEZONE: case TIMESTAMP_WITH_TIMEZONE: return "datetimeoffset($p)"; default: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java index f2fc9dddd2..f0cd04c91b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java @@ -374,6 +374,8 @@ public class StructJdbcType implements AggregateJdbcType { if ( rawJdbcValue != null ) { final JdbcMapping jdbcMapping = attributeMapping.getSingleJdbcMapping(); switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: case SqlTypes.TIMESTAMP_WITH_TIMEZONE: case SqlTypes.TIMESTAMP_UTC: // Only transform the raw jdbc value if it could be a TIMESTAMPTZ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java index 0f0ba066a4..d150a20154 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java @@ -160,6 +160,7 @@ public class XmlHelper { ); case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: return fromRawObject( jdbcMapping, JdbcTimeJavaType.INSTANCE.fromEncodedString( @@ -598,6 +599,7 @@ public class XmlHelper { break; case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: JdbcTimeJavaType.INSTANCE.appendEncodedString( appender, jdbcJavaType.unwrap( value, java.sql.Time.class, options ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java index 805808fb4d..827e18c1e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java @@ -19,6 +19,7 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; import org.hibernate.type.spi.TypeConfiguration; @@ -38,6 +39,8 @@ import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; @@ -117,6 +120,8 @@ public class OracleAggregateSupport extends AggregateSupportImpl { aggregateParentReadExpression + "." + column + ".date()" ); case TIME: + case TIME_WITH_TIMEZONE: + case TIME_UTC: return template.replace( placeholder, "to_timestamp(" + aggregateParentReadExpression + "." + column + ".string(),'hh24:mi:ss')" diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java index 60a3133c19..1bb12b76d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java @@ -11,6 +11,7 @@ import java.util.List; import org.hibernate.dialect.Dialect; import org.hibernate.internal.util.StringHelper; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.ReturnableType; import org.hibernate.query.spi.QueryEngine; @@ -48,6 +49,7 @@ import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.predicate.BetweenPredicate; import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -64,6 +66,7 @@ public class FormatFunction extends AbstractSqmFunctionDescriptor implements Fun private final String nativeFunctionName; private final boolean reversedArguments; private final boolean concatPattern; + private final boolean supportsTime; public FormatFunction(String nativeFunctionName, TypeConfiguration typeConfiguration) { this( nativeFunctionName, false, true, typeConfiguration ); @@ -74,6 +77,15 @@ public class FormatFunction extends AbstractSqmFunctionDescriptor implements Fun boolean reversedArguments, boolean concatPattern, TypeConfiguration typeConfiguration) { + this( nativeFunctionName, reversedArguments, concatPattern, true, typeConfiguration ); + } + + public FormatFunction( + String nativeFunctionName, + boolean reversedArguments, + boolean concatPattern, + boolean supportsTime, + TypeConfiguration typeConfiguration) { super( "format", new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL, STRING ), @@ -84,6 +96,7 @@ public class FormatFunction extends AbstractSqmFunctionDescriptor implements Fun this.nativeFunctionName = nativeFunctionName; this.reversedArguments = reversedArguments; this.concatPattern = concatPattern; + this.supportsTime = supportsTime; } @Override @@ -98,9 +111,15 @@ public class FormatFunction extends AbstractSqmFunctionDescriptor implements Fun if ( reversedArguments ) { format.accept( walker ); sqlAppender.append( ',' ); + if ( !supportsTime && isTimeTemporal( expression ) ) { + sqlAppender.append( "date'1970-01-01'+" ); + } expression.accept( walker ); } else { + if ( !supportsTime && isTimeTemporal( expression ) ) { + sqlAppender.append( "date'1970-01-01'+" ); + } expression.accept( walker ); sqlAppender.append( ',' ); format.accept( walker ); @@ -108,6 +127,23 @@ public class FormatFunction extends AbstractSqmFunctionDescriptor implements Fun sqlAppender.append( ')' ); } + private boolean isTimeTemporal(SqlAstNode expression) { + if ( expression instanceof Expression ) { + final JdbcMappingContainer expressionType = ( (Expression) expression ).getExpressionType(); + if ( expressionType.getJdbcTypeCount() == 1 ) { + switch ( expressionType.getSingleJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: + return true; + default: + break; + } + } + } + return false; + } + @Override protected SelfRenderingSqmFunction generateSqmFunctionExpression( List> arguments, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerFormatEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerFormatEmulation.java index c7cbb62d08..22baed0cb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerFormatEmulation.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerFormatEmulation.java @@ -13,6 +13,7 @@ import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -32,10 +33,9 @@ public class SQLServerFormatEmulation extends FormatFunction { List arguments, SqlAstTranslator walker) { final Expression datetime = (Expression) arguments.get(0); - final boolean isTime = TypeConfiguration.getSqlTemporalType( datetime.getExpressionType() ) == TemporalType.TIME; sqlAppender.appendSql("format("); - if ( isTime ) { + if ( needsDateTimeCast( datetime ) ) { sqlAppender.appendSql("cast("); datetime.accept( walker ); sqlAppender.appendSql(" as datetime)"); @@ -47,4 +47,16 @@ public class SQLServerFormatEmulation extends FormatFunction { arguments.get( 1 ).accept( walker ); sqlAppender.appendSql(')'); } + + private boolean needsDateTimeCast(Expression datetime) { + final boolean isTime = TypeConfiguration.getSqlTemporalType( datetime.getExpressionType() ) == TemporalType.TIME; + if ( isTime ) { + // Since SQL Server has no dedicated type for time with time zone, we use the offsetdatetime which has a date part + return datetime.getExpressionType() + .getSingleJdbcMapping() + .getJdbcType() + .getDefaultSqlTypeCode() != SqlTypes.TIME_WITH_TIMEZONE; + } + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java index cf228e868a..04266b2ca4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java @@ -13,6 +13,7 @@ import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetTime; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; @@ -31,6 +32,7 @@ import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; import org.hibernate.type.descriptor.java.TemporalJavaType; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType; +import org.hibernate.usertype.internal.OffsetTimeCompositeUserType; /** * @author Steve Ebersole @@ -117,7 +119,12 @@ public class SqmExpressionHelper { public static SqmExpression getActualExpression(SqmExpression expression) { if ( isCompositeTemporal( expression ) ) { - return ( (SqmPath) expression ).get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); + if ( expression.getJavaTypeDescriptor().getJavaTypeClass() == OffsetTime.class ) { + return ( (SqmPath) expression ).get( OffsetTimeCompositeUserType.LOCAL_TIME_NAME ); + } + else { + return ( (SqmPath) expression ).get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); + } } else { return expression; @@ -127,18 +134,24 @@ public class SqmExpressionHelper { public static SqmExpression getOffsetAdjustedExpression(SqmExpression expression) { if ( isCompositeTemporal( expression ) ) { final SqmPath compositePath = (SqmPath) expression; - final SqmPath instantPath = compositePath.get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); - final NodeBuilder nodeBuilder = instantPath.nodeBuilder(); + final SqmPath temporalPath; + if ( expression.getJavaTypeDescriptor().getJavaTypeClass() == OffsetTime.class ) { + temporalPath = compositePath.get( OffsetTimeCompositeUserType.LOCAL_TIME_NAME ); + } + else { + temporalPath = compositePath.get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); + } + final NodeBuilder nodeBuilder = temporalPath.nodeBuilder(); return new SqmBinaryArithmetic<>( BinaryArithmeticOperator.ADD, - instantPath, + temporalPath, new SqmToDuration<>( compositePath.get( AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME ), new SqmDurationUnit<>( TemporalUnit.SECOND, nodeBuilder.getIntegerType(), nodeBuilder ), nodeBuilder.getTypeConfiguration().getBasicTypeForJavaType( Duration.class ), nodeBuilder ), - instantPath.getNodeType(), + temporalPath.getNodeType(), nodeBuilder ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java index 1117852206..03c5eaab1f 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -440,9 +440,9 @@ public class SqlTypes { * JDBC} timezone. * * @see org.hibernate.cfg.AvailableSettings#PREFERRED_INSTANT_JDBC_TYPE - * @see org.hibernate.type.descriptor.jdbc.InstantJdbcType - * @see org.hibernate.type.descriptor.jdbc.InstantAsTimestampJdbcType - * @see org.hibernate.type.descriptor.jdbc.InstantAsTimestampWithTimeZoneJdbcType + * @see org.hibernate.type.descriptor.jdbc.TimestampUtcAsInstantJdbcType + * @see org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType + * @see org.hibernate.type.descriptor.jdbc.TimestampUtcAsOffsetDateTimeJdbcType */ public static final int TIMESTAMP_UTC = 3003; @@ -479,6 +479,18 @@ public class SqlTypes { @Internal public static final int MATERIALIZED_NCLOB = 3006; + /** + * A type code representing the generic SQL type {@code TIME}, + * where the value is given in UTC, instead of in the system or + * {@linkplain org.hibernate.cfg.AvailableSettings#JDBC_TIME_ZONE + * JDBC} timezone. + * + * @see org.hibernate.annotations.TimeZoneStorageType#NORMALIZE_UTC + * @see org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType + * @see org.hibernate.type.descriptor.jdbc.TimeUtcAsJdbcTimeJdbcType + */ + public static final int TIME_UTC = 3007; + // Interval types /** @@ -689,6 +701,8 @@ public class SqlTypes { switch ( typeCode ) { case DATE: case TIME: + case TIME_WITH_TIMEZONE: + case TIME_UTC: case TIMESTAMP: case TIMESTAMP_WITH_TIMEZONE: case TIMESTAMP_UTC: @@ -728,6 +742,8 @@ public class SqlTypes { public static boolean hasTimePart(int typeCode) { switch ( typeCode ) { case TIME: + case TIME_WITH_TIMEZONE: + case TIME_UTC: case TIMESTAMP: case TIMESTAMP_WITH_TIMEZONE: case TIMESTAMP_UTC: 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 d69c90645a..1b56bff483 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/StandardBasicTypes.java @@ -413,12 +413,40 @@ public final class StandardBasicTypes { ); /** - * The standard Hibernate type for mapping {@link OffsetTime} to JDBC {@link org.hibernate.type.SqlTypes#TIME TIME}. + * The standard Hibernate type for mapping {@link OffsetTime} to JDBC {@link org.hibernate.type.SqlTypes#TIME_WITH_TIMEZONE TIME_WITH_TIMEZONE}. */ public static final BasicTypeReference OFFSET_TIME = new BasicTypeReference<>( "OffsetTime", OffsetTime.class, - // todo (6.0): why not TIME_WITH_TIMEZONE ? + SqlTypes.TIME_WITH_TIMEZONE + ); + + /** + * The standard Hibernate type for mapping {@link OffsetTime} to JDBC {@link org.hibernate.type.SqlTypes#TIME_UTC TIME_UTC}. + * This maps to {@link org.hibernate.TimeZoneStorageStrategy#NORMALIZE_UTC}. + */ + public static final BasicTypeReference OFFSET_TIME_UTC = new BasicTypeReference<>( + "OffsetTimeUtc", + OffsetTime.class, + SqlTypes.TIME_UTC + ); + + /** + * The standard Hibernate type for mapping {@link OffsetTime} to JDBC {@link org.hibernate.type.SqlTypes#TIME_WITH_TIMEZONE TIME_WITH_TIMEZONE}. + * This maps to {@link org.hibernate.TimeZoneStorageStrategy#NATIVE}. + */ + public static final BasicTypeReference OFFSET_TIME_WITH_TIMEZONE = new BasicTypeReference<>( + "OffsetTimeWithTimezone", + OffsetTime.class, + SqlTypes.TIME_WITH_TIMEZONE + ); + + /** + * The standard Hibernate type for mapping {@link OffsetTime} to JDBC {@link org.hibernate.type.SqlTypes#TIME TIME}. + */ + public static final BasicTypeReference OFFSET_TIME_WITHOUT_TIMEZONE = new BasicTypeReference<>( + "OffsetTimeWithoutTimezone", + OffsetTime.class, SqlTypes.TIME ); @@ -1038,6 +1066,27 @@ public final class StandardBasicTypes { OffsetTime.class.getSimpleName(), OffsetTime.class.getName() ); + handle( + OFFSET_TIME_UTC, + null, + basicTypeRegistry, + OFFSET_TIME_UTC.getName() + ); + + handle( + OFFSET_TIME_WITH_TIMEZONE, + null, + basicTypeRegistry, + OFFSET_TIME_WITH_TIMEZONE.getName() + ); + + handle( + OFFSET_TIME_WITHOUT_TIMEZONE, + null, + basicTypeRegistry, + OFFSET_TIME_WITHOUT_TIMEZONE.getName() + ); + handle( ZONED_DATE_TIME, "org.hibernate.type.ZonedDateTimeType", diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java index b282e7c343..210cfdc66a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java @@ -456,12 +456,17 @@ public final class DateTimeUtils { if ( defaultTimestampPrecision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { return temporal; } - final int precisionMask = pow10( 9 - defaultTimestampPrecision ); - final int nano = temporal.get( ChronoField.NANO_OF_SECOND ); - final int nanosToRound = nano % precisionMask; - final int finalNano = nano - nanosToRound + ( nanosToRound >= ( precisionMask >> 1 ) ? precisionMask : 0 ); //noinspection unchecked - return (T) temporal.with( ChronoField.NANO_OF_SECOND, finalNano ); + return (T) temporal.with( + ChronoField.NANO_OF_SECOND, + roundToPrecision( temporal.get( ChronoField.NANO_OF_SECOND ), defaultTimestampPrecision ) + ); + } + + public static long roundToPrecision(int nano, int precision) { + final int precisionMask = pow10( 9 - precision ); + final int nanosToRound = nano % precisionMask; + return nano - nanosToRound + ( nanosToRound >= ( precisionMask >> 1 ) ? precisionMask : 0 ); } private static int pow10(int exponent) { 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 e66bf54ad5..32be08c55b 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 @@ -147,6 +147,6 @@ public class CalendarTimeJavaType extends AbstractTemporalJavaType { @Override public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) { - return 0; //seconds (currently ignored since Dialects don't parameterize time type by precision) + return dialect.getDefaultTimestampPrecision(); } } 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 8aefb2ab28..9e469cd118 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 @@ -13,6 +13,7 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.util.Calendar; import java.util.Date; @@ -22,6 +23,7 @@ import org.hibernate.HibernateException; import org.hibernate.dialect.Dialect; import org.hibernate.internal.util.CharSequenceHelper; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -127,9 +129,15 @@ public class JdbcTimeJavaType extends AbstractTemporalJavaType { } if ( LocalTime.class.isAssignableFrom( type ) ) { - return value instanceof java.sql.Time - ? ( (java.sql.Time) value ).toLocalTime() - : new java.sql.Time( value.getTime() ).toLocalTime(); + final Time time = value instanceof java.sql.Time + ? ( (java.sql.Time) value ) + : new java.sql.Time( value.getTime() ); + final LocalTime localTime = time.toLocalTime(); + final long millis = time.getTime() % 1000; + if ( millis == 0 ) { + return localTime; + } + return localTime.with( ChronoField.NANO_OF_SECOND, millis * 1_000_000L ); } if ( Time.class.isAssignableFrom( type ) ) { @@ -174,7 +182,13 @@ public class JdbcTimeJavaType extends AbstractTemporalJavaType { } if ( value instanceof LocalTime ) { - return Time.valueOf( (LocalTime) value ); + final LocalTime localTime = (LocalTime) value; + final Time time = Time.valueOf( localTime ); + if ( localTime.getNano() == 0 ) { + return time; + } + // Preserve milliseconds, which java.sql.Time supports + return new Time( time.getTime() + DateTimeUtils.roundToPrecision( localTime.getNano(), 3 ) ); } if ( value instanceof Date ) { @@ -250,8 +264,7 @@ public class JdbcTimeJavaType extends AbstractTemporalJavaType { @Override public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) { - //seconds (currently ignored since Dialects don't parameterize time type by precision) - return 0; + return dialect.getDefaultTimestampPrecision(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalTimeJavaType.java index 7b37ee2046..b1d7206bba 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalTimeJavaType.java @@ -16,6 +16,7 @@ import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -23,6 +24,7 @@ import java.util.GregorianCalendar; import jakarta.persistence.TemporalType; import org.hibernate.dialect.Dialect; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -81,7 +83,12 @@ public class LocalTimeJavaType extends AbstractTemporalJavaType { } if ( Time.class.isAssignableFrom( type ) ) { - return (X) Time.valueOf( value ); + final Time time = Time.valueOf( value ); + if ( value.getNano() == 0 ) { + return (X) time; + } + // Preserve milliseconds, which java.sql.Time supports + return (X) new Time( time.getTime() + DateTimeUtils.roundToPrecision( value.getNano(), 3 ) ); } // Oracle documentation says to set the Date to January 1, 1970 when convert from @@ -122,6 +129,16 @@ public class LocalTimeJavaType extends AbstractTemporalJavaType { return (LocalTime) value; } + if (value instanceof Time) { + final Time time = (Time) value; + final LocalTime localTime = time.toLocalTime(); + final long millis = time.getTime() % 1000; + if ( millis == 0 ) { + return localTime; + } + return localTime.with( ChronoField.NANO_OF_SECOND, millis * 1_000_000L ); + } + if (value instanceof Timestamp) { final Timestamp ts = (Timestamp) value; return LocalDateTime.ofInstant( ts.toInstant(), ZoneId.systemDefault() ).toLocalTime(); @@ -158,7 +175,7 @@ public class LocalTimeJavaType extends AbstractTemporalJavaType { @Override public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) { - return 0; + return dialect.getDefaultTimestampPrecision(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetTimeJavaType.java index 90fa7d989a..0577c7f109 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetTimeJavaType.java @@ -16,6 +16,7 @@ import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -23,6 +24,7 @@ import java.util.GregorianCalendar; import jakarta.persistence.TemporalType; import org.hibernate.dialect.Dialect; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -49,8 +51,9 @@ public class OffsetTimeJavaType extends AbstractTemporalJavaType { } @Override - public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { - return context.getJdbcType( Types.TIME ); + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators stdIndicators) { + return stdIndicators.getTypeConfiguration().getJdbcTypeRegistry() + .getDescriptor( stdIndicators.getDefaultZonedTimeSqlType() ); } @Override @@ -87,13 +90,22 @@ public class OffsetTimeJavaType extends AbstractTemporalJavaType { return (X) offsetTime.withOffsetSameInstant( getCurrentSystemOffset() ).toLocalTime(); } + if ( OffsetDateTime.class.isAssignableFrom( type ) ) { + return (X) offsetTime.atDate( LocalDate.EPOCH ); + } + // for legacy types, we assume that the JDBC timezone is passed to JDBC // (since PS.setTime() and friends do accept a timezone passed as a Calendar) final OffsetTime jdbcOffsetTime = offsetTime.withOffsetSameInstant( getCurrentJdbcOffset(options) ); if ( Time.class.isAssignableFrom( type ) ) { - return (X) Time.valueOf( jdbcOffsetTime.toLocalTime() ); + final Time time = Time.valueOf( jdbcOffsetTime.toLocalTime() ); + if ( jdbcOffsetTime.getNano() == 0 ) { + return (X) time; + } + // Preserve milliseconds, which java.sql.Time supports + return (X) new Time( time.getTime() + DateTimeUtils.roundToPrecision( jdbcOffsetTime.getNano(), 3 ) ); } final OffsetDateTime jdbcOffsetDateTime = jdbcOffsetTime.atDate( LocalDate.EPOCH ); @@ -145,6 +157,10 @@ public class OffsetTimeJavaType extends AbstractTemporalJavaType { return ((LocalTime) value).atOffset( getCurrentSystemOffset() ); } + if ( value instanceof OffsetDateTime ) { + return ( (OffsetDateTime) value ).toOffsetTime(); + } + /* * Also, in order to fix HHH-13357, and to be consistent with the conversion to Time (see above), * we set the offset to the current offset of the JVM (OffsetDateTime.now().getOffset()). @@ -165,8 +181,14 @@ public class OffsetTimeJavaType extends AbstractTemporalJavaType { if (value instanceof Time) { final Time time = (Time) value; - return time.toLocalTime().atOffset( getCurrentJdbcOffset(options) ) + final OffsetTime offsetTime = time.toLocalTime() + .atOffset( getCurrentJdbcOffset( options) ) .withOffsetSameInstant( getCurrentSystemOffset() ); + final long millis = time.getTime() % 1000; + if ( millis == 0 ) { + return offsetTime; + } + return offsetTime.with( ChronoField.NANO_OF_SECOND, millis * 1_000_000L ); } if (value instanceof Timestamp) { @@ -217,7 +239,7 @@ public class OffsetTimeJavaType extends AbstractTemporalJavaType { @Override public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) { - return 0; + return dialect.getDefaultTimestampPrecision(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampJdbcType.java index cff5572182..7b62d7ee1e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampJdbcType.java @@ -6,116 +6,15 @@ */ package org.hibernate.type.descriptor.jdbc; -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.sql.Types; -import java.time.Instant; -import java.util.Calendar; -import java.util.TimeZone; - import org.hibernate.type.SqlTypes; -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.BasicJavaType; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; -import org.hibernate.type.spi.TypeConfiguration; - -import jakarta.persistence.TemporalType; /** * Descriptor for {@link SqlTypes#TIMESTAMP_UTC TIMESTAMP_UTC} handling. * + * @deprecated Use {@link TimestampUtcAsJdbcTimestampJdbcType} * @author Christian Beikov */ -public class InstantAsTimestampJdbcType implements JdbcType { - +@Deprecated(forRemoval = true) +public class InstantAsTimestampJdbcType extends TimestampUtcAsJdbcTimestampJdbcType { public static final InstantAsTimestampJdbcType INSTANCE = new InstantAsTimestampJdbcType(); - private static final Calendar UTC_CALENDAR = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); - - public InstantAsTimestampJdbcType() { - } - - @Override - public int getJdbcTypeCode() { - return Types.TIMESTAMP; - } - - @Override - public int getDefaultSqlTypeCode() { - return SqlTypes.TIMESTAMP_UTC; - } - - @Override - public String getFriendlyName() { - return "TIMESTAMP_UTC"; - } - - @Override - public String toString() { - return "TimestampUtcDescriptor"; - } - - @Override - public JavaType getJdbcRecommendedJavaTypeMapping( - Integer length, - Integer scale, - TypeConfiguration typeConfiguration) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Instant.class ); - } - - @Override - public Class getPreferredJavaTypeClass(WrapperOptions options) { - return Instant.class; - } - - @Override - public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { - return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIMESTAMP ); - } - - @Override - public ValueBinder getBinder(final JavaType javaType) { - return new BasicBinder<>( javaType, this ) { - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - final Instant instant = javaType.unwrap( value, Instant.class, options ); - st.setTimestamp( index, Timestamp.from( instant ), UTC_CALENDAR ); - } - - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - final Instant instant = javaType.unwrap( value, Instant.class, options ); - st.setTimestamp( name, Timestamp.from( instant ), UTC_CALENDAR ); - } - }; - } - - @Override - public ValueExtractor getExtractor(final JavaType javaType) { - return new BasicExtractor<>( javaType, this ) { - @Override - protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { - final Timestamp timestamp = rs.getTimestamp( paramIndex, UTC_CALENDAR ); - return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); - } - - @Override - protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { - final Timestamp timestamp = statement.getTimestamp( index, UTC_CALENDAR ); - return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); - } - - @Override - protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { - final Timestamp timestamp = statement.getTimestamp( name, UTC_CALENDAR ); - return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); - } - }; - } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampWithTimeZoneJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampWithTimeZoneJdbcType.java index 063b3a56ad..ef6791ae08 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampWithTimeZoneJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantAsTimestampWithTimeZoneJdbcType.java @@ -6,121 +6,15 @@ */ package org.hibernate.type.descriptor.jdbc; -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; - import org.hibernate.type.SqlTypes; -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.BasicJavaType; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; -import org.hibernate.type.spi.TypeConfiguration; - -import jakarta.persistence.TemporalType; /** * Descriptor for {@link SqlTypes#TIMESTAMP_UTC TIMESTAMP_UTC} handling. * + * @deprecated Use {@link TimestampUtcAsOffsetDateTimeJdbcType} * @author Christian Beikov */ -public class InstantAsTimestampWithTimeZoneJdbcType implements JdbcType { - +@Deprecated(forRemoval = true) +public class InstantAsTimestampWithTimeZoneJdbcType extends TimestampUtcAsOffsetDateTimeJdbcType { public static final InstantAsTimestampWithTimeZoneJdbcType INSTANCE = new InstantAsTimestampWithTimeZoneJdbcType(); - - public InstantAsTimestampWithTimeZoneJdbcType() { - } - - @Override - public int getJdbcTypeCode() { - return Types.TIMESTAMP_WITH_TIMEZONE; - } - - @Override - public int getDefaultSqlTypeCode() { - return SqlTypes.TIMESTAMP_UTC; - } - - @Override - public String getFriendlyName() { - return "TIMESTAMP_UTC"; - } - - @Override - public String toString() { - return "TimestampUtcDescriptor"; - } - - @Override - public JavaType getJdbcRecommendedJavaTypeMapping( - Integer length, - Integer scale, - TypeConfiguration typeConfiguration) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Instant.class ); - } - - @Override - public Class getPreferredJavaTypeClass(WrapperOptions options) { - return OffsetDateTime.class; - } - - @Override - public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { - return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIMESTAMP ); - } - - @Override - public ValueBinder getBinder(final JavaType javaType) { - return new BasicBinder<>( javaType, this ) { - @Override - protected void doBind( - PreparedStatement st, - X value, - int index, - WrapperOptions wrapperOptions) throws SQLException { - final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, wrapperOptions ); - // supposed to be supported in JDBC 4.2 - st.setObject( index, dateTime.withOffsetSameInstant( ZoneOffset.UTC ), Types.TIMESTAMP_WITH_TIMEZONE ); - } - - @Override - protected void doBind( - CallableStatement st, - X value, - String name, - WrapperOptions wrapperOptions) - throws SQLException { - final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, wrapperOptions ); - // supposed to be supported in JDBC 4.2 - st.setObject( name, dateTime.withOffsetSameInstant( ZoneOffset.UTC ), Types.TIMESTAMP_WITH_TIMEZONE ); - } - }; - } - - @Override - public ValueExtractor getExtractor(final JavaType javaType) { - return new BasicExtractor<>( javaType, this ) { - @Override - protected X doExtract(ResultSet rs, int position, WrapperOptions wrapperOptions) throws SQLException { - return javaType.wrap( rs.getObject( position, OffsetDateTime.class ), wrapperOptions ); - } - - @Override - protected X doExtract(CallableStatement statement, int position, WrapperOptions wrapperOptions) throws SQLException { - return javaType.wrap( statement.getObject( position, OffsetDateTime.class ), wrapperOptions ); - } - - @Override - protected X doExtract(CallableStatement statement, String name, WrapperOptions wrapperOptions) throws SQLException { - return javaType.wrap( statement.getObject( name, OffsetDateTime.class ), wrapperOptions ); - } - }; - } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantJdbcType.java index 5af3a99ac7..c99f0da180 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/InstantJdbcType.java @@ -6,154 +6,15 @@ */ package org.hibernate.type.descriptor.jdbc; -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.sql.Types; -import java.time.Instant; - import org.hibernate.type.SqlTypes; -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.BasicJavaType; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; -import org.hibernate.type.spi.TypeConfiguration; - -import jakarta.persistence.TemporalType; /** * Descriptor for {@link SqlTypes#TIMESTAMP_UTC TIMESTAMP_UTC} handling. * + * @deprecated Use {@link TimestampUtcAsInstantJdbcType} * @author Christian Beikov */ -public class InstantJdbcType implements JdbcType { +@Deprecated(forRemoval = true) +public class InstantJdbcType extends TimestampUtcAsInstantJdbcType { public static final InstantJdbcType INSTANCE = new InstantJdbcType(); - - public InstantJdbcType() { - } - - @Override - public int getJdbcTypeCode() { - return Types.TIMESTAMP_WITH_TIMEZONE; - } - - @Override - public int getDefaultSqlTypeCode() { - return SqlTypes.TIMESTAMP_UTC; - } - - @Override - public String getFriendlyName() { - return "TIMESTAMP_UTC"; - } - - @Override - public String toString() { - return "TimestampUtcDescriptor"; - } - - @Override - public JavaType getJdbcRecommendedJavaTypeMapping( - Integer length, - Integer scale, - TypeConfiguration typeConfiguration) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Instant.class ); - } - - @Override - public Class getPreferredJavaTypeClass(WrapperOptions options) { - return Instant.class; - } - - @Override - public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { - return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIMESTAMP ); - } - - @Override - public ValueBinder getBinder(final JavaType javaType) { - return new BasicBinder<>( javaType, this ) { - @Override - protected void doBind( - PreparedStatement st, - X value, - int index, - WrapperOptions wrapperOptions) throws SQLException { - try { - final Instant dateTime = javaType.unwrap( value, Instant.class, wrapperOptions ); - // supposed to be supported in JDBC 4.2 - st.setObject( index, dateTime, Types.TIMESTAMP_WITH_TIMEZONE ); - } - catch (SQLException|AbstractMethodError e) { - // fall back to treating it as a JDBC Timestamp - final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, wrapperOptions ); - st.setTimestamp( index, timestamp ); - } - } - - @Override - protected void doBind( - CallableStatement st, - X value, - String name, - WrapperOptions wrapperOptions) - throws SQLException { - try { - final Instant dateTime = javaType.unwrap( value, Instant.class, wrapperOptions ); - // supposed to be supported in JDBC 4.2 - st.setObject( name, dateTime, Types.TIMESTAMP_WITH_TIMEZONE ); - } - catch (SQLException|AbstractMethodError e) { - // fall back to treating it as a JDBC Timestamp - final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, wrapperOptions ); - st.setTimestamp( name, timestamp ); - } - } - }; - } - - @Override - public ValueExtractor getExtractor(final JavaType javaType) { - return new BasicExtractor<>( javaType, this ) { - @Override - protected X doExtract(ResultSet rs, int position, WrapperOptions wrapperOptions) throws SQLException { - try { - // supposed to be supported in JDBC 4.2 - return javaType.wrap( rs.getObject( position, Instant.class ), wrapperOptions ); - } - catch (SQLException|AbstractMethodError e) { - // fall back to treating it as a JDBC Timestamp - return javaType.wrap( rs.getTimestamp( position ), wrapperOptions ); - } - } - - @Override - protected X doExtract(CallableStatement statement, int position, WrapperOptions wrapperOptions) throws SQLException { - try { - // supposed to be supported in JDBC 4.2 - return javaType.wrap( statement.getObject( position, Instant.class ), wrapperOptions ); - } - catch (SQLException|AbstractMethodError e) { - // fall back to treating it as a JDBC Timestamp - return javaType.wrap( statement.getTimestamp( position ), wrapperOptions ); - } - } - - @Override - protected X doExtract(CallableStatement statement, String name, WrapperOptions wrapperOptions) throws SQLException { - try { - // supposed to be supported in JDBC 4.2 - return javaType.wrap( statement.getObject( name, Instant.class ), wrapperOptions ); - } - catch (SQLException|AbstractMethodError e) { - // fall back to treating it as a JDBC Timestamp - return javaType.wrap( statement.getTimestamp( name ), wrapperOptions ); - } - } - }; - } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java index 8816b14315..dd64edc7e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java @@ -26,6 +26,8 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.type.SqlTypes.*; + /** * Descriptor for the SQL/JDBC side of a value mapping. A {@code JdbcType} is * always coupled with a {@link JavaType} to describe the typing aspects of an @@ -193,32 +195,32 @@ public interface JdbcType extends Serializable { default boolean isInteger() { int typeCode = getDdlTypeCode(); - return SqlTypes.isIntegral(typeCode) - || typeCode == Types.BIT; //HIGHLY DUBIOUS! + return isIntegral(typeCode) + || typeCode == BIT; //HIGHLY DUBIOUS! } default boolean isFloat() { - return SqlTypes.isFloatOrRealOrDouble( getDdlTypeCode() ); + return isFloatOrRealOrDouble( getDdlTypeCode() ); } default boolean isDecimal() { - return SqlTypes.isNumericOrDecimal( getDdlTypeCode() ); + return isNumericOrDecimal( getDdlTypeCode() ); } default boolean isNumber() { - return SqlTypes.isNumericType( getDdlTypeCode() ); + return isNumericType( getDdlTypeCode() ); } default boolean isBinary() { - return SqlTypes.isBinaryType( getDdlTypeCode() ); + return isBinaryType( getDdlTypeCode() ); } default boolean isString() { - return SqlTypes.isCharacterOrClobType( getDdlTypeCode() ); + return isCharacterOrClobType( getDdlTypeCode() ); } default boolean isTemporal() { - return SqlTypes.isTemporalType( getDdlTypeCode() ); + return isTemporalType( getDdlTypeCode() ); } default boolean isLob() { @@ -227,9 +229,9 @@ public interface JdbcType extends Serializable { static boolean isLob(int jdbcTypeCode) { switch ( jdbcTypeCode ) { - case SqlTypes.BLOB: - case SqlTypes.CLOB: - case SqlTypes.NCLOB: { + case BLOB: + case CLOB: + case NCLOB: { return true; } } @@ -242,11 +244,11 @@ public interface JdbcType extends Serializable { static boolean isNationalized(int jdbcTypeCode) { switch ( jdbcTypeCode ) { - case SqlTypes.NCHAR: - case SqlTypes.NVARCHAR: - case SqlTypes.LONGNVARCHAR: - case SqlTypes.LONG32NVARCHAR: - case SqlTypes.NCLOB: { + case NCHAR: + case NVARCHAR: + case LONGNVARCHAR: + case LONG32NVARCHAR: + case NCLOB: { return true; } } @@ -254,7 +256,7 @@ public interface JdbcType extends Serializable { } default boolean isInterval() { - return SqlTypes.isIntervalType( getDdlTypeCode() ); + return isIntervalType( getDdlTypeCode() ); } default CastType getCastType() { @@ -263,40 +265,42 @@ public interface JdbcType extends Serializable { static CastType getCastType(int typeCode) { switch ( typeCode ) { - case Types.INTEGER: - case Types.TINYINT: - case Types.SMALLINT: + case INTEGER: + case TINYINT: + case SMALLINT: return CastType.INTEGER; - case Types.BIGINT: + case BIGINT: return CastType.LONG; - case Types.FLOAT: - case Types.REAL: + case FLOAT: + case REAL: return CastType.FLOAT; - case Types.DOUBLE: + case DOUBLE: return CastType.DOUBLE; - case Types.CHAR: - case Types.NCHAR: - case Types.VARCHAR: - case Types.NVARCHAR: - case Types.LONGVARCHAR: - case Types.LONGNVARCHAR: + case CHAR: + case NCHAR: + case VARCHAR: + case NVARCHAR: + case LONGVARCHAR: + case LONGNVARCHAR: return CastType.STRING; - case Types.CLOB: + case CLOB: return CastType.CLOB; - case Types.BOOLEAN: + case BOOLEAN: return CastType.BOOLEAN; - case Types.DECIMAL: - case Types.NUMERIC: + case DECIMAL: + case NUMERIC: return CastType.FIXED; - case Types.DATE: + case DATE: return CastType.DATE; - case Types.TIME: + case TIME: + case TIME_UTC: + case TIME_WITH_TIMEZONE: return CastType.TIME; - case Types.TIMESTAMP: + case TIMESTAMP: return CastType.TIMESTAMP; - case Types.TIMESTAMP_WITH_TIMEZONE: + case TIMESTAMP_WITH_TIMEZONE: return CastType.OFFSET_TIMESTAMP; - case Types.NULL: + case NULL: return CastType.NULL; default: return CastType.OTHER; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeFamilyInformation.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeFamilyInformation.java index 49d89ac6e2..4c41b818a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeFamilyInformation.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeFamilyInformation.java @@ -25,7 +25,7 @@ public class JdbcTypeFamilyInformation { BINARY( Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY/*, SqlTypes.LONG32VARBINARY*/ ), NUMERIC( Types.BIGINT, Types.DECIMAL, Types.DOUBLE, Types.FLOAT, Types.INTEGER, Types.NUMERIC, Types.REAL, Types.SMALLINT, Types.TINYINT ), CHARACTER( Types.CHAR, Types.LONGNVARCHAR, Types.LONGVARCHAR, /*SqlTypes.LONG32NVARCHAR ,SqlTypes.LONG32VARCHAR,*/ Types.NCHAR, Types.NVARCHAR, Types.VARCHAR ), - DATETIME( Types.DATE, Types.TIME, Types.TIMESTAMP ), + DATETIME( Types.DATE, Types.TIME, Types.TIME_WITH_TIMEZONE, Types.TIMESTAMP, Types.TIMESTAMP_WITH_TIMEZONE ), CLOB( Types.CLOB, Types.NCLOB ); private final int[] typeCodes; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java index 4fda5c2338..c36344037b 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java @@ -193,6 +193,28 @@ public interface JdbcTypeIndicators { return getTypeConfiguration().getCurrentBaseSqlTypeIndicators(); } + /** + * @return the SQL column type used for storing times under the + * given {@linkplain TimeZoneStorageStrategy storage strategy} + * + * @see SqlTypes#TIME_WITH_TIMEZONE + * @see SqlTypes#TIME + * @see SqlTypes#TIME_UTC + */ + static int getZonedTimeSqlType(TimeZoneStorageStrategy storageStrategy) { + switch ( storageStrategy ) { + case NATIVE: + return SqlTypes.TIME_WITH_TIMEZONE; + case COLUMN: + case NORMALIZE: + return SqlTypes.TIME; + case NORMALIZE_UTC: + return SqlTypes.TIME_UTC; + default: + throw new AssertionFailure( "unknown time zone storage strategy" ); + } + } + /** * @return the SQL column type used for storing datetimes under the * given {@linkplain TimeZoneStorageStrategy storage strategy} @@ -216,6 +238,18 @@ public interface JdbcTypeIndicators { } } + /** + * @return the SQL column type used for storing datetimes under the + * default {@linkplain TimeZoneStorageStrategy storage strategy} + * + * @see SqlTypes#TIME_WITH_TIMEZONE + * @see SqlTypes#TIME + * @see SqlTypes#TIME_UTC + */ + default int getDefaultZonedTimeSqlType() { + return getZonedTimeSqlType( getDefaultTimeZoneStorageStrategy() ); + } + /** * @return the SQL column type used for storing datetimes under the * default {@linkplain TimeZoneStorageStrategy storage strategy} @@ -228,7 +262,7 @@ public interface JdbcTypeIndicators { final TemporalType temporalPrecision = getTemporalPrecision(); switch ( temporalPrecision == null ? TemporalType.TIMESTAMP : temporalPrecision ) { case TIME: - return Types.TIME; + return getZonedTimeSqlType( getDefaultTimeZoneStorageStrategy() ); case DATE: return Types.DATE; case TIMESTAMP: diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeJavaClassMappings.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeJavaClassMappings.java index 30ae7126c2..778d8904b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeJavaClassMappings.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeJavaClassMappings.java @@ -26,6 +26,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.UUID; @@ -124,6 +125,7 @@ public class JdbcTypeJavaClassMappings { workMap.put( Time.class, SqlTypes.TIME ); workMap.put( Timestamp.class, SqlTypes.TIMESTAMP ); workMap.put( LocalTime.class, SqlTypes.TIME ); + workMap.put( OffsetTime.class, SqlTypes.TIME_WITH_TIMEZONE ); workMap.put( LocalDate.class, SqlTypes.DATE ); workMap.put( LocalDateTime.class, SqlTypes.TIMESTAMP ); workMap.put( OffsetDateTime.class, SqlTypes.TIMESTAMP_WITH_TIMEZONE ); @@ -181,6 +183,8 @@ public class JdbcTypeJavaClassMappings { workMap.put( SqlTypes.DATE, java.sql.Date.class ); workMap.put( SqlTypes.TIME, Time.class ); workMap.put( SqlTypes.TIMESTAMP, Timestamp.class ); + workMap.put( SqlTypes.TIME_WITH_TIMEZONE, OffsetTime.class ); + workMap.put( SqlTypes.TIMESTAMP_WITH_TIMEZONE, OffsetDateTime.class ); workMap.put( SqlTypes.BLOB, Blob.class ); workMap.put( SqlTypes.CLOB, Clob.class ); workMap.put( SqlTypes.NCLOB, NClob.class ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeUtcAsJdbcTimeJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeUtcAsJdbcTimeJdbcType.java new file mode 100644 index 0000000000..cb9071ec9f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeUtcAsJdbcTimeJdbcType.java @@ -0,0 +1,121 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Types; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.util.Calendar; +import java.util.TimeZone; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.TemporalType; + +/** + * Descriptor for {@link SqlTypes#TIME_UTC TIME_UTC} handling. + * + * @author Christian Beikov + */ +public class TimeUtcAsJdbcTimeJdbcType implements JdbcType { + + public static final TimeUtcAsJdbcTimeJdbcType INSTANCE = new TimeUtcAsJdbcTimeJdbcType(); + private static final Calendar UTC_CALENDAR = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + + public TimeUtcAsJdbcTimeJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.TIME; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.TIME_UTC; + } + + @Override + public String getFriendlyName() { + return "TIME_UTC"; + } + + @Override + public String toString() { + return "TimeUtcDescriptor"; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer length, + Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( OffsetTime.class ); + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return OffsetTime.class; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIME ); + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final OffsetTime offsetTime = javaType.unwrap( value, OffsetTime.class, options ); + st.setTime( index, Time.valueOf( offsetTime.withOffsetSameInstant( ZoneOffset.UTC ).toLocalTime() ), UTC_CALENDAR ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final OffsetTime offsetTime = javaType.unwrap( value, OffsetTime.class, options ); + st.setTime( name, Time.valueOf( offsetTime.withOffsetSameInstant( ZoneOffset.UTC ).toLocalTime() ), UTC_CALENDAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + final Time time = rs.getTime( paramIndex, UTC_CALENDAR ); + return javaType.wrap( time == null ? null : time.toLocalTime().atOffset( ZoneOffset.UTC ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + final Time time = statement.getTime( index, UTC_CALENDAR ); + return javaType.wrap( time == null ? null : time.toLocalTime().atOffset( ZoneOffset.UTC ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + final Time time = statement.getTime( name, UTC_CALENDAR ); + return javaType.wrap( time == null ? null : time.toLocalTime().atOffset( ZoneOffset.UTC ), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeUtcAsOffsetTimeJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeUtcAsOffsetTimeJdbcType.java new file mode 100644 index 0000000000..a6a6b40f35 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeUtcAsOffsetTimeJdbcType.java @@ -0,0 +1,114 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.OffsetTime; +import java.time.ZoneOffset; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.TemporalType; + +/** + * Descriptor for {@link SqlTypes#TIME_UTC TIME_UTC} handling. + * + * @author Christian Beikov + */ +public class TimeUtcAsOffsetTimeJdbcType implements JdbcType { + + public static final TimeUtcAsOffsetTimeJdbcType INSTANCE = new TimeUtcAsOffsetTimeJdbcType(); + + public TimeUtcAsOffsetTimeJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.TIME_WITH_TIMEZONE; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.TIME_UTC; + } + + @Override + public String getFriendlyName() { + return "TIME_UTC"; + } + + @Override + public String toString() { + return "TimeUtcDescriptor"; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer length, + Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( OffsetTime.class ); + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return OffsetTime.class; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIME ); + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final OffsetTime offsetTime = javaType.unwrap( value, OffsetTime.class, options ); + st.setObject( index, offsetTime.withOffsetSameInstant( ZoneOffset.UTC ), Types.TIME_WITH_TIMEZONE ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final OffsetTime offsetTime = javaType.unwrap( value, OffsetTime.class, options ); + st.setObject( name, offsetTime.withOffsetSameInstant( ZoneOffset.UTC ), Types.TIME_WITH_TIMEZONE ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap( rs.getObject( paramIndex, OffsetTime.class ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return javaType.wrap( statement.getObject( index, OffsetTime.class ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return javaType.wrap( statement.getObject( name, OffsetTime.class ), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeWithTimeZoneJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeWithTimeZoneJdbcType.java new file mode 100644 index 0000000000..de8e462628 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimeWithTimeZoneJdbcType.java @@ -0,0 +1,108 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.OffsetTime; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.TemporalType; + +/** + * Descriptor for {@link SqlTypes#TIME_WITH_TIMEZONE TIME_WITH_TIMEZONE} handling. + * + * @author Christian Beikov + */ +public class TimeWithTimeZoneJdbcType implements JdbcType { + + public static final TimeWithTimeZoneJdbcType INSTANCE = new TimeWithTimeZoneJdbcType(); + + public TimeWithTimeZoneJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.TIME_WITH_TIMEZONE; + } + + @Override + public String getFriendlyName() { + return "TIME_WITH_TIMEZONE"; + } + + @Override + public String toString() { + return "TimeWithTimezoneDescriptor"; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer length, + Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( OffsetTime.class ); + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return OffsetTime.class; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIME ); + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final OffsetTime offsetTime = javaType.unwrap( value, OffsetTime.class, options ); + st.setObject( index, offsetTime, Types.TIME_WITH_TIMEZONE ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final OffsetTime offsetTime = javaType.unwrap( value, OffsetTime.class, options ); + st.setObject( name, offsetTime, Types.TIME_WITH_TIMEZONE ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap( rs.getObject( paramIndex, OffsetTime.class ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return javaType.wrap( statement.getObject( index, OffsetTime.class ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return javaType.wrap( statement.getObject( name, OffsetTime.class ), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsInstantJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsInstantJdbcType.java new file mode 100644 index 0000000000..22494f8674 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsInstantJdbcType.java @@ -0,0 +1,159 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.util.Calendar; +import java.util.TimeZone; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.TemporalType; + +/** + * Descriptor for {@link SqlTypes#TIMESTAMP_UTC TIMESTAMP_UTC} handling. + * + * @author Christian Beikov + */ +public class TimestampUtcAsInstantJdbcType implements JdbcType { + public static final TimestampUtcAsInstantJdbcType INSTANCE = new TimestampUtcAsInstantJdbcType(); + private static final Calendar UTC_CALENDAR = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + + public TimestampUtcAsInstantJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.TIMESTAMP_WITH_TIMEZONE; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.TIMESTAMP_UTC; + } + + @Override + public String getFriendlyName() { + return "TIMESTAMP_UTC"; + } + + @Override + public String toString() { + return "TimestampUtcDescriptor"; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer length, + Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( Instant.class ); + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return Instant.class; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIMESTAMP ); + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind( + PreparedStatement st, + X value, + int index, + WrapperOptions wrapperOptions) throws SQLException { + final Instant instant = javaType.unwrap( value, Instant.class, wrapperOptions ); + try { + // supposed to be supported in JDBC 4.2 + st.setObject( index, instant, Types.TIMESTAMP_WITH_TIMEZONE ); + } + catch (SQLException|AbstractMethodError e) { + // fall back to treating it as a JDBC Timestamp + st.setTimestamp( index, Timestamp.from( instant ), UTC_CALENDAR ); + } + } + + @Override + protected void doBind( + CallableStatement st, + X value, + String name, + WrapperOptions wrapperOptions) + throws SQLException { + final Instant instant = javaType.unwrap( value, Instant.class, wrapperOptions ); + try { + // supposed to be supported in JDBC 4.2 + st.setObject( name, instant, Types.TIMESTAMP_WITH_TIMEZONE ); + } + catch (SQLException|AbstractMethodError e) { + // fall back to treating it as a JDBC Timestamp + st.setTimestamp( name, Timestamp.from( instant ), UTC_CALENDAR ); + } + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int position, WrapperOptions wrapperOptions) throws SQLException { + try { + // supposed to be supported in JDBC 4.2 + return javaType.wrap( rs.getObject( position, Instant.class ), wrapperOptions ); + } + catch (SQLException|AbstractMethodError e) { + // fall back to treating it as a JDBC Timestamp + return javaType.wrap( rs.getTimestamp( position, UTC_CALENDAR ), wrapperOptions ); + } + } + + @Override + protected X doExtract(CallableStatement statement, int position, WrapperOptions wrapperOptions) throws SQLException { + try { + // supposed to be supported in JDBC 4.2 + return javaType.wrap( statement.getObject( position, Instant.class ), wrapperOptions ); + } + catch (SQLException|AbstractMethodError e) { + // fall back to treating it as a JDBC Timestamp + return javaType.wrap( statement.getTimestamp( position, UTC_CALENDAR ), wrapperOptions ); + } + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions wrapperOptions) throws SQLException { + try { + // supposed to be supported in JDBC 4.2 + return javaType.wrap( statement.getObject( name, Instant.class ), wrapperOptions ); + } + catch (SQLException|AbstractMethodError e) { + // fall back to treating it as a JDBC Timestamp + return javaType.wrap( statement.getTimestamp( name, UTC_CALENDAR ), wrapperOptions ); + } + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java new file mode 100644 index 0000000000..fa2b7f2155 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java @@ -0,0 +1,120 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.util.Calendar; +import java.util.TimeZone; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.TemporalType; + +/** + * Descriptor for {@link SqlTypes#TIMESTAMP_UTC TIMESTAMP_UTC} handling. + * + * @author Christian Beikov + */ +public class TimestampUtcAsJdbcTimestampJdbcType implements JdbcType { + + public static final TimestampUtcAsJdbcTimestampJdbcType INSTANCE = new TimestampUtcAsJdbcTimestampJdbcType(); + private static final Calendar UTC_CALENDAR = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + + public TimestampUtcAsJdbcTimestampJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.TIMESTAMP; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.TIMESTAMP_UTC; + } + + @Override + public String getFriendlyName() { + return "TIMESTAMP_UTC"; + } + + @Override + public String toString() { + return "TimestampUtcDescriptor"; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer length, + Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( Instant.class ); + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return Instant.class; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIMESTAMP ); + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final Instant instant = javaType.unwrap( value, Instant.class, options ); + st.setTimestamp( index, Timestamp.from( instant ), UTC_CALENDAR ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final Instant instant = javaType.unwrap( value, Instant.class, options ); + st.setTimestamp( name, Timestamp.from( instant ), UTC_CALENDAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + final Timestamp timestamp = rs.getTimestamp( paramIndex, UTC_CALENDAR ); + return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + final Timestamp timestamp = statement.getTimestamp( index, UTC_CALENDAR ); + return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + final Timestamp timestamp = statement.getTimestamp( name, UTC_CALENDAR ); + return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsOffsetDateTimeJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsOffsetDateTimeJdbcType.java new file mode 100644 index 0000000000..35b53c557c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsOffsetDateTimeJdbcType.java @@ -0,0 +1,125 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterTemporal; +import org.hibernate.type.spi.TypeConfiguration; + +import jakarta.persistence.TemporalType; + +/** + * Descriptor for {@link SqlTypes#TIMESTAMP_UTC TIMESTAMP_UTC} handling. + * + * @author Christian Beikov + */ +public class TimestampUtcAsOffsetDateTimeJdbcType implements JdbcType { + + public static final TimestampUtcAsOffsetDateTimeJdbcType INSTANCE = new TimestampUtcAsOffsetDateTimeJdbcType(); + + public TimestampUtcAsOffsetDateTimeJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.TIMESTAMP_WITH_TIMEZONE; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.TIMESTAMP_UTC; + } + + @Override + public String getFriendlyName() { + return "TIMESTAMP_UTC"; + } + + @Override + public String toString() { + return "TimestampUtcDescriptor"; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping( + Integer length, + Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor( Instant.class ); + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return OffsetDateTime.class; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + return new JdbcLiteralFormatterTemporal<>( javaType, TemporalType.TIMESTAMP ); + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind( + PreparedStatement st, + X value, + int index, + WrapperOptions wrapperOptions) throws SQLException { + final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, wrapperOptions ); + // supposed to be supported in JDBC 4.2 + st.setObject( index, dateTime.withOffsetSameInstant( ZoneOffset.UTC ), Types.TIMESTAMP_WITH_TIMEZONE ); + } + + @Override + protected void doBind( + CallableStatement st, + X value, + String name, + WrapperOptions wrapperOptions) + throws SQLException { + final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, wrapperOptions ); + // supposed to be supported in JDBC 4.2 + st.setObject( name, dateTime.withOffsetSameInstant( ZoneOffset.UTC ), Types.TIMESTAMP_WITH_TIMEZONE ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int position, WrapperOptions wrapperOptions) throws SQLException { + return javaType.wrap( rs.getObject( position, OffsetDateTime.class ), wrapperOptions ); + } + + @Override + protected X doExtract(CallableStatement statement, int position, WrapperOptions wrapperOptions) throws SQLException { + return javaType.wrap( statement.getObject( position, OffsetDateTime.class ), wrapperOptions ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions wrapperOptions) throws SQLException { + return javaType.wrap( statement.getObject( name, OffsetDateTime.class ), wrapperOptions ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampWithTimeZoneJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampWithTimeZoneJdbcType.java index 380213f9f5..b775ad000f 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampWithTimeZoneJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampWithTimeZoneJdbcType.java @@ -22,6 +22,7 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.time.OffsetDateTime; +import java.util.Calendar; /** * Descriptor for {@link Types#TIMESTAMP_WITH_TIMEZONE TIMESTAMP_WITH_TIMEZONE} handling. @@ -75,16 +76,24 @@ public class TimestampWithTimeZoneJdbcType implements JdbcType { PreparedStatement st, X value, int index, - WrapperOptions wrapperOptions) throws SQLException { + WrapperOptions options) throws SQLException { try { - final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, wrapperOptions ); + final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, options ); // supposed to be supported in JDBC 4.2 st.setObject( index, dateTime, Types.TIMESTAMP_WITH_TIMEZONE ); } catch (SQLException|AbstractMethodError e) { // fall back to treating it as a JDBC Timestamp - final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, wrapperOptions ); - st.setTimestamp( index, timestamp ); + final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, options ); + if ( value instanceof Calendar ) { + st.setTimestamp( index, timestamp, (Calendar) value ); + } + else if ( options.getJdbcTimeZone() != null ) { + st.setTimestamp( index, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ) ); + } + else { + st.setTimestamp( index, timestamp ); + } } } @@ -93,17 +102,25 @@ public class TimestampWithTimeZoneJdbcType implements JdbcType { CallableStatement st, X value, String name, - WrapperOptions wrapperOptions) + WrapperOptions options) throws SQLException { try { - final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, wrapperOptions ); + final OffsetDateTime dateTime = javaType.unwrap( value, OffsetDateTime.class, options ); // supposed to be supported in JDBC 4.2 st.setObject( name, dateTime, Types.TIMESTAMP_WITH_TIMEZONE ); } catch (SQLException|AbstractMethodError e) { // fall back to treating it as a JDBC Timestamp - final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, wrapperOptions ); - st.setTimestamp( name, timestamp ); + final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, options ); + if ( value instanceof Calendar ) { + st.setTimestamp( name, timestamp, (Calendar) value ); + } + else if ( options.getJdbcTimeZone() != null ) { + st.setTimestamp( name, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ) ); + } + else { + st.setTimestamp( name, timestamp ); + } } } }; @@ -113,38 +130,44 @@ public class TimestampWithTimeZoneJdbcType implements JdbcType { public ValueExtractor getExtractor(final JavaType javaType) { return new BasicExtractor<>( javaType, this ) { @Override - protected X doExtract(ResultSet rs, int position, WrapperOptions wrapperOptions) throws SQLException { + protected X doExtract(ResultSet rs, int position, WrapperOptions options) throws SQLException { try { // supposed to be supported in JDBC 4.2 - return javaType.wrap( rs.getObject( position, OffsetDateTime.class ), wrapperOptions ); + return javaType.wrap( rs.getObject( position, OffsetDateTime.class ), options ); } catch (SQLException|AbstractMethodError e) { // fall back to treating it as a JDBC Timestamp - return javaType.wrap( rs.getTimestamp( position ), wrapperOptions ); + return options.getJdbcTimeZone() != null ? + javaType.wrap( rs.getTimestamp( position, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) : + javaType.wrap( rs.getTimestamp( position ), options ); } } @Override - protected X doExtract(CallableStatement statement, int position, WrapperOptions wrapperOptions) throws SQLException { + protected X doExtract(CallableStatement statement, int position, WrapperOptions options) throws SQLException { try { // supposed to be supported in JDBC 4.2 - return javaType.wrap( statement.getObject( position, OffsetDateTime.class ), wrapperOptions ); + return javaType.wrap( statement.getObject( position, OffsetDateTime.class ), options ); } catch (SQLException|AbstractMethodError e) { // fall back to treating it as a JDBC Timestamp - return javaType.wrap( statement.getTimestamp( position ), wrapperOptions ); + return options.getJdbcTimeZone() != null ? + javaType.wrap( statement.getTimestamp( position, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) : + javaType.wrap( statement.getTimestamp( position ), options ); } } @Override - protected X doExtract(CallableStatement statement, String name, WrapperOptions wrapperOptions) throws SQLException { + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { try { // supposed to be supported in JDBC 4.2 - return javaType.wrap( statement.getObject( name, OffsetDateTime.class ), wrapperOptions ); + return javaType.wrap( statement.getObject( name, OffsetDateTime.class ), options ); } catch (SQLException|AbstractMethodError e) { // fall back to treating it as a JDBC Timestamp - return javaType.wrap( statement.getTimestamp( name ), wrapperOptions ); + return options.getJdbcTimeZone() != null ? + javaType.wrap( statement.getTimestamp( name, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) : + javaType.wrap( statement.getTimestamp( name ), options ); } } }; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/internal/JdbcTypeBaseline.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/internal/JdbcTypeBaseline.java index 03a5b03eac..2a276c66e7 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/internal/JdbcTypeBaseline.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/internal/JdbcTypeBaseline.java @@ -28,6 +28,7 @@ import org.hibernate.type.descriptor.jdbc.RealJdbcType; import org.hibernate.type.descriptor.jdbc.RowIdJdbcType; import org.hibernate.type.descriptor.jdbc.SmallIntJdbcType; import org.hibernate.type.descriptor.jdbc.TimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimeWithTimeZoneJdbcType; import org.hibernate.type.descriptor.jdbc.TimestampJdbcType; import org.hibernate.type.descriptor.jdbc.TimestampWithTimeZoneJdbcType; import org.hibernate.type.descriptor.jdbc.TinyIntJdbcType; @@ -64,6 +65,7 @@ public class JdbcTypeBaseline { target.addDescriptor( TimestampJdbcType.INSTANCE ); target.addDescriptor( TimestampWithTimeZoneJdbcType.INSTANCE ); target.addDescriptor( TimeJdbcType.INSTANCE ); + target.addDescriptor( TimeWithTimeZoneJdbcType.INSTANCE ); target.addDescriptor( BinaryJdbcType.INSTANCE ); target.addDescriptor( VarbinaryJdbcType.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/DdlTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/DdlTypeImpl.java index d3c0c8476f..9846e22a25 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/DdlTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/DdlTypeImpl.java @@ -56,7 +56,7 @@ public class DdlTypeImpl implements DdlType { final int paren = typeNamePattern.indexOf( '(' ); if ( paren > 0 ) { final int parenEnd = typeNamePattern.lastIndexOf( ')' ); - return parenEnd == typeNamePattern.length() + return parenEnd + 1 == typeNamePattern.length() ? typeNamePattern.substring( 0, paren ) : ( typeNamePattern.substring( 0, paren ) + typeNamePattern.substring( parenEnd + 1 ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/spi/DdlTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/spi/DdlTypeRegistry.java index ab08cd7439..9ded8ec141 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/spi/DdlTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/spi/DdlTypeRegistry.java @@ -155,6 +155,9 @@ public class DdlTypeRegistry implements Serializable { return getTypeName( typeCode, Size.precision( dialect.getFloatPrecision() ) ); case SqlTypes.DOUBLE: return getTypeName( typeCode, Size.precision( dialect.getDoublePrecision() ) ); + case SqlTypes.TIME: + case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: case SqlTypes.TIMESTAMP: case SqlTypes.TIMESTAMP_WITH_TIMEZONE: case SqlTypes.TIMESTAMP_UTC: diff --git a/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java index ceb8827697..400afdc749 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java +++ b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java @@ -776,6 +776,7 @@ public class TypeConfiguration implements SessionFactoryObserver, Serializable { return TemporalType.TIMESTAMP; case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: + case SqlTypes.TIME_UTC: return TemporalType.TIME; case SqlTypes.DATE: return TemporalType.DATE; diff --git a/hibernate-core/src/main/java/org/hibernate/usertype/internal/OffsetTimeCompositeUserType.java b/hibernate-core/src/main/java/org/hibernate/usertype/internal/OffsetTimeCompositeUserType.java new file mode 100644 index 0000000000..0f29a75eb2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/usertype/internal/OffsetTimeCompositeUserType.java @@ -0,0 +1,62 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.usertype.internal; + +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; + +import org.hibernate.HibernateException; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.TimeZoneStorage; +import org.hibernate.annotations.TimeZoneStorageType; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.spi.ValueAccess; +import org.hibernate.type.SqlTypes; + +/** + * @author Christian Beikov + */ +public class OffsetTimeCompositeUserType extends AbstractTimeZoneStorageCompositeUserType { + + public static final String LOCAL_TIME_NAME = "utcTime"; + + @Override + public Object getPropertyValue(OffsetTime component, int property) throws HibernateException { + switch ( property ) { + case 0: + return component.withOffsetSameInstant( ZoneOffset.UTC ); + case 1: + return component.getOffset(); + } + return null; + } + + @Override + public OffsetTime instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) { + final OffsetTime utcTime = values.getValue( 0, OffsetTime.class ); + final ZoneOffset zoneOffset = values.getValue( 1, ZoneOffset.class ); + return utcTime == null || zoneOffset == null ? null : utcTime.withOffsetSameInstant( zoneOffset ); + } + + @Override + public Class embeddable() { + return OffsetTimeEmbeddable.class; + } + + @Override + public Class returnedClass() { + return OffsetTime.class; + } + + public static class OffsetTimeEmbeddable { + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + private OffsetTime utcTime; + @JdbcTypeCode( SqlTypes.INTEGER ) + private ZoneOffset zoneOffset; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java index fa90f20488..985f9b0e9e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java @@ -11,8 +11,13 @@ import java.time.OffsetTime; import java.time.ZoneOffset; import java.util.List; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.type.descriptor.DateTimeUtils; + +import org.hibernate.testing.orm.junit.DialectContext; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.Setting; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,11 +30,15 @@ import jakarta.persistence.TypedQuery; import static org.junit.jupiter.api.Assertions.assertEquals; @Jpa( - annotatedClasses = LocalTimeTest.TestEntity.class + annotatedClasses = LocalTimeTest.TestEntity.class, + properties = @Setting(name = AvailableSettings.TIMEZONE_DEFAULT_STORAGE, value = "NORMALIZE") ) public class LocalTimeTest { - private static final LocalTime LOCAL_TIME = LocalTime.now(); + private static final LocalTime LOCAL_TIME = DateTimeUtils.roundToDefaultPrecision( + LocalTime.now(), + DialectContext.getDialect() + ); private static final OffsetTime OFFSET_TIME = OffsetTime.of( LOCAL_TIME, ZoneOffset.ofHours( 2 ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/OffsetTimeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/OffsetTimeTest.java index ececb46940..dd3bde471c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/OffsetTimeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/OffsetTimeTest.java @@ -26,6 +26,9 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import org.hibernate.annotations.TimeZoneStorageType; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; @@ -41,6 +44,12 @@ import org.junit.runners.Parameterized; */ public class OffsetTimeTest extends AbstractJavaTimeTypeTest { + @Override + protected void configure(Configuration configuration) { + super.configure(configuration); + configuration.setProperty( AvailableSettings.TIMEZONE_DEFAULT_STORAGE, TimeZoneStorageType.NORMALIZE.toString() ); + } + private static class ParametersBuilder extends AbstractParametersBuilder { public ParametersBuilder add(int hour, int minute, int second, int nanosecond, String offset, ZoneId defaultTimeZone) { if ( !isNanosecondPrecisionSupported() ) { diff --git a/migration-guide.adoc b/migration-guide.adoc index 2336ed5fc5..36f1210a48 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -14,4 +14,25 @@ earlier versions, see any other pertinent migration guides as well. * link:{docsBase}/6.1/migration-guide/migration-guide.html[6.1 Migration guide] * link:{docsBase}/6.0/migration-guide/migration-guide.html[6.0 Migration guide] +[[ddl-changes]] +== DDL type changes + +[[ddl-offset-time]] +=== OffsetTime mapping changes + +`OffsetTime` now depends on `@TimeZoneStorage` and the `hibernate.timezone.default_storage` setting. +Since the default for this setting is now `TimeZoneStorageType.DEFAULT`, this means that the DDL expectations for such columns changed. + +If the target database supports time zone types natively like H2, Oracle, SQL Server and DB2 z/OS, +the type code `SqlTypes.TIME_WITH_TIMEZONE` is now used, which maps to the DDL type `time with time zone`. + +Due to this change, schema validation errors could occur on existing databases. + +The migration to `time with time zone` requires a migration expression like `cast(old as time with time zone)` +which will interpret the previous time as local time and compute the offset for the `time with time zone` based on the current date +and time zone settings of your database session. + +If the target database does not support time zone types natively, Hibernate behaves just like before. + +To retain backwards compatibility, configure the setting `hibernate.timezone.default_storage` to `NORMALIZE`.