UUIDv7 (UUUIDv7 v0.3.0)

Copy Markdown View Source

UUIDv7 for Elixir.

Used for generating version 7 UUIDs using submicrosecond precision. Normally the default precision is 1 millisecond, this causes issues when generating UUIDs in bulk because we can generate multiple UUIDs in the same millisecond. This module allows you to generate UUIDs with submicrosecond precision.

UUIDv7.generate() "018e90d8-06e8-7f9f-bfd7-6730ba98a51b"

UUIDv7.bingenerate() <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>

Summary

Types

A raw binary representation of a UUID.

t()

A hex-encoded UUID string.

Functions

Generates a version 7 UUID in the binary format.

Generates a random version 7 UUID in binary format for a specific DateTime.

Bulk-generates n raw UUIDv7 binaries.

Decode a string representation of a UUID to the raw binary version.

Callback implementation for Ecto.Type.embed_as/1.

Encode a raw UUID to the string representation.

Callback implementation for Ecto.Type.equal?/2.

Extract the millisecond timestamp from the UUID.

Generates a version 7 UUID using submilliseconds for increased clock precision.

Generates a random version 7 UUID for a specific DateTime.

String-encoded counterpart of bingenerate_many/1.

returns the current time in milliseconds and scaled nanoseconds

Generates the minimum UUID v7 for a given Date or DateTime.

scales the remaining nanoseconds to fit in 12 bits

Extracts a Date (UTC) from a UUIDv7.

Extracts a DateTime (UTC, millisecond precision) from a UUIDv7.

Types

raw()

@type raw() :: <<_::128>>

A raw binary representation of a UUID.

t()

@type t() :: <<_::288>>

A hex-encoded UUID string.

Functions

bingenerate()

Generates a version 7 UUID in the binary format.

Example

iex> [UUIDv7.bingenerate(), UUIDv7.bingenerate()] iex> |> Enum.map(& :binary.part(&1, 0, 5)) iex> |> Enum.reduce(fn second, first -> second == first end) true

bingenerate_from_datetime(datetime)

@spec bingenerate_from_datetime(DateTime.t()) :: raw()

Generates a random version 7 UUID in binary format for a specific DateTime.

Binary version of generate_from_datetime/1.

Example

iex> {:ok, dt, _} = DateTime.from_iso8601("2024-01-01T00:00:00.000Z")
iex> uuid = UUIDv7.bingenerate_from_datetime(dt)
iex> is_binary(uuid) and byte_size(uuid) == 16
true

bingenerate_many(n)

@spec bingenerate_many(pos_integer()) :: [raw()]

Bulk-generates n raw UUIDv7 binaries.

Each millisecond can hold up to 4096 strictly-ordered UUIDs (the capacity of the 12-bit sub-millisecond field). For n <= 4096 the whole batch shares a single millisecond. For larger batches, full chunks of 4096 are generated at consecutive milliseconds and the remainder (if any) lands in the next millisecond — so the batch keeps monotonically-increasing timestamps without ever colliding inside a single ms.

Within each millisecond the 12-bit sub-millisecond field is partitioned into n equal slots (step = div(4096, n)); each UUID's value is step * i + jitter, where the jitter is a crypto-strong offset within [0, step). This guarantees strict in-batch ordering while keeping positions inside each slot randomized.

Both rand_b (62 bits) and the jitter (12 bits) are sliced from the same :crypto.strong_rand_bytes/1 call — 10 bytes per UUID, no extra PRNG overhead.

Note: when n > 4096, this function burns div(n - 1, 4096) future milliseconds. Subsequent bingenerate/0 calls landing in those same milliseconds still won't collide (62 random bits per UUID), but conceptually the batch reaches ahead of the wall clock.

Examples

iex> uuids = UUIDv7.bingenerate_many(4)
iex> length(uuids)
4
iex> uuids == Enum.sort(uuids)
true

iex> uuids = UUIDv7.bingenerate_many(10_000)
iex> length(uuids)
10_000
iex> uuids == Enum.sort(uuids)
true

decode(arg1)

@spec decode(t()) :: raw() | :error

Decode a string representation of a UUID to the raw binary version.

Example

iex> UUIDv7.decode("018e90d8-06e8-7f9f-bfd7-6730ba98a51b")
<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>

embed_as(_)

Callback implementation for Ecto.Type.embed_as/1.

encode(arg)

@spec encode(raw()) :: t()

Encode a raw UUID to the string representation.

Example

iex> UUIDv7.encode(<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>)
"018e90d8-06e8-7f9f-bfd7-6730ba98a51b"

equal?(term1, term2)

Callback implementation for Ecto.Type.equal?/2.

extract_timestamp(uuid)

@spec extract_timestamp(t() | raw() | UUID.t()) :: integer()

Extract the millisecond timestamp from the UUID.

Example

iex> UUIDv7.extract_timestamp("018ecb40-c457-73e6-a400-000398daddd9")
1712807003223

generate()

@spec generate() :: t()

Generates a version 7 UUID using submilliseconds for increased clock precision.

Example

iex> [UUIDv7.generate(), UUIDv7.generate()] iex> |> Enum.map(& String.byte_slice(&1, 0, 11)) iex> |> Enum.reduce(fn second, first -> second == first end) true

generate_from_datetime(datetime)

@spec generate_from_datetime(DateTime.t()) :: t()

Generates a random version 7 UUID for a specific DateTime.

Similar to generate/0, but uses the provided DateTime for the timestamp instead of the current time. The sub-millisecond precision and random bits are still randomly generated.

Useful for:

  • Generating backdated UUIDs for testing
  • Creating UUIDs for historical data
  • Partitioning data by time ranges

Example

iex> {:ok, dt, _} = DateTime.from_iso8601("2024-01-01T00:00:00.000Z")
iex> uuid = UUIDv7.generate_from_datetime(dt)
iex> UUIDv7.extract_timestamp(uuid) == DateTime.to_unix(dt, :millisecond)
true

generate_many(n)

@spec generate_many(pos_integer()) :: [t()]

String-encoded counterpart of bingenerate_many/1.

Example

iex> uuids = UUIDv7.generate_many(8)
iex> length(uuids)
8
iex> uuids == Enum.sort(uuids)
true

get_time()

returns the current time in milliseconds and scaled nanoseconds

min_uuid(date)

@spec min_uuid(Date.t() | DateTime.t()) :: t()

Generates the minimum UUID v7 for a given Date or DateTime.

Creates a UUID with the timestamp from the given moment and all random bits set to zero — the smallest possible UUID for that millisecond. For a Date, midnight UTC of that day is used.

Used for half-closed time ranges in database partitioning:

  • Range [T1, T2): WHERE uuid >= min_uuid(T1) AND uuid < min_uuid(T2)
  • Ranges are continuous with no gaps or overlaps

Examples

iex> {:ok, dt, _} = DateTime.from_iso8601("2024-01-01T00:00:00.000Z")
iex> uuid = UUIDv7.min_uuid(dt)
iex> String.ends_with?(uuid, "7000-8000-000000000000")
true

iex> UUIDv7.min_uuid(~D[2024-01-01])
"018cc251-f400-7000-8000-000000000000"

scale_nanoseconds(nanos)

scales the remaining nanoseconds to fit in 12 bits

Example

iex> UUIDv7.scale_nanoseconds(0)
0
iex> UUIDv7.scale_nanoseconds(500000)
2048
iex> UUIDv7.scale_nanoseconds(999999)
0xFFF

to_date(uuid)

@spec to_date(t() | raw() | UUID.t()) :: Date.t()

Extracts a Date (UTC) from a UUIDv7.

Accepts the hex string form, the raw 16-byte binary, or a %UUID{} struct.

Example

iex> UUIDv7.to_date("018ecb40-c457-73e6-a400-000398daddd9")
~D[2024-04-11]

to_datetime(uuid)

@spec to_datetime(t() | raw() | UUID.t()) :: DateTime.t()

Extracts a DateTime (UTC, millisecond precision) from a UUIDv7.

Accepts the hex string form, the raw 16-byte binary, or a %UUID{} struct.

Example

iex> UUIDv7.to_datetime("018ecb40-c457-73e6-a400-000398daddd9")
~U[2024-04-11 03:43:23.223Z]