From 49bb1281208824748e3946445650aa2821f6d27a Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Tue, 8 Aug 2023 09:46:01 -0500 Subject: [PATCH] [#1442] Add test for TimeZoneStorage - Disabled for SQLServer which does not support java.time.OffsetTime --- .../timezones/TimeZoneStorageMappingTest.java | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/TimeZoneStorageMappingTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/TimeZoneStorageMappingTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/TimeZoneStorageMappingTest.java new file mode 100644 index 000000000..4de3464be --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/timezones/TimeZoneStorageMappingTest.java @@ -0,0 +1,301 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.timezones; + +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; +import java.util.Collection; +import java.util.List; + +import org.hibernate.annotations.TimeZoneColumn; +import org.hibernate.annotations.TimeZoneStorage; +import org.hibernate.annotations.TimeZoneStorageType; +import org.hibernate.cfg.Configuration; +import org.hibernate.reactive.BaseReactiveTest; +import org.hibernate.reactive.testing.DBSelectionExtension; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.cfg.AvailableSettings.TIMEZONE_DEFAULT_STORAGE; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; + +/** + * Adapted from org.hibernate.orm.test.mapping.basic.TimeZoneStorageMappingTests + * + *

+ * Note that the tests below do not use ORM's annotations below to calculate + * if a Dialect supports Format or Timezone types: + * @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFormat.class) + * @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTimezoneTypes.class....) + *

+ *

+ * It appears that DB2, SQLServer and Oracle do not yet support FORMAT and none of reactive's supported Dialects + * support Timezone Types via Offset, so the ORM's testNormalizeOffset(...) method is not included in these tests. + *

+ */ +@Timeout(value = 10, timeUnit = MINUTES) +public class TimeZoneStorageMappingTest extends BaseReactiveTest { + + // SQLSERVER currently does not support java.time.OffsetTime + @RegisterExtension + public DBSelectionExtension selectionRule = DBSelectionExtension.skipTestsFor( SQLSERVER ); + + 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, + 3, + 1, + 12, + 0, + 0 + ), + ZoneOffset.ofHoursMinutes( 5, 45 ) + ); + private static final ZonedDateTime ZONED_DATE_TIME = ZonedDateTime.of( + LocalDateTime.of( + 2022, + 3, + 1, + 12, + 0, + 0 + ), + 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" ); + + @Override + protected Collection> annotatedEntities() { + return List.of( TimeZoneStorageEntity.class ); + } + + @Override + protected void setProperties(Configuration configuration) { + super.setProperties( configuration ); + configuration.setProperty( TIMEZONE_DEFAULT_STORAGE, "AUTO" ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + TimeZoneStorageEntity entity = new TimeZoneStorageEntity( 1, OFFSET_TIME, OFFSET_DATE_TIME, ZONED_DATE_TIME ); + + test( context, getMutinySessionFactory().withTransaction( (s, t) -> s.persist( entity ) ) ); + } + + @Test + public void testOffsetRetainedAuto(VertxTestContext context) { + testOffsetRetained( context, "Auto" ); + } + + @Test + public void testOffsetRetainedColumn(VertxTestContext context) { + testOffsetRetained( context, "Column" ); + } + + @Test + public void testOffsetRetainedFormatAuto(VertxTestContext context) { + testOffsetRetainedFormat( context, "Auto" ); + } + + @Test + public void testOffsetRetainedFormatColumn(VertxTestContext context) { + testOffsetRetainedFormat( context, "Column" ); + } + + public void testOffsetRetained(VertxTestContext context, String suffix) { + test( context, openSession() + .thenCompose( session -> 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 + ).getSingleResult() + .thenAccept( result -> { + assertThat( result.get( 0, OffsetTime.class ) ).isEqualTo( OFFSET_TIME ); + assertThat( result.get( 1, OffsetDateTime.class ) ).isEqualTo( OFFSET_DATE_TIME ); + assertThat( result.get( 2, ZonedDateTime.class ) ).isEqualTo( ZONED_DATE_TIME ); + assertThat( result.get( 3, ZoneOffset.class ) ).isEqualTo( OFFSET_TIME.getOffset() ); + assertThat( result.get( 4, ZoneOffset.class ) ).isEqualTo( OFFSET_DATE_TIME.getOffset() ); + assertThat( result.get( 5, ZoneOffset.class ) ).isEqualTo( ZONED_DATE_TIME.getOffset() ); + assertThat( result.get( 6, OffsetTime.class ) ).isEqualTo( OFFSET_TIME.plusHours( 1L ) ); + assertThat( result.get( 7, OffsetDateTime.class ) ).isEqualTo( OFFSET_DATE_TIME.plusHours( 1L ) ); + assertThat( result.get( 8, ZonedDateTime.class ) ).isEqualTo( ZONED_DATE_TIME.plusHours( 1L ) ); + assertThat( result.get( 9, Duration.class ) ).isEqualTo( Duration.ofHours( 1L ) ); + assertThat( result.get( 10, Duration.class ) ).isEqualTo( Duration.ofHours( 1L ) ); + assertThat( result.get( 11, Duration.class ) ).isEqualTo( Duration.ofHours( 1L ) ); + } ) + ) + ); + } + + public void testOffsetRetainedFormat(VertxTestContext context, String suffix) { + test( context, openSession() + .thenCompose( session -> 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 + ).getSingleResult() + .thenAccept( result -> { + assertThat( result.get( 0, String.class ) ).isEqualTo( TIME_FORMATTER.format( OFFSET_TIME ) ); + assertThat( result.get( 1, String.class ) ).isEqualTo( FORMATTER.format( OFFSET_DATE_TIME ) ); + assertThat( result.get( 2, String.class ) ).isEqualTo( FORMATTER.format( ZONED_DATE_TIME ) ); + } ) + ) + ); + } + + @Test + public void testNormalize(VertxTestContext context) { + test( context, openSession() + .thenCompose( session -> session.createQuery( + "select " + + "e.offsetTimeNormalized, " + + "e.offsetDateTimeNormalized, " + + "e.zonedDateTimeNormalized, " + + "e.offsetTimeNormalizedUtc, " + + "e.offsetDateTimeNormalizedUtc, " + + "e.zonedDateTimeNormalizedUtc " + + "from TimeZoneStorageEntity e", + Tuple.class + ).getSingleResult() + .thenAccept( result -> { + assertThat( result.get( 0, OffsetTime.class ).toLocalTime()).isEqualTo( OFFSET_TIME.withOffsetSameInstant( JVM_TIMEZONE_OFFSET ).toLocalTime() ); + assertThat( result.get( 0, OffsetTime.class ).getOffset()).isEqualTo( JVM_TIMEZONE_OFFSET ); + assertThat( result.get( 1, OffsetDateTime.class ).toInstant()).isEqualTo( OFFSET_DATE_TIME.toInstant() ); + assertThat( result.get( 2, ZonedDateTime.class ).toInstant()).isEqualTo( ZONED_DATE_TIME.toInstant() ); + assertThat( result.get( 3, OffsetTime.class ).toLocalTime()).isEqualTo( OFFSET_TIME.withOffsetSameInstant( ZoneOffset.UTC ).toLocalTime() ); + assertThat( result.get( 3, OffsetTime.class ).getOffset()).isEqualTo( ZoneOffset.UTC ); + assertThat( result.get( 4, OffsetDateTime.class ).toInstant()).isEqualTo( OFFSET_DATE_TIME.toInstant() ); + assertThat( result.get( 5, ZonedDateTime.class ).toInstant()).isEqualTo( ZONED_DATE_TIME.toInstant() ); + } + ) + ) + ); + } + + @Entity(name = "TimeZoneStorageEntity") + @Table(name = "TimeZoneStorageEntity") + public static class TimeZoneStorageEntity { + @Id + public Integer id; + + //tag::time-zone-column-examples-mapping-example[] + @TimeZoneStorage(TimeZoneStorageType.COLUMN) + @TimeZoneColumn(name = "birthtime_offset_offset") + @Column(name = "birthtime_offset") + public OffsetTime offsetTimeColumn; + + @TimeZoneStorage(TimeZoneStorageType.COLUMN) + @TimeZoneColumn(name = "birthday_offset_offset") + @Column(name = "birthday_offset") + public OffsetDateTime offsetDateTimeColumn; + + @TimeZoneStorage(TimeZoneStorageType.COLUMN) + @TimeZoneColumn(name = "birthday_zoned_offset") + @Column(name = "birthday_zoned") + public ZonedDateTime zonedDateTimeColumn; + //end::time-zone-column-examples-mapping-example[] + + @TimeZoneStorage + @Column(name = "birthtime_offset_auto") + public OffsetTime offsetTimeAuto; + + @TimeZoneStorage + @Column(name = "birthday_offset_auto") + public OffsetDateTime offsetDateTimeAuto; + + @TimeZoneStorage + @Column(name = "birthday_zoned_auto") + public ZonedDateTime zonedDateTimeAuto; + + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE) + @Column(name = "birthtime_offset_normalized") + public OffsetTime offsetTimeNormalized; + + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE) + @Column(name = "birthday_offset_normalized") + public OffsetDateTime offsetDateTimeNormalized; + + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE) + @Column(name = "birthday_zoned_normalized") + public ZonedDateTime zonedDateTimeNormalized; + + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @Column(name = "birthtime_offset_utc") + public OffsetTime offsetTimeNormalizedUtc; + + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @Column(name = "birthday_offset_utc") + public OffsetDateTime offsetDateTimeNormalizedUtc; + + @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC) + @Column(name = "birthday_zoned_utc") + private ZonedDateTime zonedDateTimeNormalizedUtc; + + public TimeZoneStorageEntity() { + } + + 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; + } + } +}