@@ -32,7 +32,16 @@ defmodule Ecto.UUID do
3232 @ typedoc """
3333 currently supported option is version, it accepts 4 or 7.
3434 """
35- @ type options :: [ version: 4 | 7 ]
35+ @ type option ::
36+ { :version , 4 | 7 }
37+ | { :precision , :millisecond | :nanosecond }
38+ | { :monotonic , boolean ( ) }
39+
40+ @ type options :: [ option ]
41+
42+ @ version_4 4
43+ @ version_7 7
44+ @ variant 2
3645
3746 @ doc false
3847 def type , do: :uuid
@@ -206,34 +215,145 @@ defmodule Ecto.UUID do
206215
207216 @ default_version 4
208217 @ doc """
209- Generates a uuid with the given options.
218+ Generates a UUID string.
219+
220+ ## Options
221+
222+ * `:version` - The UUID version to generate. Supported values are `4` (random)
223+ and `7` (time-ordered). Defaults to `4`.
224+
225+ ## Options (version 7)
226+
227+ * `:precision` - The timestamp precision for version 7 UUIDs. Supported values
228+ are `:millisecond` and `:nanosecond`. Defaults to `:millisecond` if
229+ monotonic is `false` and `:nanosecond` if `:monotonic` is `true`.
230+ When using `:nanosecond`, the sub-millisecond precision is encoded in the
231+ `rand_a` field. NOTE: Due to the 12-bit space available, nanosecond
232+ precision is limited to 4096 (2^12) distinct values per millisecond.
233+
234+ * `:monotonic` - When `true`, ensures that generated version 7 UUIDs are
235+ strictly monotonically increasing, even when multiple UUIDs are generated
236+ within the same timestamp. This is useful for maintaining insertion order
237+ in databases. Defaults to `false`.
238+ NOTE: With `:millisecond` precision, generating multiple UUIDs within the
239+ same millisecond increments the timestamp by 1ms for each UUID, causing the
240+ embedded timestamp to drift ahead of real time under high throughput.
241+ Using `precision: :nanosecond` reduces this drift significantly, as
242+ timestamps only advance by ~244ns per UUID when generation outpaces real
243+ time. When monotonic UUIDs are desired, it is recommended to also use
244+ `precision: :nanosecond`.
245+
246+ ## Examples
247+
248+ > Ecto.UUID.generate()
249+ "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
250+
251+ > Ecto.UUID.generate(version: 7)
252+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
253+
254+ > Ecto.UUID.generate(version: 7, precision: :nanosecond)
255+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
256+
257+ > Ecto.UUID.generate(version: 7, monotonic: true)
258+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
259+
210260 """
211261 @ spec generate ( ) :: t
212262 @ spec generate ( options ) :: t
213263 def generate ( opts \\ [ ] ) , do: encode ( bingenerate ( opts ) )
214264
215265 @ doc """
216266 Generates a uuid with the given options in binary format.
267+ See `generate/1` for options.
217268 """
218269 @ spec bingenerate ( options ) :: raw
219270 def bingenerate ( opts \\ [ ] ) do
220- case Keyword . get ( opts , :version , @ default_version ) do
221- 4 -> bingenerate_v4 ( )
222- 7 -> bingenerate_v7 ( )
223- version -> raise ArgumentError , "unknown UUID version: #{ inspect ( version ) } "
271+ case Keyword . pop ( opts , :version , @ default_version ) do
272+ { 4 , _opts } -> bingenerate_v4 ( )
273+ { 7 , opts } -> bingenerate_v7 ( opts )
274+ { version , _ } -> raise ArgumentError , "unsupported UUID version: #{ inspect ( version ) } "
224275 end
225276 end
226277
227278 defp bingenerate_v4 do
228279 << u0 :: 48 , _ :: 4 , u1 :: 12 , _ :: 2 , u2 :: 62 >> = :crypto . strong_rand_bytes ( 16 )
229- << u0 :: 48 , 4 :: 4 , u1 :: 12 , 2 :: 2 , u2 :: 62 >>
280+ << u0 :: 48 , @ version_4 :: 4 , u1 :: 12 , @ variant :: 2 , u2 :: 62 >>
230281 end
231282
232- defp bingenerate_v7 do
233- milliseconds = System . system_time ( :millisecond )
234- << u0 :: 12 , u1 :: 62 , _ :: 6 >> = :crypto . strong_rand_bytes ( 10 )
283+ # The bits available for sub-millisecond fractions when using increased clock
284+ # precision based on nanoseconds.
285+ @ ns_sub_ms_bits 12
286+ # The number of values that can be represented in the bit space (2^12).
287+ @ ns_possible_values Bitwise . bsl ( 1 , @ ns_sub_ms_bits )
288+ # The number of nanoseconds in a millisecond.
289+ @ ns_per_ms 1_000_000
290+ # The minimum step when using increased clock precision with fractional
291+ # milliseconds based on nanoseconds.
292+ @ ns_minimal_step div ( @ ns_per_ms , @ ns_possible_values )
293+
294+ defp bingenerate_v7 ( opts ) do
295+ monotonic = Keyword . get ( opts , :monotonic , false )
296+ time_unit = Keyword . get ( opts , :precision , if ( monotonic , do: :nanosecond , else: :millisecond ) )
297+
298+ timestamp =
299+ case monotonic do
300+ true -> next_ascending ( time_unit )
301+ false -> System . system_time ( time_unit )
302+ monotonic -> raise ArgumentError , "invalid monotonic value: #{ inspect ( monotonic ) } "
303+ end
304+
305+ case time_unit do
306+ :millisecond ->
307+ << rand_a :: 12 , _ :: 6 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 10 )
308+ << timestamp :: 48 , @ version_7 :: 4 , rand_a :: 12 , @ variant :: 2 , rand_b :: 62 >>
309+
310+ :nanosecond ->
311+ milliseconds = div ( timestamp , @ ns_per_ms )
312+
313+ clock_precision =
314+ ( rem ( timestamp , @ ns_per_ms ) * @ ns_possible_values ) |> div ( @ ns_per_ms )
315+
316+ << rand_b :: 62 , _ :: 2 >> = :crypto . strong_rand_bytes ( 8 )
317+ << milliseconds :: 48 , @ version_7 :: 4 , clock_precision :: 12 , @ variant :: 2 , rand_b :: 62 >>
318+
319+ time_unit ->
320+ raise ArgumentError , "unsupported precision: #{ inspect ( time_unit ) } "
321+ end
322+ end
235323
236- << milliseconds :: 48 , 7 :: 4 , u0 :: 12 , 2 :: 2 , u1 :: 62 >>
324+ defp next_ascending ( time_unit ) when time_unit in [ :millisecond , :nanosecond ] do
325+ timestamp_ref = :persistent_term . get ( { __MODULE__ , time_unit } , nil )
326+
327+ step =
328+ case time_unit do
329+ :millisecond -> 1
330+ :nanosecond -> @ ns_minimal_step
331+ end
332+
333+ previous_ts = :atomics . get ( timestamp_ref , 1 )
334+ min_step_ts = previous_ts + step
335+ current_ts = System . system_time ( time_unit )
336+
337+ # If the current timestamp is not at least the minimal step greater than the
338+ # previous step, then we make it so.
339+ new_ts =
340+ if current_ts > min_step_ts do
341+ current_ts
342+ else
343+ min_step_ts
344+ end
345+
346+ compare_exchange ( timestamp_ref , previous_ts , new_ts , step )
347+ end
348+
349+ defp compare_exchange ( timestamp_ref , previous_ts , new_ts , step ) do
350+ case :atomics . compare_exchange ( timestamp_ref , 1 , previous_ts , new_ts ) do
351+ # If the new value was written, then we return it.
352+ :ok -> new_ts
353+ # If the atomic value has changed in the meantime, we add the minimal step
354+ # nanoseconds value to that and try again.
355+ updated_ts -> compare_exchange ( timestamp_ref , updated_ts , updated_ts + step , step )
356+ end
237357 end
238358
239359 # Callback invoked by autogenerate fields.
0 commit comments