diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/UuidGenerator.java b/hibernate-core/src/main/java/org/hibernate/annotations/UuidGenerator.java index 25d73166fd09..38fabec3a26e 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/UuidGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/UuidGenerator.java @@ -9,6 +9,8 @@ import java.util.UUID; import org.hibernate.Incubating; +import org.hibernate.id.uuid.UuidVersion6Strategy; +import org.hibernate.id.uuid.UuidVersion7Strategy; import org.hibernate.id.uuid.UuidValueGenerator; import static java.lang.annotation.ElementType.FIELD; @@ -52,7 +54,19 @@ enum Style { * @implNote Can be a bottleneck, since synchronization is used when * incrementing an internal counter as part of the algorithm. */ - TIME + TIME, + /** + * Use a time-based generation strategy consistent with RFC 4122 + * version 6. + * @see UuidVersion6Strategy + */ + VERSION_6, + /** + * Use a time-based generation strategy consistent with RFC 4122 + * version 7. + * @see UuidVersion7Strategy + */ + VERSION_7 } /** diff --git a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java index 380606c390e1..92b4465b7be1 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java @@ -23,6 +23,8 @@ import static org.hibernate.annotations.UuidGenerator.Style.AUTO; import static org.hibernate.annotations.UuidGenerator.Style.TIME; +import static org.hibernate.annotations.UuidGenerator.Style.VERSION_6; +import static org.hibernate.annotations.UuidGenerator.Style.VERSION_7; import static org.hibernate.generator.EventTypeSets.INSERT_ONLY; import static org.hibernate.internal.util.ReflectHelper.getPropertyType; @@ -78,9 +80,15 @@ private static UuidValueGenerator determineValueGenerator( } return instantiateCustomGenerator( config.algorithm() ); } - else if ( config.style() == TIME ) { + if ( config.style() == TIME ) { return new CustomVersionOneStrategy(); } + else if ( config.style() == VERSION_6 ) { + return UuidVersion6Strategy.INSTANCE; + } + else if ( config.style() == VERSION_7 ) { + return UuidVersion7Strategy.INSTANCE; + } } return StandardRandomStrategy.INSTANCE; diff --git a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion6Strategy.java b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion6Strategy.java new file mode 100644 index 000000000000..1e629d7a5bdb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion6Strategy.java @@ -0,0 +1,130 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.id.uuid; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.UUIDGenerationStrategy; + +/** + * Implements UUID Version 6 generation strategy as defined by the RFC 9562. + * + * + * + * @author Cedomir Igaly + */ +public class UuidVersion6Strategy implements UUIDGenerationStrategy, UuidValueGenerator { + + private static final Instant EPOCH_1582; + + static { + EPOCH_1582 = LocalDate.of( 1582, 10, 15 ) + .atStartOfDay( ZoneId.of( "UTC" ) ) + .toInstant(); + } + + private static class Holder { + + static final SecureRandom numberGenerator = new SecureRandom(); + } + + public static final UuidVersion6Strategy INSTANCE = new UuidVersion6Strategy(); + + private final Lock lock = new ReentrantLock( true ); + + private long currentTimestamp; + + private final AtomicLong clockSequence = new AtomicLong( 0 ); + + public UuidVersion6Strategy() { + this( getCurrentTimestamp(), 0 ); + } + + public UuidVersion6Strategy(final long currentTimestamp, final long clockSequence) { + this.currentTimestamp = currentTimestamp; + this.clockSequence.set( clockSequence ); + } + + /** + * A variant 6 + */ + @Override + public int getGeneratedVersion() { + // UUIDv6 is a field-compatible version of UUIDv1, reordered for improved DB locality + return 6; + } + + /** + * Delegates to {@link #generateUuid} + */ + @Override + public UUID generateUUID(SharedSessionContractImplementor session) { + return generateUuid( session ); + } + + + /** + * @param session session + * + * @return UUID version 6 + * @see UuidValueGenerator#generateUuid(SharedSessionContractImplementor) + */ + @Override + public UUID generateUuid(SharedSessionContractImplementor session) { + final long currentTimestamp = getCurrentTimestamp(); + + return new UUID( + // MSB bits 0-47 - most significant 32 bits of the 60-bit starting timestamp + currentTimestamp << 4 & 0xFFFF_FFFF_FFFF_0000L + // MSB bits 48-51 - version = 6 + | 0x6000L + // MSB bits 52-63 - least significant 12 bits from the 60-bit starting timestamp + | currentTimestamp & 0x0FFFL, + // LSB bits 0-1 - variant = 4 + 0x8000_0000_0000_0000L + // LSB bits 2-15 - clock sequence + | ( getSequence( currentTimestamp ) & 0x3FFFL ) << 48 + // LSB bits 16-63 - pseudorandom data + | Holder.numberGenerator.nextLong() & 0xFFFF_FFFF_FFFFL + ); + } + + + private long getSequence(final long currentTimestamp) { + lock.lock(); + try { + if ( this.currentTimestamp > currentTimestamp ) { + this.currentTimestamp = currentTimestamp; + clockSequence.set( 0 ); + } + } + finally { + lock.unlock(); + } + return clockSequence.getAndIncrement(); + } + + private static long getCurrentTimestamp() { + final Duration duration = Duration.between( EPOCH_1582, Instant.now() ); + return duration.toSeconds() * 10_000_000 + duration.toNanosPart() / 100; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java new file mode 100644 index 000000000000..6f51c8f48f6c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java @@ -0,0 +1,131 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.id.uuid; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.UUIDGenerationStrategy; + +import static java.time.Instant.EPOCH; +import static java.time.temporal.ChronoUnit.MILLIS; + +/** + * Implements UUID Version 7 generation strategy as defined by the RFC 9562. + * + * + * + * @author Cedomir Igaly + */ +public class UuidVersion7Strategy implements UUIDGenerationStrategy, UuidValueGenerator { + + public static final UuidVersion7Strategy INSTANCE = new UuidVersion7Strategy(); + + private static class Holder { + + static final SecureRandom numberGenerator = new SecureRandom(); + } + + private final Lock lock = new ReentrantLock( true ); + + private Duration currentTimestamp; + + private final AtomicLong clockSequence; + + public UuidVersion7Strategy() { + this( getCurrentTimestamp(), 0 ); + } + + public UuidVersion7Strategy(final Duration currentTimestamp, final long clockSequence) { + this.currentTimestamp = currentTimestamp; + this.clockSequence = new AtomicLong( clockSequence ); + } + + /** + * A variant 7 + */ + @Override + public int getGeneratedVersion() { + /* + * UUIDv7 features a time-ordered value field derived from the widely implemented and well- + * known Unix Epoch timestamp source, the number of milliseconds since midnight 1 Jan 1970 UTC, + * leap seconds excluded. + */ + return 7; + } + + /** + * Delegates to {@link #generateUuid} + */ + @Override + public UUID generateUUID(SharedSessionContractImplementor session) { + return generateUuid( session ); + } + + /** + * @param session session + * + * @return UUID version 7 + * @see UuidValueGenerator#generateUuid(SharedSessionContractImplementor) + */ + @Override + public UUID generateUuid(SharedSessionContractImplementor session) { + final Duration currentTimestamp = getCurrentTimestamp(); + + final long seq = getSequence( currentTimestamp ); + + final long millis = currentTimestamp.getSeconds() * 1000 + currentTimestamp.getNano() / 1_000_000; + final long nanosPart = Math.round( ( currentTimestamp.getNano() % 1_000_000L ) * 0.004096 ); + + return new UUID( + // MSB bits 0-47 - 48-bit big-endian unsigned number of the Unix Epoch timestamp in milliseconds + millis << 16 & 0xFFFF_FFFF_FFFF_0000L + // MSB bits 48-51 - version = 7 + | 0x7000L + // MSB bits 52-63 - sub-milliseconds part of timestamp + | nanosPart & 0xFFFL, + // LSB bits 0-1 - variant = 4 + 0x8000_0000_0000_0000L + // LSB bits 2-15 - counter + | ( seq & 0x3FFFL ) << 48 + // LSB bits 16-63 - pseudorandom data + | Holder.numberGenerator.nextLong() & 0xFFFF_FFFF_FFFFL + ); + } + + private long getSequence(final Duration currentTimestamp) { + lock.lock(); + try { + if ( !this.currentTimestamp.equals( currentTimestamp ) ) { + this.currentTimestamp = currentTimestamp; + clockSequence.set( 0 ); + } + } + finally { + lock.unlock(); + } + return clockSequence.getAndIncrement(); + } + + private static Duration getCurrentTimestamp() { + return Duration.between( EPOCH, Instant.now() ).truncatedTo( MILLIS ); + } +} 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 000000000000..4a9a9c7b71df --- /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 000000000000..adbd07b577a1 --- /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 000000000000..1726e1d51c8c --- /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 000000000000..678bc00b8a75 --- /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 000000000000..58314d6740b2 --- /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 ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java index 4602e2267df2..7f36bed33e62 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java @@ -69,7 +69,6 @@ import static org.hibernate.testing.orm.domain.gambit.EntityOfBasics.Gender.FEMALE; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertThrows;