Booleans are one of the oldest constructs in programming. They’re simple. Binary. And we use booleans everywhere. One of my earliest software projects had a database table called “users” with several columns used to specify role like is_admin, is_moderator, and is_seller.

%User{is_admin: false, is_moderator: false, is_seller: false}

This gets even worse when booleans are used to represent some sort of state. For example a payment service that stores its state in booleans could potentially have columns like is_authorized, is_captured, is_failed, and is_refunded. That’s just complicated. And that’s just with a single piece of state. Many places support partial refunds, which means now you’ve got another boolean is_partial_refund.

%Payment{
  is_authorized: false,
  is_captured: false,
  is_failed: false,
  is_refunded: false,
  is_partial_refund: false
}

But there’s another issue: now you can have impossible states. For instance if somehow a record has is_partial_refund set to true but is_refunded set to false what does that even mean? Or if a record has is_failed set to true but also is_captured set to true. Make impossible states impossible!

Enums

Enums allow us to do just that. They enable a variable to contain a value from a set of predefined constants. In a statically-typed language, enum values are checked at compile time to eradicate bad values. Elixir is dynamically-typed, so we don’t get that safety guarantee, but that doesn’t demean enums' usefulness.

The user example above is made simpler with the use of an enum:

%User{role: :admin}

The role field would contain one of the following atoms: :admin, :moderator, :seller, :buyer. That simplifies our user struct immensely, but the difference is even bigger with the payment example:

%Payment{status: :pending}

Instead of setting different combinations of five boolean flags, we can change the status to one of :authorized, :captured, :failed, :refunded, or :partially_refunded. And there is no way for the payment to get into one of the impossible states mentioned above. But how does this work with a database?

Rolling your own enums with Strings

The simplest way to store “enums” in a database with Elixir and Ecto has been to roll your own enums. By creating a new varchar column on your table, you can then write the appropriate string to it when creating or updating a record. Ecto.Changeset.validate_inclusion/4 will allow you to check that the string being provided contains one of the supported values so that you’re not inputting bad data.

But that safety only exists at the application layer. It ignores the potential that someone accessing the database directly could input a bad value by bypassing the application. If a bad string gets in and the application doesn’t handle it, that bad string will crash the process. But you shouldn’t have to waste time validating values coming from your data layer. Your application deserves better.

EctoEnum

Postgres and MySQL have the ability to create enum types that can be used for columns and will validate them at the data layer rather than the application layer. This means that your data store would be protected from bad values. For a long time, the EctoEnum library has been the best way to set up custom enum types for Postgres:

# lib/my_app/accounts/user_role.ex
defmodule MyApp.Accounts.UserRole do
  use EctoEnum, type: :user_role, enums: [:admin, :moderator, :seller, :buyer]
end

# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  alias MyApp.Accounts.UserRole

  schema "users" do
    field :role, UserRole
  end
end

# priv/repo/migrations/20210102193646_add_role_to_users.exs
defmodule MyApp.Repo.Migrations.AddRoleToUsers do
  use Ecto.Migration
  alias MyApp.Accounts.UserRole

  def change do
    UserRole.create_type()

    alter table(:users) do
      add :role, :user_role
    end
  end
end

It requires creating a new module for the enum type, but it has some nice conveniences for using the type in your schema and for creating the type in your migrations in the first place. But using one more dependency also means one more thing in the application that has to be maintained.

Ecto 3.5

Ecto 3.5 was released in October 2020, and it included the new Ecto.Enum module. The module’s documentation expects the Enum type to be a string when stored in the database, but we can also make it work with a Postgres enum. This means you can now use enums without needing another library. Ecto SQL still doesn’t have convenience functions for creating enums in migrations (yet) so you’ll have to drop down to raw SQL and do that manually:

defmodule MyApp.Repo.Migrations.AddRoleToUsers do
  use Ecto.Migration

  def change do
    create_query = "CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'seller', 'buyer')"
    drop_query = "DROP TYPE user_role"
    execute(create_query, drop_query)

    alter table(:users) do
      add :role, :user_role
    end
  end
end

Then you can add an enum to your schema without having to create a new module and Ecto type though:

defmodule MyApp.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :role, Ecto.Enum, values: [:admin, :moderator, :seller, :buyer]
  end
end

Just like the EctoEnum library, Ecto handles all of the casting for changesets, and the role is available as an atom rather than a string. This provides some safety because you will be able to more easily differentiate between the unsafe string values provided by users and the safer atoms that have been validated by the application.

What do you think?

Do you like the new Ecto.Enum API, or do you wish the API was different? Are you going to stick to the EctoEnum library or to just using Strings? Let me know on Twitter!

And if you liked this post, hopefully you’ll like the others I write. Use the form below to subscribe to my email newsletter and get a notification whenever I write a new one!

Happy New Year!