Current section

Files

Jump to
ecto lib ecto uuid.ex
Raw

lib/ecto/uuid.ex

defmodule Ecto.UUID do
@moduledoc """
An Ecto type for UUID strings.
## Autogeneration
This type can be used for any UUID field in your schemas.
It is used when autogenerating binary IDs in Ecto.Schema.
By default, autogenerated UUIDs use version 4 (random):
use Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true}
To use UUID v7 (time-ordered) instead:
use Ecto.Schema
@primary_key {:id, Ecto.UUID, autogenerate: [version: 7]}
To use UUID v7 (time-ordered) monotonic:
use Ecto.Schema
@primary_key {:id, Ecto.UUID, autogenerate: [version: 7, precision: :monotonic]}
According to [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters):
"Monotonicity (each subsequent value being greater than the last) is the
backbone of time-based sortable UUIDs."
"""
use Ecto.Type
@typedoc """
A hex-encoded UUID string.
"""
@type t :: <<_::288>>
@typedoc """
A raw binary representation of a UUID.
"""
@type raw :: <<_::128>>
@typedoc """
Supported options: `:version` and `:precision` (v7-only).
"""
@type option ::
{:version, 4 | 7}
| {:precision, :millisecond | :monotonic}
@type options :: [option]
@version_4 4
@version_7 7
@variant 2
@doc false
def type, do: :uuid
@doc """
Casts either a string in the canonical, human-readable UUID format or a
16-byte binary to a UUID in its canonical, human-readable UUID format.
If `uuid` is neither of these, `:error` will be returned.
Since both binaries and strings are represented as binaries, this means some
strings you may not expect are actually also valid UUIDs in their binary form
and so will be casted into their string form.
If you need further-restricted behavior or validation, you should define your
own custom `Ecto.Type`. There is also `Ecto.UUID.load/1` if you only want to
process `raw` UUIDs, which may be a more suitable reverse operation to
`Ecto.UUID.dump/1`.
## Examples
iex> Ecto.UUID.cast(<<0x60, 0x1D, 0x74, 0xE4, 0xA8, 0xD3, 0x4B, 0x6E,
...> 0x83, 0x65, 0xED, 0xDB, 0x4C, 0x89, 0x33, 0x27>>)
{:ok, "601d74e4-a8d3-4b6e-8365-eddb4c893327"}
iex> Ecto.UUID.cast("601d74e4-a8d3-4b6e-8365-eddb4c893327")
{:ok, "601d74e4-a8d3-4b6e-8365-eddb4c893327"}
iex> Ecto.UUID.cast("warehouse worker")
{:ok, "77617265-686f-7573-6520-776f726b6572"}
"""
@spec cast(t | raw | any) :: {:ok, t} | :error
def cast(uuid)
def cast(
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
) do
<<c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-, c(b1), c(b2), c(b3), c(b4), ?-,
c(c1), c(c2), c(c3), c(c4), ?-, c(d1), c(d2), c(d3), c(d4), ?-, c(e1), c(e2), c(e3), c(e4),
c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12)>>
catch
:error -> :error
else
hex_uuid -> {:ok, hex_uuid}
end
def cast(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)}
def cast(_), do: :error
@doc """
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
"""
@spec cast!(t | raw | any) :: t
def cast!(uuid) do
case cast(uuid) do
{:ok, hex_uuid} -> hex_uuid
:error -> raise Ecto.CastError, type: __MODULE__, value: uuid
end
end
@compile {:inline, c: 1}
defp c(?0), do: ?0
defp c(?1), do: ?1
defp c(?2), do: ?2
defp c(?3), do: ?3
defp c(?4), do: ?4
defp c(?5), do: ?5
defp c(?6), do: ?6
defp c(?7), do: ?7
defp c(?8), do: ?8
defp c(?9), do: ?9
defp c(?A), do: ?a
defp c(?B), do: ?b
defp c(?C), do: ?c
defp c(?D), do: ?d
defp c(?E), do: ?e
defp c(?F), do: ?f
defp c(?a), do: ?a
defp c(?b), do: ?b
defp c(?c), do: ?c
defp c(?d), do: ?d
defp c(?e), do: ?e
defp c(?f), do: ?f
defp c(_), do: throw(:error)
@doc """
Converts a string representing a UUID into a raw binary.
"""
@spec dump(uuid_string :: t | any) :: {:ok, raw} | :error
def dump(uuid_string)
def dump(
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
) do
<<d(a1)::4, d(a2)::4, d(a3)::4, d(a4)::4, d(a5)::4, d(a6)::4, d(a7)::4, d(a8)::4, d(b1)::4,
d(b2)::4, d(b3)::4, d(b4)::4, d(c1)::4, d(c2)::4, d(c3)::4, d(c4)::4, d(d1)::4, d(d2)::4,
d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4, d(e5)::4, d(e6)::4, d(e7)::4,
d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
catch
:error -> :error
else
raw_uuid -> {:ok, raw_uuid}
end
def dump(_), do: :error
@compile {:inline, d: 1}
defp d(?0), do: 0
defp d(?1), do: 1
defp d(?2), do: 2
defp d(?3), do: 3
defp d(?4), do: 4
defp d(?5), do: 5
defp d(?6), do: 6
defp d(?7), do: 7
defp d(?8), do: 8
defp d(?9), do: 9
defp d(?A), do: 10
defp d(?B), do: 11
defp d(?C), do: 12
defp d(?D), do: 13
defp d(?E), do: 14
defp d(?F), do: 15
defp d(?a), do: 10
defp d(?b), do: 11
defp d(?c), do: 12
defp d(?d), do: 13
defp d(?e), do: 14
defp d(?f), do: 15
defp d(_), do: throw(:error)
@doc """
Same as `dump/1` but raises `Ecto.ArgumentError` on invalid arguments.
"""
@spec dump!(t | any) :: raw
def dump!(uuid) do
case dump(uuid) do
{:ok, raw_uuid} -> raw_uuid
:error -> raise ArgumentError, "cannot dump given UUID to binary: #{inspect(uuid)}"
end
end
@doc """
Converts a binary UUID into a string.
"""
@spec load(raw | any) :: {:ok, t} | :error
def load(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)}
def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
raise ArgumentError,
"trying to load string UUID as Ecto.UUID: #{inspect(string)}. " <>
"Maybe you wanted to declare :uuid as your database field?"
end
def load(_), do: :error
@doc """
Same as `load/1` but raises `Ecto.ArgumentError` on invalid arguments.
"""
@spec load!(raw | any) :: t
def load!(value) do
case load(value) do
{:ok, hex_uuid} -> hex_uuid
:error -> raise ArgumentError, "cannot load given binary as UUID: #{inspect(value)}"
end
end
@default_version 4
@doc """
Generates a UUID string.
## Options
* `:version` - The UUID version to generate. Supported values are `4` (random)
and `7` (time-ordered). Defaults to `4`.
## Options (version 7 only)
* `:precision` - The timestamp precision for version 7 UUIDs. Supported
values are `:millisecond` and `:monotonic`. Defaults to `:millisecond`.
> #### Monotonic precision {: .info}
>
> When using `:monotonic`, sub-millisecond precision is encoded in the
> `rand_a` field. The generated version 7 UUIDs are strictly monotonically
> increasing (per node), even when multiple UUIDs are generated within the same
> timestamp. This is useful for maintaining insertion order in databases.
## Examples
> Ecto.UUID.generate()
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
> Ecto.UUID.generate(version: 7)
"018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
> Ecto.UUID.generate(version: 7, precision: :millisecond)
"018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
> Ecto.UUID.generate(version: 7, precision: :monotonic)
"018ec4c1-ae46-7f5a-8f5a-6f5a8f5a6f5a"
"""
@spec generate() :: t
@spec generate(options) :: t
def generate(opts \\ []), do: encode(bingenerate(opts))
@doc """
Generates a uuid with the given options in binary format.
See `generate/1` for details and available options.
"""
@spec bingenerate(options) :: raw
def bingenerate(opts \\ []) do
case Keyword.pop(opts, :version, @default_version) do
{4, []} -> bingenerate_v4()
{7, opts} -> bingenerate_v7(opts)
{4, opts} -> raise ArgumentError, "unsupported options for v4: #{inspect(opts)}"
{version, _} -> raise ArgumentError, "unsupported UUID version: #{inspect(version)}"
end
end
defp bingenerate_v4 do
<<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)
<<u0::48, @version_4::4, u1::12, @variant::2, u2::62>>
end
# The bits available for sub-millisecond fractions when using increased clock
# precision for monotonicity (based on nanoseconds).
@ns_sub_ms_bits 12
# The number of values that can be represented in the bit space (2^12).
@ns_possible_values Bitwise.bsl(1, @ns_sub_ms_bits)
# The number of nanoseconds in a millisecond.
@ns_per_ms 1_000_000
# The minimum step when using increased clock precision with fractional
# milliseconds based on nanoseconds.
@ns_minimal_step div(@ns_per_ms, @ns_possible_values)
defp bingenerate_v7(opts) do
{precision, rest} = Keyword.pop(opts, :precision, :millisecond)
if rest != [], do: raise(ArgumentError, "unsupported options for v7: #{inspect(rest)}")
case precision do
:millisecond ->
timestamp = System.system_time(:millisecond)
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
<<timestamp::48, @version_7::4, rand_a::12, @variant::2, rand_b::62>>
:monotonic ->
timestamp = next_ascending()
milliseconds = div(timestamp, @ns_per_ms)
clock_precision =
(rem(timestamp, @ns_per_ms) * @ns_possible_values) |> div(@ns_per_ms)
<<_::2, rand_b::62>> = :crypto.strong_rand_bytes(8)
<<milliseconds::48, @version_7::4, clock_precision::12, @variant::2, rand_b::62>>
precision ->
raise ArgumentError, "unsupported precision: #{inspect(precision)}"
end
end
defp next_ascending do
timestamp_ref =
:persistent_term.get({__MODULE__, :nanosecond}, nil) || raise "Ecto has not been started"
previous_ts = :atomics.get(timestamp_ref, 1)
min_step_ts = previous_ts + @ns_minimal_step
current_ts = System.system_time(:nanosecond)
# If the current timestamp is not at least the minimal step greater than the
# previous step, then we make it so.
new_ts = max(current_ts, min_step_ts)
compare_exchange(timestamp_ref, previous_ts, new_ts)
end
defp compare_exchange(timestamp_ref, previous_ts, new_ts) do
case :atomics.compare_exchange(timestamp_ref, 1, previous_ts, new_ts) do
# If the new value was written, then we return it.
:ok -> new_ts
# Otherwise, the atomic value has changed in the meantime. We add the
# minimal step value to that and try again.
updated_ts -> compare_exchange(timestamp_ref, updated_ts, updated_ts + @ns_minimal_step)
end
end
# Callback invoked by autogenerate fields.
@doc false
def autogenerate(opts \\ []), do: generate(opts)
@spec encode(raw) :: t
defp encode(
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
) do
<<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,
e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
end
@compile {:inline, e: 1}
defp e(0), do: ?0
defp e(1), do: ?1
defp e(2), do: ?2
defp e(3), do: ?3
defp e(4), do: ?4
defp e(5), do: ?5
defp e(6), do: ?6
defp e(7), do: ?7
defp e(8), do: ?8
defp e(9), do: ?9
defp e(10), do: ?a
defp e(11), do: ?b
defp e(12), do: ?c
defp e(13), do: ?d
defp e(14), do: ?e
defp e(15), do: ?f
@doc """
Converts the timestamp in the UUID to a datetime. Only works for UUID v7.
Raises `ArgumentError` for non-v7 UUIDs.
"""
@spec to_datetime(binary()) :: DateTime.t()
def to_datetime(<<milliseconds::48, @version_7::4, _::76>>),
do: DateTime.from_unix!(milliseconds, :millisecond)
def to_datetime(<<_::48, version::4, _::76>>),
do: raise(ArgumentError, "to_datetime doesn't support UUID v#{version}")
def to_datetime(uuid), do: uuid |> dump!() |> to_datetime()
@doc """
Extracts the unix timestamp from the UUID. Only works for v7 UUIDs.
Raises `ArgumentError` for non-v7 UUIDs.
"""
@spec to_unix(binary(), System.time_unit()) :: integer()
def to_unix(uuid, precision \\ :second)
def to_unix(<<unix::48, @version_7::4, _::76>>, precision),
do: System.convert_time_unit(unix, :millisecond, precision)
def to_unix(<<_::48, version::4, _::76>>, _precision),
do: raise(ArgumentError, "to_unix doesn't support UUID v#{version}")
def to_unix(uuid, precision),
do: uuid |> dump!() |> to_unix(precision)
@doc """
Returns the version number for the UUID.
"""
@spec version(binary()) :: 1..8
def version(<<_::48, version::4, _::76>>), do: version
def version(<<_::288>> = uuid), do: uuid |> dump!() |> version()
end