From 6e3c562afdfaafe1e55e89aa4d8d90171161ed42 Mon Sep 17 00:00:00 2001 From: Max Zinal Date: Thu, 9 Jul 2026 11:46:53 +0300 Subject: [PATCH 1/7] feat: added key generator --- .../java/tech/ydb/core/keygen/BaseKeyGen.java | 173 ++++++++++++++++++ .../java/tech/ydb/core/keygen/TextKeyGen.java | 118 ++++++++++++ .../java/tech/ydb/core/keygen/UuidKeyGen.java | 134 ++++++++++++++ .../ydb/core/keygen/ConvertToYdbTest.java | 23 +++ 4 files changed, 448 insertions(+) create mode 100644 core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java create mode 100644 core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java create mode 100644 core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java create mode 100644 core/src/test/java/tech/ydb/core/keygen/ConvertToYdbTest.java diff --git a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java new file mode 100644 index 000000000..4b5925593 --- /dev/null +++ b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java @@ -0,0 +1,173 @@ +package tech.ydb.core.keygen; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.UUID; + +/** + * Implementation helpers for key generators. + * + * @author zinal + */ +class BaseKeyGen { + + /** + * Bit width of the embedded second-precision timestamp field. + */ + static final int TIMESTAMP_BITS = 30; + + /** + * Number of seconds which fit within the bit width specified. + */ + static final long TIMESTAMP_SECONDS = (1L << TIMESTAMP_BITS); + + /** + * Low bit index (inclusive) of the 30-bit timestamp field when + * {@code maskPos == 0} (1-bit prefix). Shifts down with larger prefixes. + */ + static final int TIMESTAMP_FIELD_LOW_BIT = 33; + + /** + * A position within an array of pre-computed bitmasks to be used. + */ + protected final int maskPos; + + protected BaseKeyGen(int prefixBits) { + if (prefixBits < 1 || prefixBits > 18) { + throw new IllegalArgumentException("Unsupported prefix length: " + prefixBits); + } + this.maskPos = prefixBits - 1; + } + + /** + * @return Prefix size used for construction, in bits. + */ + public int getPrefixBits() { + return maskPos + 1; + } + + /** + * @return Prefix mask to be applied + */ + public long getPrefixMask() { + return Holder.prefixMasks[maskPos]; + } + + /** + * Generates the new shared prefix to generate a series of related IDs. + * + * @return Random value to be used as a prefix. + */ + public long nextPrefix() { + final SecureRandom ng = Holder.numberGenerator; + byte[] data = new byte[8]; + ng.nextBytes(data); + long lsb = 0; + for (int i = 0; i < 8; i++) { + lsb = (lsb << 8) | (data[i] & 0xff); + } + return lsb; + } + + protected final long update(long msb, long prefix, Instant instant) { + long tsMask = Holder.timestampMasks[maskPos]; + long tsCode = getTimestampCode(instant); + tsCode = tsCode << (TIMESTAMP_FIELD_LOW_BIT - maskPos); + long bits; + if (prefix == -1L) { + bits = msb & ~tsMask; + bits |= tsCode & tsMask; + } else { + long prefixMask = Holder.prefixMasks[maskPos]; + bits = msb & ~(prefixMask | tsMask); + bits |= (prefix & prefixMask) | (tsCode & tsMask); + } + return bits; + } + + /** + * Computes the second-precision timestamp code: seconds since + * 2020-01-01T00:00:00Z, in the range {@code [0, 2^30)}. + * + * @param instant the instant to be used + * @return timestamp code between 0 and 2^30 - 1, inclusive + */ + public static int getTimestampCode(Instant instant) { + Instant sec = instant.truncatedTo(ChronoUnit.SECONDS); + long diff = sec.getEpochSecond() % TIMESTAMP_SECONDS; + if (diff < 0) { + throw new IllegalArgumentException( + "Instant out of 30-bit timestamp range: " + instant); + } + return (int) diff; + } + + /** + * YDB uses GUID (Microsoft-style) mixed-endian format. + * + * xxxxxxxx 0 1 2 3x 4 5 6 7
+ * xINPUT: 01020304 05060708 090a0b0c 0d0e0f10
+ * OUTPUT: 04030201 06050807 090a0b0c 0d0e0f10 + * + * This function puts the bytes of MSB in the proper order. + * + * @param v + * @return + */ + public static long reorder(long v) { + long b0 = (v >>> 56) & 0xffL; + long b1 = (v >>> 48) & 0xffL; + long b2 = (v >>> 40) & 0xffL; + long b3 = (v >>> 32) & 0xffL; + long b4 = (v >>> 24) & 0xffL; + long b5 = (v >>> 16) & 0xffL; + long b6 = (v >>> 8) & 0xffL; + long b7 = v & 0xffL; + return (b3 << 56) | (b2 << 48) | (b1 << 40) | (b0 << 32) + | (b5 << 24) | (b4 << 16) | (b7 << 8) | b6; + } + + /** + * Convert a UUID value to a base64 text representation. + * + * @param uuid Value to be converted + * @return Text representation of a UUID value of a fixed length 22 symbols + */ + public static String toString(UUID uuid) { + // apply byte swaps to restore the "regular" ordering + long msb = reorder(uuid.getMostSignificantBits()); + ByteBuffer byteArray = ByteBuffer.allocate(16); + byteArray.putLong(msb); + byteArray.putLong(uuid.getLeastSignificantBits()); + return Base64.getUrlEncoder() + .encodeToString(byteArray.array()) + .substring(0, 22); + } + + /** + * A holder class to defer initialization until needed. + */ + static class Holder { + + static final SecureRandom numberGenerator = new SecureRandom(); + + static final long prefixMasks[]; + static final long timestampMasks[]; + + static { + long pf[] = new long[32]; + long ts[] = new long[32]; + pf[0] = 0x8000000000000000L; + ts[0] = (((1L << TIMESTAMP_BITS) - 1) << TIMESTAMP_FIELD_LOW_BIT); + for (int i = 1; i < 32; ++i) { + pf[i] = pf[i - 1] | (1L << (63 - i)); + ts[i] = (ts[i - 1] >>> 1); + } + prefixMasks = pf; + timestampMasks = ts; + } + } +} diff --git a/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java new file mode 100644 index 000000000..27adcb955 --- /dev/null +++ b/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java @@ -0,0 +1,118 @@ +package tech.ydb.core.keygen; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Base64; + +/** + * Random text-format ID generator creates cache friendly identifiers to be used + * as primary keys for YDB row-organized tables. + * + * Similar to UuidKeyGen, but for keys in textual (base64-encoded) format. + * + * An extra difference is that, unlike UuidKeyGen, TextKeyGen does not set the + * UUID version and variant bits, so the generated value is formally not a UUID. + * + * @author zinal + */ +public class TextKeyGen extends BaseKeyGen { + + /** + * Constructs the generator instance with the default prefix size of 10 + * bits. + * + * Works best for up to 1k table partitions. + */ + public TextKeyGen() { + super(10); + } + + /** + * Constructs the generator instance with the custom prefix size. + * + * @param prefixBits Number of bits for the prefix, 1 to 18 bits. + */ + public TextKeyGen(int prefixBits) { + super(prefixBits); + } + + /** + * Generates the new ID with the specified prefix and instant (second + * precision for the embedded timestamp field). + * + * @param prefix Prefix value + * @param instant The instant whose second is embedded in the ID + * @return Base64 encoded ID with the embedded prefix and timestamp. + */ + public String nextValue(long prefix, Instant instant) { + SecureRandom ng = Holder.numberGenerator; + byte[] data = new byte[16]; + ng.nextBytes(data); + + long msb = 0; + for (int i = 0; i < 8; i++) { + msb = (msb << 8) | (data[i] & 0xff); + } + msb = update(msb, prefix, instant); + ByteBuffer.wrap(data).putLong(msb); + return Base64.getUrlEncoder() + .encodeToString(data) + .substring(0, 22); + } + + /** + * Generates the new ID with the specified prefix value. + * + * @param prefix Prefix value + * @param date The date (UTC midnight) used for the embedded timestamp + * @return Base64 encoded ID with the embedded prefix and timestamp for the + * start of date specified. + */ + public String nextValue(long prefix, LocalDate date) { + return nextValue(prefix, date.atStartOfDay(ZoneOffset.UTC).toInstant()); + } + + /** + * Generates the new ID with the specified prefix value. + * + * @param prefix Prefix value + * @return Base64 encoded ID with the embedded prefix and the current + * timestamp. + */ + public String nextValue(long prefix) { + return nextValue(prefix, Instant.now()); + } + + /** + * Generates the new ID with the specified instant. + * + * @param instant Timestamp for the value being generated. + * @return Base64 encoded ID with the embedded timestamp code. + */ + public String nextValue(Instant instant) { + return nextValue(-1L, instant); + } + + /** + * Generates the new ID with the random prefix value and a specified date. + * + * @param date The date (UTC midnight) used for the embedded timestamp + * @return Base64 encoded ID with the embedded timestamp for the start of + * date specified. + */ + public String nextValue(LocalDate date) { + return nextValue(-1L, date); + } + + /** + * Generates the new ID with the random prefix value. + * + * @return Base64 encoded ID with the embedded current timestamp. + */ + public String nextValue() { + return nextValue(-1L, Instant.now()); + } +} diff --git a/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java new file mode 100644 index 000000000..5d63e86d2 --- /dev/null +++ b/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java @@ -0,0 +1,134 @@ +package tech.ydb.core.keygen; + +import java.security.SecureRandom; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.UUID; + +/** + * Random UUID generator creates cache friendly identifiers to be used as + * primary keys for YDB row-organized tables. + * + * Each generated value consists of a random prefix of the specified length, a + * second-precision timestamp code of 30 bits, followed by the random suffix. + * + * The timestamp is UNIX epoch seconds, modulo 2^30 (about 34 years), which + * avoids code reuse for well over 30 years from the overlap. + * + * In addition, the generator supports the "fixed prefix" schema, in which a + * common prefix value is used for a series of related ids generated (typically + * to be written in a single transaction). + * + * The generated value is optimized for the actual internal format of UUID + * storage in YDB, which uses GUID (Microsoft-style) mixed-endian byte ordering. + * This affects the actual ordering, so it is important to properly put the + * bytes in the correct order. + * + * @author zinal + */ +public class UuidKeyGen extends BaseKeyGen { + + /** + * Constructs the generator instance with the default prefix size of 10 + * bits. + * + * Works best for up to 1k table partitions. + */ + public UuidKeyGen() { + super(10); + } + + /** + * Constructs the generator instance with the custom prefix size. + * + * @param prefixBits Number of bits for the prefix, 1 to 18 bits. + */ + public UuidKeyGen(int prefixBits) { + super(prefixBits); + } + + /** + * Generates the new ID with the specified prefix value and calendar date + * (UTC midnight). + * + * @param prefix Prefix value + * @param date The date whose start-of-day UTC instant is embedded + * @return Random UUID with the embedded prefix, timestamp code and suffix. + */ + public UUID nextValue(long prefix, LocalDate date) { + return nextValue(prefix, date.atStartOfDay(ZoneOffset.UTC).toInstant()); + } + + /** + * Generates the new ID with the specified prefix value and instant + * (truncated to whole seconds for the embedded timestamp field). + * + * @param prefix Prefix value + * @param instant The instant whose second is embedded in the UUID + * @return Random UUID with the embedded prefix, timestamp code and suffix. + */ + public UUID nextValue(long prefix, Instant instant) { + SecureRandom ng = Holder.numberGenerator; + byte[] data = new byte[16]; + ng.nextBytes(data); + + data[6] &= 0x0f; + data[6] |= (byte) 0x80; + data[8] &= 0x3f; + data[8] |= (byte) 0x80; + + long msb = 0; + long lsb = 0; + for (int i = 0; i < 8; i++) { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) { + lsb = (lsb << 8) | (data[i] & 0xff); + } + + msb = update(msb, prefix, instant); + return new UUID(reorder(msb), lsb); + } + + /** + * Generates the new ID with the specified prefix value. + * + * @param prefix Prefix value + * @return Random UUID with the embedded prefix, timestamp code and suffix. + */ + public UUID nextValue(long prefix) { + return nextValue(prefix, Instant.now()); + } + + /** + * Generates the new ID with the random prefix value and a specified + * instant. + * + * @param instant Timestamp for the value being generated. + * @return Random UUID with the embedded prefix, timestamp code and suffix. + */ + public UUID nextValue(Instant instant) { + return nextValue(-1L, instant); + } + + /** + * Generates the new ID with the random prefix value and a specified date. + * + * @param date The date (UTC midnight) used for the embedded timestamp + * @return Random UUID with the embedded prefix, timestamp code and suffix. + */ + public UUID nextValue(LocalDate date) { + return nextValue(-1L, date); + } + + /** + * Generates the new ID with the random prefix value. + * + * @return Random UUID with the embedded prefix, timestamp code and suffix. + */ + public UUID nextValue() { + return nextValue(-1L, Instant.now()); + } + +} diff --git a/core/src/test/java/tech/ydb/core/keygen/ConvertToYdbTest.java b/core/src/test/java/tech/ydb/core/keygen/ConvertToYdbTest.java new file mode 100644 index 000000000..30ebaeb4f --- /dev/null +++ b/core/src/test/java/tech/ydb/core/keygen/ConvertToYdbTest.java @@ -0,0 +1,23 @@ +package tech.ydb.core.keygen; + +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +/** + * + * @author zinal + */ +public class ConvertToYdbTest { + + @Test + public void convertToYdbTest() { + byte[] input = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; + long v0 = ByteBuffer.wrap(input).getLong(); + long v1 = UuidKeyGen.reorder(v0); + long v2 = UuidKeyGen.reorder(v1); + Assert.assertEquals(v0, v2); + Assert.assertNotEquals(v0, v1); + } + +} From 2aecb5aee76984099a7996076b3f0291b1f9d1d0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Jul 2026 08:59:29 +0000 Subject: [PATCH 2/7] Add comprehensive tests for UuidKeyGen and TextKeyGen Introduce UuidKeyGenTest, TextKeyGenTest, and KeyGenTestSupport with coverage for constructors, prefix/timestamp embedding, all nextValue overloads, UUID version/variant handling, base64 text format, and BaseKeyGen helper methods used by both generators. Co-authored-by: Maksim Zinal --- .../ydb/core/keygen/KeyGenTestSupport.java | 69 ++++ .../tech/ydb/core/keygen/TextKeyGenTest.java | 257 +++++++++++++++ .../tech/ydb/core/keygen/UuidKeyGenTest.java | 295 ++++++++++++++++++ 3 files changed, 621 insertions(+) create mode 100644 core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java create mode 100644 core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java create mode 100644 core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java diff --git a/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java b/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java new file mode 100644 index 000000000..bd6f17d42 --- /dev/null +++ b/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java @@ -0,0 +1,69 @@ +package tech.ydb.core.keygen; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Base64; +import java.util.UUID; + +/** + * Shared helpers for {@link UuidKeyGen} and {@link TextKeyGen} tests. + */ +final class KeyGenTestSupport { + + private KeyGenTestSupport() { + } + + static long timestampMask(int prefixBits) { + int maskPos = prefixBits - 1; + return (((1L << BaseKeyGen.TIMESTAMP_BITS) - 1L) << BaseKeyGen.TIMESTAMP_FIELD_LOW_BIT) >>> maskPos; + } + + static long logicalMsbFromUuid(UUID uuid) { + return BaseKeyGen.reorder(uuid.getMostSignificantBits()); + } + + static long logicalMsbFromTextKey(String key) { + return bytesFromTextKey(key).getLong(0); + } + + static long logicalLsbFromTextKey(String key) { + return bytesFromTextKey(key).getLong(8); + } + + private static java.nio.ByteBuffer bytesFromTextKey(String key) { + String padded = key; + while (padded.length() % 4 != 0) { + padded += "="; + } + byte[] data = Base64.getUrlDecoder().decode(padded); + if (data.length != 16) { + throw new IllegalArgumentException("Expected 16 bytes, got " + data.length); + } + return java.nio.ByteBuffer.wrap(data); + } + + static long extractEmbeddedPrefix(long logicalMsb, BaseKeyGen generator) { + return logicalMsb & generator.getPrefixMask(); + } + + static int extractEmbeddedTimestampCode(long logicalMsb, int prefixBits) { + long tsMask = timestampMask(prefixBits); + int shift = BaseKeyGen.TIMESTAMP_FIELD_LOW_BIT - (prefixBits - 1); + return (int) ((logicalMsb & tsMask) >>> shift); + } + + static int logicalVersionNibble(long logicalMsb) { + return (int) ((logicalMsb >> 12) & 0x0f); + } + + static int logicalVariantBits(long logicalLsb) { + return (int) ((logicalLsb >>> 56) & 0xc0); + } + + static UUID uuidFromTextKey(String key) { + return new UUID( + BaseKeyGen.reorder(KeyGenTestSupport.logicalMsbFromTextKey(key)), + KeyGenTestSupport.logicalLsbFromTextKey(key)); + } +} diff --git a/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java b/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java new file mode 100644 index 000000000..dae5e34b8 --- /dev/null +++ b/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java @@ -0,0 +1,257 @@ +package tech.ydb.core.keygen; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import org.junit.Assert; +import org.junit.Test; + +/** + * Correctness tests for {@link TextKeyGen}. + */ +public class TextKeyGenTest { + + private static final Instant FIXED_INSTANT = Instant.parse("2024-06-15T12:34:56Z"); + private static final LocalDate FIXED_DATE = LocalDate.of(2024, 6, 15); + private static final long FIXED_PREFIX = 0x2A5L; + + @Test + public void defaultConstructorUsesTenPrefixBits() { + TextKeyGen generator = new TextKeyGen(); + Assert.assertEquals(10, generator.getPrefixBits()); + Assert.assertEquals(0xFFC0000000000000L, generator.getPrefixMask()); + } + + @Test + public void customPrefixBitsExposeMask() { + TextKeyGen oneBit = new TextKeyGen(1); + TextKeyGen eighteenBit = new TextKeyGen(18); + + Assert.assertEquals(1, oneBit.getPrefixBits()); + Assert.assertEquals(0x8000000000000000L, oneBit.getPrefixMask()); + Assert.assertEquals(18, eighteenBit.getPrefixBits()); + Assert.assertEquals(0xFFFFC00000000000L, eighteenBit.getPrefixMask()); + } + + @Test + public void unsupportedPrefixLengthIsRejected() { + try { + new TextKeyGen(0); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("Unsupported prefix length: 0", ex.getMessage()); + } + + try { + new TextKeyGen(19); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("Unsupported prefix length: 19", ex.getMessage()); + } + } + + @Test + public void nextPrefixProducesDistinctValues() { + TextKeyGen generator = new TextKeyGen(12); + Set seen = new HashSet<>(); + + for (int i = 0; i < 100; ++i) { + seen.add(generator.nextPrefix()); + } + + Assert.assertTrue("Expected multiple distinct prefixes", seen.size() > 1); + } + + @Test + public void fixedPrefixIsMaskedWhenEmbedded() { + TextKeyGen generator = new TextKeyGen(12); + long rawPrefix = 0x123456789ABCDEFL; + + String key = generator.nextValue(rawPrefix, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + + Assert.assertEquals(rawPrefix & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + } + + @Test + public void nextValueProducesTwentyTwoCharUrlSafeBase64() { + TextKeyGen generator = new TextKeyGen(); + String key = generator.nextValue(); + + Assert.assertEquals(22, key.length()); + Assert.assertFalse(key.contains("=")); + Assert.assertFalse(key.contains("+")); + Assert.assertFalse(key.contains("/")); + Base64.getUrlDecoder().decode(key + "=="); + } + + @Test + public void nextValueEmbedsFixedPrefixAndTimestamp() { + TextKeyGen generator = new TextKeyGen(10); + + String key = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + + Assert.assertEquals(FIXED_PREFIX & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertEquals(BaseKeyGen.getTimestampCode(FIXED_INSTANT), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits())); + } + + @Test + public void nextValueWithLocalDateUsesUtcMidnight() { + TextKeyGen generator = new TextKeyGen(10); + Instant expectedInstant = FIXED_DATE.atStartOfDay(ZoneOffset.UTC).toInstant(); + + String key = generator.nextValue(FIXED_PREFIX, FIXED_DATE); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + + Assert.assertEquals(BaseKeyGen.getTimestampCode(expectedInstant), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits())); + } + + @Test + public void nextValueWithRandomPrefixPreservesRandomPrefixBits() { + TextKeyGen generator = new TextKeyGen(10); + Set prefixes = new HashSet<>(); + + for (int i = 0; i < 50; ++i) { + String key = generator.nextValue(FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + } + + Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); + } + + @Test + public void nextValueWithPrefixOnlyUsesCurrentSecond() { + TextKeyGen generator = new TextKeyGen(10); + int before = BaseKeyGen.getTimestampCode(Instant.now()); + + String key = generator.nextValue(FIXED_PREFIX); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + int embedded = KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits()); + int after = BaseKeyGen.getTimestampCode(Instant.now()); + + Assert.assertEquals(FIXED_PREFIX & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertTrue(embedded >= before && embedded <= after); + } + + @Test + public void nextValueWithDateOnlyUsesUtcMidnightAndRandomPrefix() { + TextKeyGen generator = new TextKeyGen(8); + Instant expectedInstant = FIXED_DATE.atStartOfDay(ZoneOffset.UTC).toInstant(); + Set prefixes = new HashSet<>(); + + for (int i = 0; i < 20; ++i) { + String key = generator.nextValue(FIXED_DATE); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertEquals(BaseKeyGen.getTimestampCode(expectedInstant), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits())); + } + + Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); + } + + @Test + public void nextValueWithoutArgumentsUsesRandomPrefixAndCurrentSecond() { + TextKeyGen generator = new TextKeyGen(10); + int before = BaseKeyGen.getTimestampCode(Instant.now()); + Set prefixes = new HashSet<>(); + + String key = generator.nextValue(); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + int embedded = KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits()); + int after = BaseKeyGen.getTimestampCode(Instant.now()); + + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + for (int i = 0; i < 20; ++i) { + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix( + KeyGenTestSupport.logicalMsbFromTextKey(generator.nextValue()), generator)); + } + + Assert.assertTrue(embedded >= before && embedded <= after); + Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); + } + + @Test + public void nextValueDoesNotForceUuidVersionBits() { + TextKeyGen textGenerator = new TextKeyGen(10); + UuidKeyGen uuidGenerator = new UuidKeyGen(10); + Set textVersions = new HashSet<>(); + + for (int i = 0; i < 50; ++i) { + String key = textGenerator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + textVersions.add(KeyGenTestSupport.logicalVersionNibble( + KeyGenTestSupport.logicalMsbFromTextKey(key))); + + UUID uuid = uuidGenerator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + Assert.assertEquals(8, KeyGenTestSupport.logicalVersionNibble( + KeyGenTestSupport.logicalMsbFromUuid(uuid))); + } + + Assert.assertTrue("Text keys should not all share version nibble 8", textVersions.size() > 1); + } + + @Test + public void nextValueWithSamePrefixChangesRandomSuffix() { + TextKeyGen generator = new TextKeyGen(10); + + String first = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + String second = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + + Assert.assertNotEquals(first, second); + Assert.assertNotEquals(KeyGenTestSupport.logicalLsbFromTextKey(first), + KeyGenTestSupport.logicalLsbFromTextKey(second)); + Assert.assertEquals( + KeyGenTestSupport.extractEmbeddedPrefix( + KeyGenTestSupport.logicalMsbFromTextKey(first), generator), + KeyGenTestSupport.extractEmbeddedPrefix( + KeyGenTestSupport.logicalMsbFromTextKey(second), generator)); + } + + @Test + public void generatedTextKeyMatchesBaseKeyGenToStringForEquivalentUuid() { + TextKeyGen generator = new TextKeyGen(10); + + for (int i = 0; i < 20; ++i) { + String key = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + Assert.assertEquals(BaseKeyGen.toString(KeyGenTestSupport.uuidFromTextKey(key)), key); + } + } + + @Test + public void prefixAndTimestampLayoutsWorkForEdgePrefixSizes() { + for (int prefixBits : new int[]{1, 18}) { + TextKeyGen generator = new TextKeyGen(prefixBits); + + String key = generator.nextValue(0x123456789ABCDEFL, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromTextKey(key); + + Assert.assertEquals(0x123456789ABCDEFL & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertEquals(BaseKeyGen.getTimestampCode(FIXED_INSTANT), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, prefixBits)); + } + } + + @Test + public void distinctCallsProduceDistinctKeys() { + TextKeyGen generator = new TextKeyGen(); + Set keys = new HashSet<>(); + + for (int i = 0; i < 100; ++i) { + keys.add(generator.nextValue(FIXED_PREFIX, FIXED_INSTANT)); + } + + Assert.assertEquals(100, keys.size()); + } +} diff --git a/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java b/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java new file mode 100644 index 000000000..b0064f90a --- /dev/null +++ b/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java @@ -0,0 +1,295 @@ +package tech.ydb.core.keygen; + +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import org.junit.Assert; +import org.junit.Test; + +/** + * Correctness tests for {@link UuidKeyGen}. + */ +public class UuidKeyGenTest { + + private static final Instant FIXED_INSTANT = Instant.parse("2024-06-15T12:34:56Z"); + private static final LocalDate FIXED_DATE = LocalDate.of(2024, 6, 15); + private static final long FIXED_PREFIX = 0x2A5L; + + @Test + public void defaultConstructorUsesTenPrefixBits() { + UuidKeyGen generator = new UuidKeyGen(); + Assert.assertEquals(10, generator.getPrefixBits()); + Assert.assertEquals(0xFFC0000000000000L, generator.getPrefixMask()); + } + + @Test + public void customPrefixBitsExposeMask() { + UuidKeyGen oneBit = new UuidKeyGen(1); + UuidKeyGen eighteenBit = new UuidKeyGen(18); + + Assert.assertEquals(1, oneBit.getPrefixBits()); + Assert.assertEquals(0x8000000000000000L, oneBit.getPrefixMask()); + Assert.assertEquals(18, eighteenBit.getPrefixBits()); + Assert.assertEquals(0xFFFFC00000000000L, eighteenBit.getPrefixMask()); + } + + @Test + public void unsupportedPrefixLengthIsRejected() { + assertUnsupportedPrefix(0); + assertUnsupportedPrefix(-1); + assertUnsupportedPrefix(19); + assertUnsupportedPrefix(100); + } + + private static void assertUnsupportedPrefix(int prefixBits) { + try { + new UuidKeyGen(prefixBits); + Assert.fail("Expected IllegalArgumentException for prefixBits=" + prefixBits); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("Unsupported prefix length: " + prefixBits, ex.getMessage()); + } + } + + @Test + public void getTimestampCodeUsesWholeSeconds() { + Instant withNanos = Instant.parse("2024-01-02T03:04:05.999999999Z"); + Instant truncated = Instant.parse("2024-01-02T03:04:05Z"); + + Assert.assertEquals(BaseKeyGen.getTimestampCode(truncated), + BaseKeyGen.getTimestampCode(withNanos)); + Assert.assertEquals(truncated.getEpochSecond() % BaseKeyGen.TIMESTAMP_SECONDS, + BaseKeyGen.getTimestampCode(withNanos)); + } + + @Test + public void getTimestampCodeRejectsNegativeEpochSecond() { + try { + BaseKeyGen.getTimestampCode(Instant.parse("1969-12-31T23:59:59Z")); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("Instant out of 30-bit timestamp range")); + } + } + + @Test + public void reorderSwapsMsbBytesAndIsInvolutory() { + byte[] input = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; + long original = ByteBuffer.wrap(input).getLong(); + long swapped = BaseKeyGen.reorder(original); + + Assert.assertNotEquals(original, swapped); + Assert.assertEquals(original, BaseKeyGen.reorder(swapped)); + Assert.assertEquals(0x0403020106050807L, swapped); + } + + @Test + public void toStringProducesTwentyTwoCharUrlSafeBase64() { + UUID uuid = UUID.fromString("00112233-4455-6677-8899-aabbccddeeff"); + String text = BaseKeyGen.toString(uuid); + + Assert.assertEquals(22, text.length()); + Assert.assertEquals("MyIRAFVEd2aImaq7zN3u_w", text); + Assert.assertFalse(text.contains("+")); + Assert.assertFalse(text.contains("/")); + } + + @Test + public void nextPrefixProducesDistinctValues() { + UuidKeyGen generator = new UuidKeyGen(12); + Set seen = new HashSet<>(); + + for (int i = 0; i < 100; ++i) { + seen.add(generator.nextPrefix()); + } + + Assert.assertTrue("Expected multiple distinct prefixes", seen.size() > 1); + } + + @Test + public void fixedPrefixIsMaskedWhenEmbedded() { + UuidKeyGen generator = new UuidKeyGen(12); + long rawPrefix = 0x123456789ABCDEFL; + + UUID uuid = generator.nextValue(rawPrefix, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + + Assert.assertEquals(rawPrefix & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + } + + @Test + public void nextValueEmbedsFixedPrefixAndTimestamp() { + UuidKeyGen generator = new UuidKeyGen(10); + + UUID uuid = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + + Assert.assertEquals(FIXED_PREFIX & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertEquals(BaseKeyGen.getTimestampCode(FIXED_INSTANT), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits())); + } + + @Test + public void nextValueWithLocalDateUsesUtcMidnight() { + UuidKeyGen generator = new UuidKeyGen(10); + Instant expectedInstant = FIXED_DATE.atStartOfDay(ZoneOffset.UTC).toInstant(); + + UUID uuid = generator.nextValue(FIXED_PREFIX, FIXED_DATE); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + + Assert.assertEquals(BaseKeyGen.getTimestampCode(expectedInstant), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits())); + } + + @Test + public void nextValueWithRandomPrefixPreservesRandomPrefixBits() { + UuidKeyGen generator = new UuidKeyGen(10); + Set prefixes = new HashSet<>(); + + for (int i = 0; i < 50; ++i) { + UUID uuid = generator.nextValue(FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + } + + Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); + } + + @Test + public void nextValueWithPrefixOnlyUsesCurrentSecond() { + UuidKeyGen generator = new UuidKeyGen(10); + int before = BaseKeyGen.getTimestampCode(Instant.now()); + + UUID uuid = generator.nextValue(FIXED_PREFIX); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + int embedded = KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits()); + int after = BaseKeyGen.getTimestampCode(Instant.now()); + + Assert.assertTrue(embedded >= before && embedded <= after); + } + + @Test + public void nextValueWithDateOnlyUsesUtcMidnightAndRandomPrefix() { + UuidKeyGen generator = new UuidKeyGen(8); + Instant expectedInstant = FIXED_DATE.atStartOfDay(ZoneOffset.UTC).toInstant(); + Set prefixes = new HashSet<>(); + + for (int i = 0; i < 20; ++i) { + UUID uuid = generator.nextValue(FIXED_DATE); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertEquals(BaseKeyGen.getTimestampCode(expectedInstant), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits())); + } + + Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); + } + + @Test + public void nextValueWithoutArgumentsUsesRandomPrefixAndCurrentSecond() { + UuidKeyGen generator = new UuidKeyGen(10); + int before = BaseKeyGen.getTimestampCode(Instant.now()); + Set prefixes = new HashSet<>(); + + UUID uuid = generator.nextValue(); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + int embedded = KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, generator.getPrefixBits()); + int after = BaseKeyGen.getTimestampCode(Instant.now()); + + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + for (int i = 0; i < 20; ++i) { + UUID next = generator.nextValue(); + prefixes.add(KeyGenTestSupport.extractEmbeddedPrefix( + KeyGenTestSupport.logicalMsbFromUuid(next), generator)); + } + + Assert.assertTrue(embedded >= before && embedded <= after); + Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); + } + + @Test + public void nextValueSetsVersionAndVariantBits() { + UuidKeyGen generator = new UuidKeyGen(); + + for (int i = 0; i < 20; ++i) { + UUID uuid = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + + Assert.assertEquals(2, uuid.variant()); + Assert.assertEquals(8, KeyGenTestSupport.logicalVersionNibble(logicalMsb)); + Assert.assertEquals(0x80, KeyGenTestSupport.logicalVariantBits(uuid.getLeastSignificantBits())); + } + } + + @Test + public void nextValueWithSamePrefixChangesRandomSuffix() { + UuidKeyGen generator = new UuidKeyGen(10); + + UUID first = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + UUID second = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + + Assert.assertNotEquals(first, second); + Assert.assertNotEquals(first.getLeastSignificantBits(), second.getLeastSignificantBits()); + Assert.assertEquals( + KeyGenTestSupport.extractEmbeddedPrefix( + KeyGenTestSupport.logicalMsbFromUuid(first), generator), + KeyGenTestSupport.extractEmbeddedPrefix( + KeyGenTestSupport.logicalMsbFromUuid(second), generator)); + Assert.assertEquals( + KeyGenTestSupport.extractEmbeddedTimestampCode( + KeyGenTestSupport.logicalMsbFromUuid(first), generator.getPrefixBits()), + KeyGenTestSupport.extractEmbeddedTimestampCode( + KeyGenTestSupport.logicalMsbFromUuid(second), generator.getPrefixBits())); + } + + @Test + public void textRepresentationMatchesBaseKeyGenToString() { + UuidKeyGen generator = new UuidKeyGen(10); + + for (int i = 0; i < 20; ++i) { + UUID uuid = generator.nextValue(FIXED_PREFIX, FIXED_INSTANT); + Assert.assertEquals(BaseKeyGen.toString(uuid), encodeUuidPayload(uuid)); + } + } + + @Test + public void prefixAndTimestampLayoutsWorkForEdgePrefixSizes() { + for (int prefixBits : new int[]{1, 18}) { + UuidKeyGen generator = new UuidKeyGen(prefixBits); + + UUID uuid = generator.nextValue(0x123456789ABCDEFL, FIXED_INSTANT); + long logicalMsb = KeyGenTestSupport.logicalMsbFromUuid(uuid); + + Assert.assertEquals(0x123456789ABCDEFL & generator.getPrefixMask(), + KeyGenTestSupport.extractEmbeddedPrefix(logicalMsb, generator)); + Assert.assertEquals(BaseKeyGen.getTimestampCode(FIXED_INSTANT), + KeyGenTestSupport.extractEmbeddedTimestampCode(logicalMsb, prefixBits)); + } + } + + @Test + public void distinctCallsProduceDistinctValues() { + UuidKeyGen generator = new UuidKeyGen(); + Set values = new HashSet<>(); + + for (int i = 0; i < 100; ++i) { + values.add(generator.nextValue(FIXED_PREFIX, FIXED_INSTANT)); + } + + Assert.assertEquals(100, values.size()); + } + + private static String encodeUuidPayload(UUID uuid) { + long msb = BaseKeyGen.reorder(uuid.getMostSignificantBits()); + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.putLong(msb); + buffer.putLong(uuid.getLeastSignificantBits()); + return Base64.getUrlEncoder().encodeToString(buffer.array()).substring(0, 22); + } +} From a7dc92c95d4198549f03dd641d17046dc3885cca Mon Sep 17 00:00:00 2001 From: Max Zinal Date: Thu, 9 Jul 2026 12:11:19 +0300 Subject: [PATCH 3/7] minor test updates --- .../ydb/core/keygen/KeyGenTestSupport.java | 3 --- .../tech/ydb/core/keygen/TextKeyGenTest.java | 21 +------------------ .../tech/ydb/core/keygen/UuidKeyGenTest.java | 2 +- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java b/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java index bd6f17d42..8c19dc86c 100644 --- a/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java +++ b/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java @@ -1,8 +1,5 @@ package tech.ydb.core.keygen; -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneOffset; import java.util.Base64; import java.util.UUID; diff --git a/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java b/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java index dae5e34b8..770a42db0 100644 --- a/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java +++ b/core/src/test/java/tech/ydb/core/keygen/TextKeyGenTest.java @@ -63,7 +63,7 @@ public void nextPrefixProducesDistinctValues() { seen.add(generator.nextPrefix()); } - Assert.assertTrue("Expected multiple distinct prefixes", seen.size() > 1); + Assert.assertTrue("Expected multiple distinct prefixes", seen.size() > 90); } @Test @@ -182,25 +182,6 @@ public void nextValueWithoutArgumentsUsesRandomPrefixAndCurrentSecond() { Assert.assertTrue("Expected varying random prefixes", prefixes.size() > 1); } - @Test - public void nextValueDoesNotForceUuidVersionBits() { - TextKeyGen textGenerator = new TextKeyGen(10); - UuidKeyGen uuidGenerator = new UuidKeyGen(10); - Set textVersions = new HashSet<>(); - - for (int i = 0; i < 50; ++i) { - String key = textGenerator.nextValue(FIXED_PREFIX, FIXED_INSTANT); - textVersions.add(KeyGenTestSupport.logicalVersionNibble( - KeyGenTestSupport.logicalMsbFromTextKey(key))); - - UUID uuid = uuidGenerator.nextValue(FIXED_PREFIX, FIXED_INSTANT); - Assert.assertEquals(8, KeyGenTestSupport.logicalVersionNibble( - KeyGenTestSupport.logicalMsbFromUuid(uuid))); - } - - Assert.assertTrue("Text keys should not all share version nibble 8", textVersions.size() > 1); - } - @Test public void nextValueWithSamePrefixChangesRandomSuffix() { TextKeyGen generator = new TextKeyGen(10); diff --git a/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java b/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java index b0064f90a..21f2a0dad 100644 --- a/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java +++ b/core/src/test/java/tech/ydb/core/keygen/UuidKeyGenTest.java @@ -107,7 +107,7 @@ public void nextPrefixProducesDistinctValues() { seen.add(generator.nextPrefix()); } - Assert.assertTrue("Expected multiple distinct prefixes", seen.size() > 1); + Assert.assertTrue("Expected multiple distinct prefixes", seen.size() > 90); } @Test From e155bd22f5f9d6f77d1b18e61dfadd1e768632ff Mon Sep 17 00:00:00 2001 From: Max Zinal Date: Thu, 9 Jul 2026 13:23:06 +0300 Subject: [PATCH 4/7] checkstype fixes --- .../java/tech/ydb/core/keygen/BaseKeyGen.java | 24 +++++++++---------- .../java/tech/ydb/core/keygen/TextKeyGen.java | 4 ++-- .../java/tech/ydb/core/keygen/UuidKeyGen.java | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java index 4b5925593..7aabfd211 100644 --- a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java @@ -53,7 +53,7 @@ public int getPrefixBits() { * @return Prefix mask to be applied */ public long getPrefixMask() { - return Holder.prefixMasks[maskPos]; + return Holder.PREFIX_MASKS[maskPos]; } /** @@ -62,9 +62,9 @@ public long getPrefixMask() { * @return Random value to be used as a prefix. */ public long nextPrefix() { - final SecureRandom ng = Holder.numberGenerator; + final SecureRandom sr = Holder.SR; byte[] data = new byte[8]; - ng.nextBytes(data); + sr.nextBytes(data); long lsb = 0; for (int i = 0; i < 8; i++) { lsb = (lsb << 8) | (data[i] & 0xff); @@ -73,7 +73,7 @@ public long nextPrefix() { } protected final long update(long msb, long prefix, Instant instant) { - long tsMask = Holder.timestampMasks[maskPos]; + long tsMask = Holder.TS_MASKS[maskPos]; long tsCode = getTimestampCode(instant); tsCode = tsCode << (TIMESTAMP_FIELD_LOW_BIT - maskPos); long bits; @@ -81,7 +81,7 @@ protected final long update(long msb, long prefix, Instant instant) { bits = msb & ~tsMask; bits |= tsCode & tsMask; } else { - long prefixMask = Holder.prefixMasks[maskPos]; + long prefixMask = Holder.PREFIX_MASKS[maskPos]; bits = msb & ~(prefixMask | tsMask); bits |= (prefix & prefixMask) | (tsCode & tsMask); } @@ -152,22 +152,22 @@ public static String toString(UUID uuid) { */ static class Holder { - static final SecureRandom numberGenerator = new SecureRandom(); + static final SecureRandom SR = new SecureRandom(); - static final long prefixMasks[]; - static final long timestampMasks[]; + static final long[] PREFIX_MASKS; + static final long[] TS_MASKS; static { - long pf[] = new long[32]; - long ts[] = new long[32]; + long[] pf = new long[32]; + long[] ts = new long[32]; pf[0] = 0x8000000000000000L; ts[0] = (((1L << TIMESTAMP_BITS) - 1) << TIMESTAMP_FIELD_LOW_BIT); for (int i = 1; i < 32; ++i) { pf[i] = pf[i - 1] | (1L << (63 - i)); ts[i] = (ts[i - 1] >>> 1); } - prefixMasks = pf; - timestampMasks = ts; + PREFIX_MASKS = pf; + TS_MASKS = ts; } } } diff --git a/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java index 27adcb955..7cdb16a86 100644 --- a/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/TextKeyGen.java @@ -48,9 +48,9 @@ public TextKeyGen(int prefixBits) { * @return Base64 encoded ID with the embedded prefix and timestamp. */ public String nextValue(long prefix, Instant instant) { - SecureRandom ng = Holder.numberGenerator; + SecureRandom sr = Holder.SR; byte[] data = new byte[16]; - ng.nextBytes(data); + sr.nextBytes(data); long msb = 0; for (int i = 0; i < 8; i++) { diff --git a/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java index 5d63e86d2..c62da43cc 100644 --- a/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java @@ -69,9 +69,9 @@ public UUID nextValue(long prefix, LocalDate date) { * @return Random UUID with the embedded prefix, timestamp code and suffix. */ public UUID nextValue(long prefix, Instant instant) { - SecureRandom ng = Holder.numberGenerator; + SecureRandom sr = Holder.SR; byte[] data = new byte[16]; - ng.nextBytes(data); + sr.nextBytes(data); data[6] &= 0x0f; data[6] |= (byte) 0x80; From c976f48ffcc62427baaf519a7dc30066dbc336ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Jul 2026 14:24:54 +0000 Subject: [PATCH 5/7] Fix Javadoc errors in BaseKeyGen.reorder() documentation Replace self-closing br tags with a pre block and add missing @param and @return descriptions. Co-authored-by: Maksim Zinal --- .../src/main/java/tech/ydb/core/keygen/BaseKeyGen.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java index 7aabfd211..b1ddb7486 100644 --- a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java @@ -108,14 +108,16 @@ public static int getTimestampCode(Instant instant) { /** * YDB uses GUID (Microsoft-style) mixed-endian format. * - * xxxxxxxx 0 1 2 3x 4 5 6 7
- * xINPUT: 01020304 05060708 090a0b0c 0d0e0f10
+ *
+     * xxxxxxxx 0 1 2 3x 4 5 6 7
+     * INPUT:  01020304 05060708 090a0b0c 0d0e0f10
      * OUTPUT: 04030201 06050807 090a0b0c 0d0e0f10
+     * 
* * This function puts the bytes of MSB in the proper order. * - * @param v - * @return + * @param v MSB value in standard big-endian long representation + * @return MSB value with byte order adjusted for YDB GUID storage */ public static long reorder(long v) { long b0 = (v >>> 56) & 0xffL; From f67996d20c373b1b90d82fa5db9f60d3329b26b1 Mon Sep 17 00:00:00 2001 From: Max Zinal Date: Fri, 10 Jul 2026 20:34:37 +0300 Subject: [PATCH 6/7] proper position for the version number of uuid --- .../src/main/java/tech/ydb/core/keygen/BaseKeyGen.java | 9 +++++++++ .../src/main/java/tech/ydb/core/keygen/UuidKeyGen.java | 10 ++++------ .../java/tech/ydb/core/keygen/KeyGenTestSupport.java | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java index b1ddb7486..2461d3d52 100644 --- a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java @@ -132,6 +132,15 @@ public static long reorder(long v) { | (b5 << 24) | (b4 << 16) | (b7 << 8) | b6; } + public static long updateVersion(long msb) { + return (msb & ~0xF000L) | 0x8000L; + } + + public static long updateVariant(long lsb) { + return (lsb & 0x3FFFFFFFFFFFFFFFL) + | 0x8000000000000000L; + } + /** * Convert a UUID value to a base64 text representation. * diff --git a/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java index c62da43cc..befbcaec0 100644 --- a/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/UuidKeyGen.java @@ -73,11 +73,6 @@ public UUID nextValue(long prefix, Instant instant) { byte[] data = new byte[16]; sr.nextBytes(data); - data[6] &= 0x0f; - data[6] |= (byte) 0x80; - data[8] &= 0x3f; - data[8] |= (byte) 0x80; - long msb = 0; long lsb = 0; for (int i = 0; i < 8; i++) { @@ -88,7 +83,10 @@ public UUID nextValue(long prefix, Instant instant) { } msb = update(msb, prefix, instant); - return new UUID(reorder(msb), lsb); + msb = reorder(msb); + msb = updateVersion(msb); + lsb = updateVariant(lsb); + return new UUID(msb, lsb); } /** diff --git a/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java b/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java index 8c19dc86c..20d97f385 100644 --- a/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java +++ b/core/src/test/java/tech/ydb/core/keygen/KeyGenTestSupport.java @@ -51,7 +51,7 @@ static int extractEmbeddedTimestampCode(long logicalMsb, int prefixBits) { } static int logicalVersionNibble(long logicalMsb) { - return (int) ((logicalMsb >> 12) & 0x0f); + return (int) ((logicalMsb >>> 4) & 0x0f); } static int logicalVariantBits(long logicalLsb) { From 74b2bc6180647e1736921cd6efb0cafa4237a89a Mon Sep 17 00:00:00 2001 From: Maksim Zinal Date: Mon, 13 Jul 2026 13:42:35 +0300 Subject: [PATCH 7/7] Update BaseKeyGen.java Make BaseKeyGen public, as it is a superclass of other public classes --- core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java index 2461d3d52..89032168f 100644 --- a/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java +++ b/core/src/main/java/tech/ydb/core/keygen/BaseKeyGen.java @@ -12,7 +12,7 @@ * * @author zinal */ -class BaseKeyGen { +public class BaseKeyGen { /** * Bit width of the embedded second-precision timestamp field.