@@ -30,9 +30,21 @@ defmodule Ecto.UUID do
3030 @ type raw :: << _ :: 128 >>
3131
3232 @ typedoc """
33- currently supported option is version, it accepts 4 or 7.
33+ Supported options:
34+ * `:version` (4 or 7)
35+ * `:precision` (`:millisecond` | `:nanosecond`, v7 only)
36+ * `:monotonic` (boolean, v7 only).
3437 """
35- @ type options :: [ version: 4 | 7 ]
38+ @ type option ::
39+ { :version , 4 | 7 }
40+ | { :precision , :millisecond | :nanosecond }
41+ | { :monotonic , boolean ( ) }
42+
43+ @ type options :: [ option ]
44+
45+ @ version_4 4
46+ @ version_7 7
47+ @ variant 2
3648
3749 @ doc false
3850 def type , do: :uuid
@@ -206,34 +218,150 @@ defmodule Ecto.UUID do
206218
207219 @ default_version 4
208220 @ doc """
209- Generates a uuid with the given options.
221+ Generates a UUID string.
222+
223+ ## Options
224+
225+ * `:version` - The UUID version to generate. Supported values are `4` (random)
226+ and `7` (time-ordered). Defaults to `4`.
227+
228+ ## Options (version 7)
229+
230+ * `:precision` - The timestamp precision for version 7 UUIDs. Supported values
231+ are `:millisecond` and `:nanosecond`. Defaults to `:millisecond` if
232+ monotonic is `false` and `:nanosecond` if `:monotonic` is `true`.
233+ When using `:nanosecond`, the sub-millisecond precision is encoded in the
234+ `rand_a` field. NOTE: Due to the 12-bit space available, nanosecond
235+ precision is limited to 4096 (2^12) distinct values per millisecond.
236+
237+ * `:monotonic` - When `true`, ensures that generated version 7 UUIDs are
238+ strictly monotonically increasing, even when multiple UUIDs are generated
239+ within the same timestamp. This is useful for maintaining insertion order
240+ in databases. Defaults to `false`.
241+ NOTE: With `:millisecond` precision, generating multiple UUIDs within the
242+ same millisecond increments the timestamp by 1ms for each UUID, causing the
243+ embedded timestamp to drift ahead of real time under high throughput.
244+ Using `precision: :nanosecond` reduces this drift significantly, as
245+ timestamps only advance by 244ns per UUID when generation outpaces real
246+ time. When monotonic UUIDs are desired, it is recommended to also use
247+ `precision: :nanosecond`.
248+
249+ ## Examples
250+
251+ > Ecto.UUID.generate()
252+ "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
253+
254+ > Ecto.UUID.generate(version: 7)
255+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
256+
257+ > Ecto.UUID.generate(version: 7, precision: :nanosecond)
258+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
259+
260+ > Ecto.UUID.generate(version: 7, monotonic: true)
261+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
262+
210263 """
211264 @ spec generate ( ) :: t
212265 @ spec generate ( options ) :: t
213266 def generate ( opts \\ [ ] ) , do: encode ( bingenerate ( opts ) )
214267
215268 @ doc """
216269 Generates a uuid with the given options in binary format.
270+ See `generate/1` for details and available options.
217271 """
218272 @ spec bingenerate ( options ) :: raw
219273 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 ) } "
274+ case Keyword . pop ( opts , :version , @ default_version ) do
275+ { 4 , [ ] } -> bingenerate_v4 ( )
276+ { 7 , opts } -> bingenerate_v7 ( opts )
277+ { 4 , opts } -> raise ArgumentError , "unsupported options for v4: #{ inspect ( opts ) } "
278+ { version , _ } -> raise ArgumentError , "unsupported UUID version: #{ inspect ( version ) } "
224279 end
225280 end
226281
227282 defp bingenerate_v4 do
228283 << u0 :: 48 , _ :: 4 , u1 :: 12 , _ :: 2 , u2 :: 62 >> = :crypto . strong_rand_bytes ( 16 )
229- << u0 :: 48 , 4 :: 4 , u1 :: 12 , 2 :: 2 , u2 :: 62 >>
284+ << u0 :: 48 , @ version_4 :: 4 , u1 :: 12 , @ variant :: 2 , u2 :: 62 >>
230285 end
231286
232- defp bingenerate_v7 do
233- milliseconds = System . system_time ( :millisecond )
234- << u0 :: 12 , u1 :: 62 , _ :: 6 >> = :crypto . strong_rand_bytes ( 10 )
287+ # The bits available for sub-millisecond fractions when using increased clock
288+ # precision based on nanoseconds.
289+ @ ns_sub_ms_bits 12
290+ # The number of values that can be represented in the bit space (2^12).
291+ @ ns_possible_values Bitwise . bsl ( 1 , @ ns_sub_ms_bits )
292+ # The number of nanoseconds in a millisecond.
293+ @ ns_per_ms 1_000_000
294+ # The minimum step when using increased clock precision with fractional
295+ # milliseconds based on nanoseconds.
296+ @ ns_minimal_step div ( @ ns_per_ms , @ ns_possible_values )
297+
298+ defp bingenerate_v7 ( opts ) do
299+ monotonic = Keyword . get ( opts , :monotonic , false )
300+ time_unit = Keyword . get ( opts , :precision , if ( monotonic , do: :nanosecond , else: :millisecond ) )
301+
302+ timestamp =
303+ case monotonic do
304+ true -> next_ascending ( time_unit )
305+ false -> System . system_time ( time_unit )
306+ monotonic -> raise ArgumentError , "invalid monotonic value: #{ inspect ( monotonic ) } "
307+ end
308+
309+ case time_unit do
310+ :millisecond ->
311+ << rand_a :: 12 , _ :: 6 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 10 )
312+ << timestamp :: 48 , @ version_7 :: 4 , rand_a :: 12 , @ variant :: 2 , rand_b :: 62 >>
313+
314+ :nanosecond ->
315+ milliseconds = div ( timestamp , @ ns_per_ms )
316+
317+ clock_precision =
318+ ( rem ( timestamp , @ ns_per_ms ) * @ ns_possible_values ) |> div ( @ ns_per_ms )
319+
320+ << _ :: 2 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 8 )
321+ << milliseconds :: 48 , @ version_7 :: 4 , clock_precision :: 12 , @ variant :: 2 , rand_b :: 62 >>
322+
323+ time_unit ->
324+ raise ArgumentError , "unsupported precision: #{ inspect ( time_unit ) } "
325+ end
326+ end
235327
236- << milliseconds :: 48 , 7 :: 4 , u0 :: 12 , 2 :: 2 , u1 :: 62 >>
328+ defp next_ascending ( time_unit ) when time_unit in [ :millisecond , :nanosecond ] do
329+ timestamp_ref =
330+ with nil <- :persistent_term . get ( { __MODULE__ , time_unit } , nil ) do
331+ :persistent_term . put ( { __MODULE__ , time_unit } , :atomics . new ( 1 , signed: false ) )
332+ :persistent_term . get ( { __MODULE__ , time_unit } )
333+ end
334+
335+ step =
336+ case time_unit do
337+ :millisecond -> 1
338+ :nanosecond -> @ ns_minimal_step
339+ end
340+
341+ previous_ts = :atomics . get ( timestamp_ref , 1 )
342+ min_step_ts = previous_ts + step
343+ current_ts = System . system_time ( time_unit )
344+
345+ # If the current timestamp is not at least the minimal step greater than the
346+ # previous step, then we make it so.
347+ new_ts =
348+ if current_ts > min_step_ts do
349+ current_ts
350+ else
351+ min_step_ts
352+ end
353+
354+ compare_exchange ( timestamp_ref , previous_ts , new_ts , step )
355+ end
356+
357+ defp compare_exchange ( timestamp_ref , previous_ts , new_ts , step ) do
358+ case :atomics . compare_exchange ( timestamp_ref , 1 , previous_ts , new_ts ) do
359+ # If the new value was written, then we return it.
360+ :ok -> new_ts
361+ # If the atomic value has changed in the meantime, we add the minimal step
362+ # value to that and try again.
363+ updated_ts -> compare_exchange ( timestamp_ref , updated_ts , updated_ts + step , step )
364+ end
237365 end
238366
239367 # Callback invoked by autogenerate fields.
0 commit comments