diff options
| author | nick evans <nicholas.evans@gmail.com> | 2023-06-29 18:39:29 -0400 |
|---|---|---|
| committer | git <svn-admin@ruby-lang.org> | 2023-09-19 01:55:27 +0000 |
| commit | dfb2b4cbc9aa5edc315e210bf6bfd92fcf6e45de (patch) | |
| tree | 239d6eb81409e7d3e3fecfd979570ef2fa44fd05 /test/ruby | |
| parent | e77c766b7ab17e801c5cfa881754c392f8c13f0b (diff) | |
[ruby/securerandom] Add support for UUID version 7
Although the specification for UUIDv7 is still in draft, the UUIDv7
algorithm has been relatively stable as it progresses to completion.
Version 7 UUIDs can be very useful, because they are lexographically
sortable, which can improve e.g: database index locality. See section
6.10 of the draft specification for further explanation:
https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/
The specification allows up to 12 bits of extra timestamp precision, to
make UUID generation closer to monotonically increasing. This provides
between 1ms and ~240ns of timestamp precision. At the cost of some code
complexity and a small performance penalty, a kwarg may specify any
arbitrary precision between 0 and 12 extra bits. Any stronger
guarantees of monotonicity have considerably larger tradeoffs, so
nothing more is implemented. This limitation is documented.
Ruby issue: https://bugs.ruby-lang.org/issues/19735
https://github.com/ruby/securerandom/commit/34ed1a2ec3
Diffstat (limited to 'test/ruby')
| -rw-r--r-- | test/ruby/test_random_formatter.rb | 48 |
1 files changed, 48 insertions, 0 deletions
diff --git a/test/ruby/test_random_formatter.rb b/test/ruby/test_random_formatter.rb index 2c01d99b3d..a061946e97 100644 --- a/test/ruby/test_random_formatter.rb +++ b/test/ruby/test_random_formatter.rb @@ -75,6 +75,54 @@ module Random::Formatter assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, uuid) end + def test_uuid_v7(extra_timestamp_bits) + t1 = current_uuid7_time + uuid = @it.uuid_v7 + t3 = current_uuid7_time + + assert_match(/\A\h{8}-\h{4}-7\h{3}-[89ab]\h{3}-\h{12}\z/, uuid) + + t2 = get_uuid7_time(uuid) + assert_operator(t1, :<=, t2) + assert_operator(t2, :<=, t3) + end + + def test_uuid_v7_extra_timestamp_bits + 0.upto(12) do |extra_timestamp_bits| + t1 = current_uuid7_time extra_timestamp_bits: extra_timestamp_bits + uuid = @it.uuid_v7 extra_timestamp_bits: extra_timestamp_bits + t3 = current_uuid7_time extra_timestamp_bits: extra_timestamp_bits + + assert_match(/\A\h{8}-\h{4}-7\h{3}-[89ab]\h{3}-\h{12}\z/, uuid) + + t2 = get_uuid7_time uuid, extra_timestamp_bits: extra_timestamp_bits + assert_operator(t1, :<=, t2) + assert_operator(t2, :<=, t3) + end + end + + # It would be nice to simply use Time#floor here. But that is problematic + # due to the difference between decimal vs binary fractions. + def current_uuid7_time(extra_timestamp_bits: 0) + denominator = (1 << extra_timestamp_bits).to_r + Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + .then {|ns| ((ns / 1_000_000r) * denominator).floor / denominator } + .then {|ms| Time.at(ms / 1000r, in: "+00:00") } + end + + def get_uuid7_time(uuid, extra_timestamp_bits: 0) + denominator = (1 << extra_timestamp_bits) * 1000r + extra_chars = extra_timestamp_bits / 4 + last_char_bits = extra_timestamp_bits % 4 + extra_chars += 1 if last_char_bits != 0 + timestamp_re = /\A(\h{8})-(\h{4})-7(\h{#{extra_chars}})/ + timestamp_chars = uuid.match(timestamp_re).captures.join + timestamp = timestamp_chars.to_i(16) + timestamp >>= 4 - last_char_bits unless last_char_bits == 0 + timestamp /= denominator + Time.at timestamp, in: "+00:00" + end + def test_alphanumeric 65.times do |n| an = @it.alphanumeric(n) |
