Ecto.ULID Next is an Ecto.Type
implementation for ULID.
Originally forked from TheRealReal/ecto-ulid, this library is actively maintained to ensure compatibility with both current and upcoming Ecto releases.
- Generate ULIDs in either Base32 or binary format.
- Generate ULIDs based on a specific timestamp.
- Autogenerate ULIDs when used as a primary key in your schema.
- Read and write ULIDs using the database's native
uuid
type—no need for additional database extensions. - Supports Ecto 3.2 and above.
- Supports the officially supported Elixir versions (currently ~> 1.11).
- Tested and confirmed to work on both PostgreSQL and MySQL.
- Optimized for high throughput scenarios.
ULID (Universally Unique Lexicographically Sortable Identifier) is designed for lexicographic sorting, unlike traditional UUIDs. Its timestamp-based prefix enables natural chronological sorting and efficient range queries, making ULID particularly suitable for time-series data, event sourcing, and other use cases where order and time are important factors.
Note
As of May 2024, UUIDv7 has been proposed as a standard (RFC 9562). UUIDv7 also offers lexicographic sorting and may be preferred for new applications, given its broader adoption and standardized nature.
ULID is binary-compatible with UUID, which means Ecto.ULID
can be used
interchangeably in environments where Ecto.UUID
is supported. It has been
confirmed to work with PostgreSQL and MySQL with the uuid
column type.
Ecto.ULID
is optimized for quick operations, particularly useful in scenarios
requiring the handling of a large volume of events. The library borrows
techniques from Ecto.UUID
to achieve sub-microsecond times for most
operations.
A benchmark suite is included to test the performance. Clone the repository and
run mix run bench/ulid_bench.exs
to gauge how the library performs on your own
system.
To install ecto_ulid_next
, add it to your project's dependencies in mix.exs
:
defp deps do
[
{:ecto_ulid_next, "~> 1.0.2"}
]
end
The following sections outline three practical examples of how to use
Ecto.ULID
. For API details, refer to the
documentation.
In your migration file, specify :binary_id
as the column type for the
primary key and any foreign keys.
create table(:events, primary_key: false) do
add :id, :binary_id, primary_key: true
add :user_id, references(:users, type: :binary_id)
# additional fields
end
In your schema, set both @primary_key
and @foreign_key_type
to Ecto.ULID
.
This will default both the primary key and all foreign keys in the schema to use
the ULID type.
defmodule MyApp.Event do
use Ecto.Schema
@primary_key {:id, Ecto.ULID, autogenerate: true}
@foreign_key_type Ecto.ULID
schema "events" do
belongs_to :user, MyApp.User
# additional fields
end
end
To specify the ULID type for an individual association within the current
schema, you can set the type
option in the belongs_to
function:
defmodule MyApp.Event do
use Ecto.Schema
@primary_key {:id, Ecto.ULID, autogenerate: true}
schema "events" do
belongs_to :user, MyApp.User, type: Ecto.ULID
# additional fields
end
end
First, set a global configuration for all migrations to use :binary_id
as the
primary key type in config/config.exs
.
config :my_app, MyApp.Repo,
migration_primary_key: [name: :id, type: :binary_id],
migration_foreign_key: [type: :binary_id]
With this configuration, there's no need to explicitly define the primary key column and the foreign key column type.
create table(:events) do
belongs_to :user, MyApp.User
# additional fields
end
Create a shared module to house general schema configurations. This module will
set Ecto.ULID
as the default for both primary and foreign keys.
defmodule MyApp.Schema do
@moduledoc false
defmacro __using__(_) do
quote do
use Ecto.Schema
@primary_key {:id, Ecto.ULID, autogenerate: true}
@foreign_key_type Ecto.ULID
end
end
end
When defining individual schemas, simply use MyApp.Schema
instead of
Ecto.Schema
. This eliminates the need to specify these attributes in each
schema.
defmodule MyApp.Event do
use MyApp.Schema
schema "events" do
# additional fields
end
end
Use Ecto.ULID.generate/0
or Ecto.ULID.bingenerate/0
to create ULIDs in
string or binary format based on the current system time. This is useful for
sending ULIDs to external systems or for use with c:Ecto.Repo.insert_all/3
,
where autogenerated fields are not set automatically.
iex> Ecto.ULID.generate()
"01BZ13RV29T5S8HV45EDNC748P"
iex> Ecto.ULID.bingenerate()
<<1, 95, 194, 60, 108, 73, 209, 114, 136, 236, 133, 115, 106, 195, 145, 22>>
To generate a ULID based on a specific timestamp—useful for backfilling
historical data—you can use Ecto.ULID.generate/1
or Ecto.ULID.bingenerate/1
.
iex> Ecto.ULID.generate(1402899630000)
"018THNB1XGQZ7T929PD126SM3Z"
iex> Ecto.ULID.bingenerate(1402899630000)
<<1, 70, 163, 85, 135, 176, 12, 219, 112, 32, 157, 209, 98, 152, 55, 37>>
ULIDs are not natively supported by most databases, which means querying data directly may display IDs in UUID format rather than the Crockford Base32 representation. For PostgreSQL, the following tool can help convert between ULID and UUID formats: