@@ -19,7 +19,7 @@ defmodule Ecto.UUID do
1919 To use UUID v7 (time-ordered) monotonic:
2020
2121 use Ecto.Schema
22- @primary_key {:id, Ecto.UUID, autogenerate: [version: 7, monotonic: true ]}
22+ @primary_key {:id, Ecto.UUID, autogenerate: [version: 7, precision: :monotonic ]}
2323
2424 According to [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters):
2525 "Monotonicity (each subsequent value being greater than the last) is the
@@ -39,12 +39,11 @@ defmodule Ecto.UUID do
3939 @ type raw :: << _ :: 128 >>
4040
4141 @ typedoc """
42- Supported options: `:version`, `:precision` (v7-only), and `:monotonic ` (v7-only).
42+ Supported options: `:version` and `:precision ` (v7-only).
4343 """
4444 @ type option ::
4545 { :version , 4 | 7 }
46- | { :precision , :millisecond | :nanosecond }
47- | { :monotonic , boolean ( ) }
46+ | { :precision , :millisecond | :monotonic }
4847
4948 @ type options :: [ option ]
5049
@@ -233,24 +232,15 @@ defmodule Ecto.UUID do
233232
234233 ## Options (version 7 only)
235234
236- * `:precision` - The timestamp precision for version 7 UUIDs. Supported values
237- are `:millisecond` and `:nanosecond`. Defaults to `:millisecond` if
238- monotonic is `false` and `:nanosecond` if `:monotonic` is `true`.
239- When using `:nanosecond`, the sub-millisecond precision is encoded in the
240- `rand_a` field. NOTE: Due to the 12-bit space available, nanosecond
241- precision is limited to 4096 (2^12) distinct values per millisecond.
242-
243- * `:monotonic` - When `true`, ensures that generated version 7 UUIDs are
244- strictly monotonically increasing, even when multiple UUIDs are generated
245- within the same timestamp. This is useful for maintaining insertion order
246- in databases. Defaults to `false`.
247- NOTE: With `:millisecond` precision, generating multiple UUIDs within the
248- same millisecond increments the timestamp by 1ms for each UUID, causing the
249- embedded timestamp to drift ahead of real time under high throughput.
250- Using `precision: :nanosecond` reduces this drift significantly, as
251- timestamps only advance by 244ns per UUID when generation outpaces real
252- time. When monotonic UUIDs are desired, it is recommended to also use
253- `precision: :nanosecond`.
235+ * `:precision` - The timestamp precision for version 7 UUIDs. Supported
236+ values are `:millisecond` and `:monotonic`. Defaults to `:millisecond`.
237+
238+ > #### Monotonic precision {: .info}
239+ >
240+ > When using `:monotonic`, sub-millisecond precision is encoded in the
241+ > `rand_a` field. The generated version 7 UUIDs are strictly monotonically
242+ > increasing (per node), even when multiple UUIDs are generated within the same
243+ > timestamp. This is useful for maintaining insertion order in databases.
254244
255245 ## Examples
256246
@@ -260,10 +250,10 @@ defmodule Ecto.UUID do
260250 > Ecto.UUID.generate(version: 7)
261251 "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
262252
263- > Ecto.UUID.generate(version: 7, precision: :nanosecond )
253+ > Ecto.UUID.generate(version: 7, precision: :millisecond )
264254 "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
265255
266- > Ecto.UUID.generate(version: 7, monotonic: true )
256+ > Ecto.UUID.generate(version: 7, precision: :monotonic )
267257 "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
268258
269259 """
@@ -291,7 +281,7 @@ defmodule Ecto.UUID do
291281 end
292282
293283 # The bits available for sub-millisecond fractions when using increased clock
294- # precision based on nanoseconds.
284+ # precision for monotonicity ( based on nanoseconds) .
295285 @ ns_sub_ms_bits 12
296286 # The number of values that can be represented in the bit space (2^12).
297287 @ ns_possible_values Bitwise . bsl ( 1 , @ ns_sub_ms_bits )
@@ -302,22 +292,17 @@ defmodule Ecto.UUID do
302292 @ ns_minimal_step div ( @ ns_per_ms , @ ns_possible_values )
303293
304294 defp bingenerate_v7 ( opts ) do
305- monotonic = Keyword . get ( opts , :monotonic , false )
306- time_unit = Keyword . get ( opts , :precision , if ( monotonic , do: :nanosecond , else: :millisecond ) )
307-
308- timestamp =
309- case monotonic do
310- true -> next_ascending ( time_unit )
311- false -> System . system_time ( time_unit )
312- monotonic -> raise ArgumentError , "invalid monotonic value: #{ inspect ( monotonic ) } "
313- end
295+ { precision , rest } = Keyword . pop ( opts , :precision , :millisecond )
296+ if rest != [ ] , do: raise ( ArgumentError , "unsupported options for v7: #{ inspect ( rest ) } " )
314297
315- case time_unit do
298+ case precision do
316299 :millisecond ->
300+ timestamp = System . system_time ( :millisecond )
317301 << rand_a :: 12 , _ :: 6 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 10 )
318302 << timestamp :: 48 , @ version_7 :: 4 , rand_a :: 12 , @ variant :: 2 , rand_b :: 62 >>
319303
320- :nanosecond ->
304+ :monotonic ->
305+ timestamp = next_ascending ( )
321306 milliseconds = div ( timestamp , @ ns_per_ms )
322307
323308 clock_precision =
@@ -326,39 +311,33 @@ defmodule Ecto.UUID do
326311 << _ :: 2 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 8 )
327312 << milliseconds :: 48 , @ version_7 :: 4 , clock_precision :: 12 , @ variant :: 2 , rand_b :: 62 >>
328313
329- time_unit ->
330- raise ArgumentError , "unsupported precision: #{ inspect ( time_unit ) } "
314+ precision ->
315+ raise ArgumentError , "unsupported precision: #{ inspect ( precision ) } "
331316 end
332317 end
333318
334- defp next_ascending ( time_unit ) when time_unit in [ :millisecond , :nanosecond ] do
319+ defp next_ascending do
335320 timestamp_ref =
336- :persistent_term . get ( { __MODULE__ , time_unit } , nil ) || raise "Ecto has not been started"
337-
338- step =
339- case time_unit do
340- :millisecond -> 1
341- :nanosecond -> @ ns_minimal_step
342- end
321+ :persistent_term . get ( { __MODULE__ , :nanosecond } , nil ) || raise "Ecto has not been started"
343322
344323 previous_ts = :atomics . get ( timestamp_ref , 1 )
345- min_step_ts = previous_ts + step
346- current_ts = System . system_time ( time_unit )
324+ min_step_ts = previous_ts + @ ns_minimal_step
325+ current_ts = System . system_time ( :nanosecond )
347326
348327 # If the current timestamp is not at least the minimal step greater than the
349328 # previous step, then we make it so.
350329 new_ts = max ( current_ts , min_step_ts )
351330
352- compare_exchange ( timestamp_ref , previous_ts , new_ts , step )
331+ compare_exchange ( timestamp_ref , previous_ts , new_ts )
353332 end
354333
355- defp compare_exchange ( timestamp_ref , previous_ts , new_ts , step ) do
334+ defp compare_exchange ( timestamp_ref , previous_ts , new_ts ) do
356335 case :atomics . compare_exchange ( timestamp_ref , 1 , previous_ts , new_ts ) do
357336 # If the new value was written, then we return it.
358337 :ok -> new_ts
359338 # Otherwise, the atomic value has changed in the meantime. We add the
360339 # minimal step value to that and try again.
361- updated_ts -> compare_exchange ( timestamp_ref , updated_ts , updated_ts + step , step )
340+ updated_ts -> compare_exchange ( timestamp_ref , updated_ts , updated_ts + @ ns_minimal_step )
362341 end
363342 end
364343
0 commit comments