Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UUID v7 #14732

Merged
merged 7 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions spec/std/uuid_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,21 @@ describe "UUID" do
UUID.v5_x500(data).v5?.should eq(true)
end
end

describe "v7" do
it "generates a v7 UUID" do
uuid = UUID.v7
uuid.v7?.should eq true
uuid.variant.rfc9562?.should eq true
end

pending_wasm32 "generates UUIDs that are sortable with 1ms precision" do
uuids = Array.new(10) do
jgaskins marked this conversation as resolved.
Show resolved Hide resolved
sleep 1.millisecond
UUID.v7
end

uuids.should eq uuids.sort
end
end
end
33 changes: 31 additions & 2 deletions src/uuid.cr
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ struct UUID
NCS
# Reserved for RFC 4122 Specification (default).
RFC4122
# Reserved for RFC 9562 Specification (default for v7).
RFC9562 = RFC4122
# Reserved by Microsoft for backward compatibility.
Microsoft
# Reserved for future expansion.
Expand All @@ -43,6 +45,8 @@ struct UUID
V4 = 4
# SHA1 hash and namespace.
V5 = 5
# Prefixed with a UNIX timestamp with millisecond precision, filled in with randomness.
V7 = 7
end

# A Domain represents a Version 2 domain (DCE security).
Expand Down Expand Up @@ -80,7 +84,7 @@ struct UUID
# do nothing
when Variant::NCS
@bytes[8] = (@bytes[8] & 0x7f)
when Variant::RFC4122
when Variant::RFC4122, Variant::RFC9562
@bytes[8] = (@bytes[8] & 0x3f) | 0x80
when Variant::Microsoft
@bytes[8] = (@bytes[8] & 0x1f) | 0xc0
Expand Down Expand Up @@ -321,6 +325,30 @@ struct UUID
end
{% end %}

# Generates an RFC9562-compatible v7 UUID, allowing the values to be sorted
# chronologically (with 1ms precision) by their raw or hexstring
# representation.
def self.v7(random r : Random = Random::Secure)
buffer = uninitialized UInt8[18]
value = buffer.to_slice

# Generate the first 48 bits of the UUID with the current timestamp. We
# allocated enough room for a 64-bit timestamp to accommodate the
# NetworkEndian.encode call here, but we only need 48 bits of it so we chop
# off the first 2 bytes.
IO::ByteFormat::NetworkEndian.encode Time.utc.to_unix_ms, value
value = value[2..]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (nitpick): maybe extract a timestamp variable and rearrange a bit the above to make the steps more explicit?

timestamp = Time.utc.to_unix_ms
buffer = uninitialized UInt8[18] # 2 bytes (skipped, see below) + 16 bytes (uuid)

# convert timestamp integer into bytes and skip the upper bytes (...)
IO::ByteFormat::NetworkEndian.encode timestamp, buffer.to_slice
value = buffer.to_slice[2..]


# Fill in the rest with random bytes
r.random_bytes(value[6..])

# Set the version and variant
value[6] = (value[6] & 0x3F) | 0x70
value[8] = (value[8] & 0x0F) | 0x80

new(value, variant: :rfc9562, version: :v7)
end

# Generates an empty UUID.
#
# ```
Expand Down Expand Up @@ -375,6 +403,7 @@ struct UUID
when 3 then Version::V3
when 4 then Version::V4
when 5 then Version::V5
when 7 then Version::V7
else Version::Unknown
end
end
Expand Down Expand Up @@ -442,7 +471,7 @@ struct UUID
class Error < Exception
end

{% for v in %w(1 2 3 4 5) %}
{% for v in %w(1 2 3 4 5 7) %}
# Returns `true` if UUID is a V{{ v.id }}, `false` otherwise.
def v{{ v.id }}?
variant == Variant::RFC4122 && version == Version::V{{ v.id }}
Expand Down
Loading