summaryrefslogtreecommitdiff
path: root/test/ruby
diff options
context:
space:
mode:
authornick evans <nicholas.evans@gmail.com>2023-06-29 18:39:29 -0400
committergit <svn-admin@ruby-lang.org>2023-09-19 01:55:27 +0000
commitdfb2b4cbc9aa5edc315e210bf6bfd92fcf6e45de (patch)
tree239d6eb81409e7d3e3fecfd979570ef2fa44fd05 /test/ruby
parente77c766b7ab17e801c5cfa881754c392f8c13f0b (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.rb48
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)