Skip to content

Commit f27cf18

Browse files
updating uuidv7 options
1 parent a864137 commit f27cf18

File tree

2 files changed

+45
-50
lines changed

2 files changed

+45
-50
lines changed

lib/ecto/application.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ defmodule Ecto.Application do
33
use Application
44

55
def start(_type, _args) do
6+
:ok = :persistent_term.put({Ecto.UUID, :millisecond}, :atomics.new(1, signed: false))
7+
:ok = :persistent_term.put({Ecto.UUID, :nanosecond}, :atomics.new(1, signed: false))
8+
69
children = [
710
Ecto.Repo.Registry
811
]

lib/ecto/uuid.ex

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,14 @@ defmodule Ecto.UUID do
3434
"""
3535
@type option ::
3636
{:version, 4 | 7}
37-
| {:monotonic_method, :millisecond | :increased_clock_precision}
37+
| {:precision, :millisecond | :nanosecond}
38+
| {:monotonic, boolean()}
3839

3940
@type options :: [option]
4041

41-
# The bits available for sub-millisecond fractions when using increased clock
42-
# precision.
43-
@sub_ms_bits 12
44-
45-
# The number of values that can be represented in the bit space (2^12).
46-
@possible_values Bitwise.bsl(1, @sub_ms_bits)
47-
48-
# The number of nanoseconds in a millisecond.
49-
@ns_per_ms 1_000_000
50-
51-
# The minimum step when using increased clock precision with fractional
52-
# milliseconds.
53-
@minimal_step_ns div(@ns_per_ms, @possible_values)
42+
@version_4 4
43+
@version_7 7
44+
@variant 2
5445

5546
@doc false
5647
def type, do: :uuid
@@ -244,56 +235,57 @@ defmodule Ecto.UUID do
244235

245236
defp bingenerate_v4 do
246237
<<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)
247-
<<u0::48, 4::4, u1::12, 2::2, u2::62>>
238+
<<u0::48, @version_4::4, u1::12, @variant::2, u2::62>>
248239
end
249240

241+
# The bits available for sub-millisecond fractions when using increased clock
242+
# precision based on nanoseconds.
243+
@ns_sub_ms_bits 12
244+
# The number of values that can be represented in the bit space (2^12).
245+
@ns_possible_values Bitwise.bsl(1, @ns_sub_ms_bits)
246+
# The number of nanoseconds in a millisecond.
247+
@ns_per_ms 1_000_000
248+
# The minimum step when using increased clock precision with fractional
249+
# milliseconds based on nanoseconds.
250+
@ns_minimal_step div(@ns_per_ms, @ns_possible_values)
251+
250252
defp bingenerate_v7(opts) do
251-
# From RFC 9562:
252-
# Monotonicity (each subsequent value being greater than the last) is the
253-
# backbone of time-based sortable UUIDs. Normally, time-based UUIDs will be
254-
# monotonic due to an embedded timestamp; however, implementations can
255-
# guarantee additional monotonicity via the concepts covered in section 6.2.
256-
case Keyword.get(opts, :monotonic_method) do
257-
# Millisecond granularity only. No monotonic guarantee.
258-
nil ->
259-
milliseconds = System.system_time(:millisecond)
260-
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
261-
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
253+
time_unit = Keyword.get(opts, :precision, :millisecond)
254+
monotonic = Keyword.get(opts, :monotonic, false)
255+
256+
timestamp =
257+
case monotonic do
258+
true -> next_ascending(time_unit)
259+
false -> System.system_time(time_unit)
260+
monotonic -> raise ArgumentError, "invalid monotonic value: #{inspect(monotonic)}"
261+
end
262262

263-
# For single-node UUID implementations that do not need to create
264-
# batches of UUIDs, the embedded timestamp within UUIDv7 can provide
265-
# sufficient monotonicity guarantees by simply ensuring that timestamp
266-
# increments before creating a new UUID. (RFC9562§6.2)
263+
case time_unit do
267264
:millisecond ->
268-
milliseconds = next_ascending(:millisecond)
269265
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
270-
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
271-
272-
# Replace Leftmost Random Bits with Increased Clock Precision (RFC9562§6.2, Method 3):
273-
:increased_clock_precision ->
274-
nanoseconds = next_ascending(:nanosecond)
275-
milliseconds = div(nanoseconds, @ns_per_ms)
276-
clock_precision = (rem(nanoseconds, @ns_per_ms) * @possible_values) |> div(@ns_per_ms)
277-
<<_rand_a::2, rand_b::62>> = :crypto.strong_rand_bytes(8)
278-
<<milliseconds::48, 7::4, clock_precision::12, 2::2, rand_b::62>>
279-
280-
method ->
281-
raise ArgumentError, "invalid monotonic method: #{inspect(method)}"
266+
<<timestamp::48, @version_7::4, rand_a::12, @variant::2, rand_b::62>>
267+
268+
:nanosecond ->
269+
milliseconds = div(timestamp, @ns_per_ms)
270+
271+
clock_precision =
272+
(rem(timestamp, @ns_per_ms) * @ns_possible_values) |> div(@ns_per_ms)
273+
274+
<<rand_b::62, _::2>> = :crypto.strong_rand_bytes(8)
275+
<<milliseconds::48, @version_7::4, clock_precision::12, @variant::2, rand_b::62>>
276+
277+
time_unit ->
278+
raise ArgumentError, "unsupported precision: #{inspect(time_unit)}"
282279
end
283280
end
284281

285282
defp next_ascending(time_unit) when time_unit in [:millisecond, :nanosecond] do
286-
timestamp_ref =
287-
with nil <- :persistent_term.get({__MODULE__, time_unit}, nil) do
288-
timestamp_ref = :atomics.new(1, signed: false)
289-
:ok = :persistent_term.put({__MODULE__, time_unit}, timestamp_ref)
290-
timestamp_ref
291-
end
283+
timestamp_ref = :persistent_term.get({__MODULE__, time_unit}, nil)
292284

293285
step =
294286
case time_unit do
295287
:millisecond -> 1
296-
:nanosecond -> @minimal_step_ns
288+
:nanosecond -> @ns_minimal_step
297289
end
298290

299291
previous_ts = :atomics.get(timestamp_ref, 1)

0 commit comments

Comments
 (0)