@@ -15,6 +15,15 @@ defmodule Ecto.UUID do
1515
1616 use Ecto.Schema
1717 @primary_key {:id, :binary_id, autogenerate: [version: 7]}
18+
19+ To use UUID v7 (time-ordered) monotonic:
20+
21+ use Ecto.Schema
22+ @primary_key {:id, :binary_id, autogenerate: [version: 7, monotonic: true]}
23+
24+ According to [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters):
25+ "Monotonicity (each subsequent value being greater than the last) is the
26+ backbone of time-based sortable UUIDs."
1827 """
1928
2029 use Ecto.Type
@@ -30,9 +39,18 @@ defmodule Ecto.UUID do
3039 @ type raw :: << _ :: 128 >>
3140
3241 @ typedoc """
33- currently supported option is version, it accepts 4 or 7 .
42+ Supported options: `: version`, `:precision` (v7-only), and `:monotonic` (v7-only) .
3443 """
35- @ type options :: [ version: 4 | 7 ]
44+ @ type option ::
45+ { :version , 4 | 7 }
46+ | { :precision , :millisecond | :nanosecond }
47+ | { :monotonic , boolean ( ) }
48+
49+ @ type options :: [ option ]
50+
51+ @ version_4 4
52+ @ version_7 7
53+ @ variant 2
3654
3755 @ doc false
3856 def type , do: :uuid
@@ -206,34 +224,150 @@ defmodule Ecto.UUID do
206224
207225 @ default_version 4
208226 @ doc """
209- Generates a uuid with the given options.
227+ Generates a UUID string.
228+
229+ ## Options
230+
231+ * `:version` - The UUID version to generate. Supported values are `4` (random)
232+ and `7` (time-ordered). Defaults to `4`.
233+
234+ ## Options (version 7)
235+
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`.
254+
255+ ## Examples
256+
257+ > Ecto.UUID.generate()
258+ "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
259+
260+ > Ecto.UUID.generate(version: 7)
261+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
262+
263+ > Ecto.UUID.generate(version: 7, precision: :nanosecond)
264+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
265+
266+ > Ecto.UUID.generate(version: 7, monotonic: true)
267+ "018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
268+
210269 """
211270 @ spec generate ( ) :: t
212271 @ spec generate ( options ) :: t
213272 def generate ( opts \\ [ ] ) , do: encode ( bingenerate ( opts ) )
214273
215274 @ doc """
216275 Generates a uuid with the given options in binary format.
276+ See `generate/1` for details and available options.
217277 """
218278 @ spec bingenerate ( options ) :: raw
219279 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 ) } "
280+ case Keyword . pop ( opts , :version , @ default_version ) do
281+ { 4 , [ ] } -> bingenerate_v4 ( )
282+ { 7 , opts } -> bingenerate_v7 ( opts )
283+ { 4 , opts } -> raise ArgumentError , "unsupported options for v4: #{ inspect ( opts ) } "
284+ { version , _ } -> raise ArgumentError , "unsupported UUID version: #{ inspect ( version ) } "
224285 end
225286 end
226287
227288 defp bingenerate_v4 do
228289 << u0 :: 48 , _ :: 4 , u1 :: 12 , _ :: 2 , u2 :: 62 >> = :crypto . strong_rand_bytes ( 16 )
229- << u0 :: 48 , 4 :: 4 , u1 :: 12 , 2 :: 2 , u2 :: 62 >>
290+ << u0 :: 48 , @ version_4 :: 4 , u1 :: 12 , @ variant :: 2 , u2 :: 62 >>
230291 end
231292
232- defp bingenerate_v7 do
233- milliseconds = System . system_time ( :millisecond )
234- << u0 :: 12 , u1 :: 62 , _ :: 6 >> = :crypto . strong_rand_bytes ( 10 )
293+ # The bits available for sub-millisecond fractions when using increased clock
294+ # precision based on nanoseconds.
295+ @ ns_sub_ms_bits 12
296+ # The number of values that can be represented in the bit space (2^12).
297+ @ ns_possible_values Bitwise . bsl ( 1 , @ ns_sub_ms_bits )
298+ # The number of nanoseconds in a millisecond.
299+ @ ns_per_ms 1_000_000
300+ # The minimum step when using increased clock precision with fractional
301+ # milliseconds based on nanoseconds.
302+ @ ns_minimal_step div ( @ ns_per_ms , @ ns_possible_values )
303+
304+ 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
314+
315+ case time_unit do
316+ :millisecond ->
317+ << rand_a :: 12 , _ :: 6 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 10 )
318+ << timestamp :: 48 , @ version_7 :: 4 , rand_a :: 12 , @ variant :: 2 , rand_b :: 62 >>
319+
320+ :nanosecond ->
321+ milliseconds = div ( timestamp , @ ns_per_ms )
322+
323+ clock_precision =
324+ ( rem ( timestamp , @ ns_per_ms ) * @ ns_possible_values ) |> div ( @ ns_per_ms )
325+
326+ << _ :: 2 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 8 )
327+ << milliseconds :: 48 , @ version_7 :: 4 , clock_precision :: 12 , @ variant :: 2 , rand_b :: 62 >>
328+
329+ time_unit ->
330+ raise ArgumentError , "unsupported precision: #{ inspect ( time_unit ) } "
331+ end
332+ end
333+
334+ defp next_ascending ( time_unit ) when time_unit in [ :millisecond , :nanosecond ] do
335+ timestamp_ref =
336+ with nil <- :persistent_term . get ( { __MODULE__ , time_unit } , nil ) do
337+ :persistent_term . put ( { __MODULE__ , time_unit } , :atomics . new ( 1 , signed: false ) )
338+ :persistent_term . get ( { __MODULE__ , time_unit } )
339+ end
340+
341+ step =
342+ case time_unit do
343+ :millisecond -> 1
344+ :nanosecond -> @ ns_minimal_step
345+ end
346+
347+ previous_ts = :atomics . get ( timestamp_ref , 1 )
348+ min_step_ts = previous_ts + step
349+ current_ts = System . system_time ( time_unit )
350+
351+ # If the current timestamp is not at least the minimal step greater than the
352+ # previous step, then we make it so.
353+ new_ts =
354+ if current_ts > min_step_ts do
355+ current_ts
356+ else
357+ min_step_ts
358+ end
359+
360+ compare_exchange ( timestamp_ref , previous_ts , new_ts , step )
361+ end
235362
236- << milliseconds :: 48 , 7 :: 4 , u0 :: 12 , 2 :: 2 , u1 :: 62 >>
363+ defp compare_exchange ( timestamp_ref , previous_ts , new_ts , step ) do
364+ case :atomics . compare_exchange ( timestamp_ref , 1 , previous_ts , new_ts ) do
365+ # If the new value was written, then we return it.
366+ :ok -> new_ts
367+ # If the atomic value has changed in the meantime, we add the minimal step
368+ # value to that and try again.
369+ updated_ts -> compare_exchange ( timestamp_ref , updated_ts , updated_ts + step , step )
370+ end
237371 end
238372
239373 # Callback invoked by autogenerate fields.
0 commit comments