defmodule Ecto.UUID7 do @moduledoc """ A parameterized Ecto type for UUID version 7 strings. An extension to `Ecto.UUID`. To use the original UUID as your primary key: ```elixir defmodule Doc do use Ecto.Schema @primary_key {:uuid, :binary_id, autogenerate: true} schema "doc" do ... end end ``` To use a tagged version 7 UUID: ```elixir defmodule Doc do use Ecto.Schema alias Ecto.UUID7 schema "doc" do field :id, UUID7, primary_key: true, autogenerate: true, skip_default_validation: true, tag: :0xd0c end end ``` """ use Ecto.ParameterizedType import Bitwise, only: [band: 2] alias Ecto.UUID @type params :: %{ optional(:seq) => [:positive | :monotonic], optional(:tag) => pos_integer() } @seq_opts [:positive, :monotonic] @impl Ecto.ParameterizedType @spec init(Keyword.t()) :: params def init(opts), do: Enum.reduce(opts, %{}, &accumulate_opts/2) @impl Ecto.ParameterizedType def autogenerate(opts, now_ms \\ &now_ms/0) def autogenerate(%{seq: modifiers, tag: tag}, now_ms) do s = modifiers |> System.unique_integer() |> band(0xFFF) t = now_ms.() <<_::48, _::4, _::12, _::2, _::12, b::50>> = :crypto.strong_rand_bytes(16) encode(<>) end def autogenerate(%{seq: modifiers}, now_ms) do s = modifiers |> System.unique_integer() |> band(0xFFF) t = now_ms.() <<_::48, _::4, a::12, _::2, _::12, b::50>> = :crypto.strong_rand_bytes(16) encode(<>) end def autogenerate(%{tag: tag}, now_ms) do t = now_ms.() <<_::48, _::4, _::12, _::2, b::62>> = :crypto.strong_rand_bytes(16) encode(<>) end def autogenerate(%{}, now_ms) do t = now_ms.() <<_::48, _::4, a::12, _::2, b::62>> = :crypto.strong_rand_bytes(16) encode(<>) end @impl Ecto.ParameterizedType def cast(raw_uuid, _params), do: UUID.cast(raw_uuid) @impl Ecto.ParameterizedType def dump(arg1, _dumper, _params), do: UUID.dump(arg1) @impl Ecto.ParameterizedType def load(<<_::128>> = raw_uuid, _, _), do: UUID.load(raw_uuid) def load(_), do: :error @impl Ecto.ParameterizedType def type(_), do: UUID.type() defp encode(raw_uuid) do case UUID.cast(raw_uuid) do {:ok, uuid} -> uuid _ -> :error end end # Verify options at compile time defp accumulate_opts({:seq, true}, acc), do: Map.put(acc, :seq, [:monotonic]) defp accumulate_opts({:seq, [a]}, acc) when a in @seq_opts, do: Map.put(acc, :seq, [a]) defp accumulate_opts({:seq, [a, b]}, acc) when a in @seq_opts and b in @seq_opts, do: Map.put(acc, :seq, [a, b]) defp accumulate_opts({:seq = k, _}, _acc), do: raise(ArgumentError, to_string(k)) defp accumulate_opts({:tag, tag}, acc) when is_integer(tag) and tag >= 0 and tag <= 0xFFF, do: Map.put(acc, :tag, tag) defp accumulate_opts({:tag = k, _}, _acc), do: raise(ArgumentError, to_string(k)) # Millisecond since unix epoch defp now_ms, do: System.system_time(:millisecond) end