diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/EntitySeven.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/EntitySeven.java new file mode 100644 index 0000000000..4a9a9c7b71 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/EntitySeven.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.uuid.rfc9562; + +import java.util.UUID; + +import org.hibernate.annotations.UuidGenerator; +import org.hibernate.id.uuid.UuidVersion7Strategy; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity(name = "EntitySeven") +@Table(name = "entity_seven") +public class EntitySeven { + @Id + @UuidGenerator(algorithm = UuidVersion7Strategy.class) + public UUID id; + @Basic + public String name; + + private EntitySeven() { + // for Hibernate use + } + + public EntitySeven(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/EntitySix.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/EntitySix.java new file mode 100644 index 0000000000..adbd07b577 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/EntitySix.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.uuid.rfc9562; + +import java.util.UUID; + +import org.hibernate.annotations.UuidGenerator; +import org.hibernate.id.uuid.UuidVersion6Strategy; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Table(name = "entity_six") +@Entity +public class EntitySix { + @Id + @GeneratedValue + @UuidGenerator(algorithm = UuidVersion6Strategy.class) + private UUID id; + @Basic + private String name; + + protected EntitySix() { + // for Hibernate use + } + + public EntitySix(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/OtherEntitySeven.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/OtherEntitySeven.java new file mode 100644 index 0000000000..1726e1d51c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/OtherEntitySeven.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.uuid.rfc9562; + +import java.util.UUID; + +import org.hibernate.annotations.UuidGenerator; +import org.hibernate.id.uuid.UuidVersion7Strategy; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity(name = "OtherEntitySeven") +@Table(name = "other_entity_seven") +public class OtherEntitySeven { + @Id + @GeneratedValue + public Long pk; + + @UuidGenerator(algorithm = UuidVersion7Strategy.class) + public UUID id; + + @Basic + public String name; + + private OtherEntitySeven() { + // for Hibernate use + } + + public OtherEntitySeven(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/UUidV6V7GenetartorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/UUidV6V7GenetartorTest.java new file mode 100644 index 0000000000..678bc00b8a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/UUidV6V7GenetartorTest.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.uuid.rfc9562; + +import java.util.UUID; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.uuid.UuidValueGenerator; +import org.hibernate.id.uuid.UuidVersion6Strategy; +import org.hibernate.id.uuid.UuidVersion7Strategy; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class UUidV6V7GenetartorTest { + + private static final UUID NIL_UUID = new UUID( 0L, 0L ); + private static final int ITERATIONS = 1_000_000; + + @Test + void testMonotonicityUuid6() { + testMonotonicity( UuidVersion6Strategy.INSTANCE ); + } + + @Test + void testMonotonicityUuid7() { + testMonotonicity( UuidVersion7Strategy.INSTANCE ); + } + + private static void testMonotonicity(UuidValueGenerator generator) { + final SharedSessionContractImplementor session = mock( SharedSessionContractImplementor.class ); + final UUID[] uuids = new UUID[ITERATIONS + 1]; + uuids[0] = NIL_UUID; + for ( int n = 1; n <= ITERATIONS; ++n ) { + uuids[n] = generator.generateUuid( session ); + } + + for ( var n = 0; n < ITERATIONS; ++n ) { + assertThat( uuids[n + 1].toString() ).isGreaterThan( uuids[n].toString() ); + assertThat( uuids[n + 1] ).isGreaterThan( uuids[n] ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/UuidGeneratorAnnotationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/UuidGeneratorAnnotationTests.java new file mode 100644 index 0000000000..58314d6740 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/rfc9562/UuidGeneratorAnnotationTests.java @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.uuid.rfc9562; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.generator.Generator; +import org.hibernate.id.uuid.UuidGenerator; +import org.hibernate.id.uuid.UuidVersion6Strategy; +import org.hibernate.id.uuid.UuidVersion7Strategy; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Property; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.util.uuid.IdGeneratorCreationContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = { + EntitySeven.class, OtherEntitySeven.class, EntitySix.class +}) +@SessionFactory +@SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true, + reason = "Skipped for Sybase to avoid problems with UUIDs potentially ending with a trailing 0 byte") +public class UuidGeneratorAnnotationTests { + @Test + public void verifyUuidV7IdGeneratorModel(final DomainModelScope scope) { + scope.withHierarchy( EntitySeven.class, descriptor -> { + final Property idProperty = descriptor.getIdentifierProperty(); + final BasicValue value = (BasicValue) idProperty.getValue(); + + assertThat( value.getCustomIdGeneratorCreator() ).isNotNull(); + final Generator generator = value.getCustomIdGeneratorCreator() + .createGenerator( new IdGeneratorCreationContext( + scope.getDomainModel(), + descriptor + ) ); + + assertThat( generator ).isInstanceOf( UuidGenerator.class ); + final UuidGenerator uuidGenerator = (UuidGenerator) generator; + assertThat( uuidGenerator.getValueGenerator() ).isInstanceOf( UuidVersion7Strategy.class ); + } ); + } + + @Test + public void verifyUuidV6IdGeneratorModel(final DomainModelScope scope) { + scope.withHierarchy( EntitySix.class, descriptor -> { + final Property idProperty = descriptor.getIdentifierProperty(); + final BasicValue value = (BasicValue) idProperty.getValue(); + + assertThat( value.getCustomIdGeneratorCreator() ).isNotNull(); + final Generator generator = value.getCustomIdGeneratorCreator() + .createGenerator( new IdGeneratorCreationContext( + scope.getDomainModel(), + descriptor + ) ); + + assertThat( generator ).isInstanceOf( UuidGenerator.class ); + final UuidGenerator uuidGenerator = (UuidGenerator) generator; + assertThat( uuidGenerator.getValueGenerator() ).isInstanceOf( UuidVersion6Strategy.class ); + } ); + } + + @Test + public void basicUseTest(final SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntitySeven seven = new EntitySeven( "John Doe" ); + session.persist( seven ); + session.flush(); + assertThat( seven.id ).isNotNull(); + assertThat( seven.id.version() ).isEqualTo( 7 ); + } ); + } + + @Test + public void nonPkUseTest(final SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Instant startTime = Instant.now(); + + final OtherEntitySeven seven = new OtherEntitySeven( "Dave Default" ); + session.persist( seven ); + session.flush(); + + final Instant endTime = Instant.now(); + assertThat( seven.id ).isNotNull(); + assertThat( seven.id.version() ).isEqualTo( 7 ); + + assertThat( Instant.ofEpochMilli( seven.id.getMostSignificantBits() >> 16 & 0xFFFF_FFFF_FFFFL ) ) + .isBetween( startTime.truncatedTo( ChronoUnit.MILLIS ), endTime.truncatedTo( ChronoUnit.MILLIS ) ); + } ); + } + + @Test + void testUuidV6IdGenerator(final SessionFactoryScope sessionFactoryScope) { + sessionFactoryScope.inTransaction( session -> { + final Instant startTime = Instant.now(); + + final EntitySix six = new EntitySix( "Jane Doe" ); + session.persist( six ); + assertThat( six.getId() ).isNotNull(); + assertThat( six.getId().version() ).isEqualTo( 6 ); + + session.flush(); + final Instant endTime = Instant.now(); + assertThat( six.getId() ).isNotNull(); + assertThat( six.getId().version() ).isEqualTo( 6 ); + assertThat( uuid6Instant( six.getId() ) ).isBetween( startTime, endTime ); + } ); + } + + @AfterEach + void dropTestData(final SessionFactoryScope sessionFactoryScope) { + sessionFactoryScope.inTransaction( session -> { + session.createMutationQuery( "delete EntitySeven" ).executeUpdate(); + session.createMutationQuery( "delete OtherEntitySeven" ).executeUpdate(); + session.createMutationQuery( "delete EntitySix" ).executeUpdate(); + } ); + } + + public static Instant uuid6Instant(final UUID uuid) { + assert uuid.version() == 6; + + final var msb = uuid.getMostSignificantBits(); + final var ts = msb >> 4 & 0x0FFF_FFFF_FFFF_F000L | msb & 0x0FFFL; + return LocalDate.of( 1582, 10, 15 ).atStartOfDay( ZoneId.of( "UTC" ) ).toInstant() + .plusSeconds( ts / 10_000_000 ).plusNanos( ts % 10_000_000 * 100 ); + } + +}