Skip to content

Commit da23d60

Browse files
updating uuidv7 options
1 parent a864137 commit da23d60

File tree

2 files changed

+43
-41
lines changed

2 files changed

+43
-41
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: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,21 @@ 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

4142
# The bits available for sub-millisecond fractions when using increased clock
42-
# precision.
43-
@sub_ms_bits 12
44-
43+
# precision based on nanoseconds.
44+
@ns_sub_ms_bits 12
4545
# The number of values that can be represented in the bit space (2^12).
46-
@possible_values Bitwise.bsl(1, @sub_ms_bits)
47-
46+
@ns_possible_values Bitwise.bsl(1, @ns_sub_ms_bits)
4847
# The number of nanoseconds in a millisecond.
4948
@ns_per_ms 1_000_000
50-
5149
# The minimum step when using increased clock precision with fractional
52-
# milliseconds.
53-
@minimal_step_ns div(@ns_per_ms, @possible_values)
50+
# milliseconds based on nanoseconds.
51+
@ns_minimal_step div(@ns_per_ms, @ns_possible_values)
5452

5553
@doc false
5654
def type, do: :uuid
@@ -248,52 +246,53 @@ defmodule Ecto.UUID do
248246
end
249247

250248
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)
249+
time_unit = Keyword.get(opts, :precision, :millisecond)
250+
monotonic = Keyword.get(opts, :monotonic, false)
251+
252+
case {time_unit, monotonic} do
253+
{:millisecond, _monotonic = true} ->
254+
unix_ts_ms = next_ascending(:millisecond)
260255
<<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>>
256+
<<unix_ts_ms::48, 7::4, rand_a::12, 2::2, rand_b::62>>
262257

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)
267-
:millisecond ->
268-
milliseconds = next_ascending(:millisecond)
258+
{:millisecond, _monotonic = false} ->
259+
unix_ts_ms = System.system_time(:millisecond)
269260
<<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>>
261+
<<unix_ts_ms::48, 7::4, rand_a::12, 2::2, rand_b::62>>
262+
263+
{:nanosecond, _monotonic = true} ->
264+
unix_ts_ns = next_ascending(:nanosecond)
265+
milliseconds = div(unix_ts_ns, @ns_per_ms)
271266

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)
267+
clock_precision =
268+
(rem(unix_ts_ns, @ns_per_ms) * @ns_possible_values) |> div(@ns_per_ms)
269+
270+
<<rand_b::62, _::2>> = :crypto.strong_rand_bytes(8)
278271
<<milliseconds::48, 7::4, clock_precision::12, 2::2, rand_b::62>>
279272

280-
method ->
281-
raise ArgumentError, "invalid monotonic method: #{inspect(method)}"
273+
{:nanosecond, _monotonic = false} ->
274+
unix_ts_ns = System.system_time(:nanosecond)
275+
milliseconds = div(unix_ts_ns, @ns_per_ms)
276+
nanoseconds = rem(unix_ts_ns, @ns_per_ms)
277+
278+
milliseconds =
279+
if nanoseconds > @ns_possible_values, do: milliseconds + 1, else: milliseconds
280+
281+
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
282+
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
283+
284+
{_time_unit, _monotonic} ->
285+
raise ArgumentError, "unsupported options: #{inspect(opts)}"
282286
end
283287
end
284288

285289
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
290+
timestamp_ref = :persistent_term.get({__MODULE__, time_unit}, nil)
292291

293292
step =
294293
case time_unit do
295294
:millisecond -> 1
296-
:nanosecond -> @minimal_step_ns
295+
:nanosecond -> @ns_minimal_step
297296
end
298297

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

0 commit comments

Comments
 (0)