defmodule UUIDv7 do @moduledoc """ A UUID version 7 implementation and `Ecto.Type` for Elixir - based on Rust. This library defers the UUID v7 implementation to the Rust create [UUID](https://crates.io/crates/uuid) using an Erlang NIF. It includes an `Ecto.Type` to (auto-)generate version 7 UUIDs in `Ecto.Schema` and beyond. Thanks to Rust, it is ~72% faster in generating version 7 UUIDs than the Elixir implementation of version 4 UUIDs by Ecto. See the benchmarks for more details. > The underlying Rust library marks the v7 UUID implementation as experimental, so please be aware > that it _could_ change; but you will be notified of that in the [CHANGELOG](https://github.com/martinthenth/uuidv7/blob/main/CHANGELOG.md). (23-06-2023) ## Installation The package can be installed by adding `uuidv7` to your list of dependencies in `mix.exs`: ```elixir def deps do [{:uuidv7, "~> 0.1.0"}] end ``` ## Usage In your database schema, change primary key attribute from `:binary_id` to `UUIDv7`: ```elixir def App.Schemas.User do @primary_key {:id, UUIDv7, autogenerate: true} end ``` ## Benchmark Run `mix deps.get` and then `mix run scripts/bench.exs` to run the benchmark on your computer. ```zsh Operating System: macOS CPU Information: Apple M2 Pro Number of Available Cores: 10 Available memory: 16 GB Elixir 1.14.2 Erlang 25.1.2 Benchmark suite executing with the following configuration: warmup: 5 s time: 10 s memory time: 5 s reduction time: 0 ns parallel: 1 inputs: none specified Estimated total run time: 1 min Benchmarking ecto (uuid v4) ... Benchmarking uniq (uuid v7) ... Benchmarking uuidv7 ... Name ips average deviation median 99th % uuidv7 1.75 M 570.22 ns ±3940.19% 500 ns 667 ns uniq (uuid v7) 1.07 M 937.20 ns ±1852.78% 916 ns 1000 ns ecto (uuid v4) 1.02 M 978.17 ns ±1593.54% 958 ns 1042 ns Comparison: uuidv7 1.75 M uniq (uuid v7) 1.07 M - 1.64x slower +366.98 ns ecto (uuid v4) 1.02 M - 1.72x slower +407.95 ns Memory usage statistics: Name average deviation median 99th % uuidv7 104 B ±0.00% 104 B 104 B uniq (uuid v7) 214.00 B ±2.47% 216 B 216 B ecto (uuid v4) 214.00 B ±2.47% 216 B 216 B Comparison: uuidv7 104 B uniq (uuid v7) 214.00 B - 2.06x memory usage +110.00 B ecto (uuid v4) 214.00 B - 2.06x memory usage +110.00 B ``` ## Credits This library is based on the Rust library [UUID](https://crates.io/crates/uuid). Thank you! The `Ecto.Type` is heavily borrowed from from [Ecto](https://github.com/elixir-ecto/ecto). Thanks! """ version = Mix.Project.config()[:version] use Ecto.Type use RustlerPrecompiled, otp_app: :uuidv7, crate: "uuidv7", base_url: "https://github.com/martinthenth/uuidv7/releases/download/v#{version}", targets: [ "aarch64-unknown-linux-gnu", "aarch64-apple-darwin", "riscv64gc-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl" ], nif_versions: ["2.16"], force_build: System.get_env("FORCE_BUILD") in ["1", "true"], version: version @typedoc """ A hex-encoded UUID string. """ @type t :: <<_::288>> @typedoc """ A raw binary representation of a UUID. """ @type raw :: <<_::128>> @doc false def type, do: :uuid @doc """ Casts to a UUID. """ @spec cast(t() | raw() | any()) :: {:ok, t()} | :error def cast( <> ) do <> 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!(value) do case cast(value) do {:ok, hex_uuid} -> hex_uuid :error -> raise Ecto.CastError, type: __MODULE__, value: value 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(t() | any()) :: {:ok, raw()} | :error def dump( <> ) do <> 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!(value) do case dump(value) do {:ok, raw_uuid} -> raw_uuid :error -> raise ArgumentError, "cannot dump given UUID to binary: #{inspect(value)}" 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 @doc """ Generates a random, version 7 UUID. """ @spec generate() :: raw() def generate, do: :erlang.nif_error(:nif_not_loaded) @doc """ Generates a random, version 7 UUID based on the timestamp (ns). """ def generate(nanoseconds), do: generate_from_ns(nanoseconds) @doc false @spec generate_from_ns(non_neg_integer()) :: raw() def generate_from_ns(_nanoseconds), do: :erlang.nif_error(:nif_not_loaded) @doc false @spec autogenerate() :: binary() def autogenerate, do: generate() @spec encode(raw) :: t defp encode( <> ) do <> 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 end