We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
`Ecto.Enum` maps atoms to integer columns
almirsarajcic
Status columns tend to rot the same way everywhere: an integer in the database, a string in the params, an atom in the business logic, and String.to_atom/1 calls scattered across three contexts to bridge them. Ecto.Enum collapses all of it into the field definition — atoms in Elixir, integers in Postgres, with the conversion and the validation handled for you.
defmodule MyApp.Blog.Article do
use Ecto.Schema
import Ecto.Changeset
schema "articles" do
field :title, :string
field :status, Ecto.Enum, values: [draft: 0, review: 1, published: 2]
end
def changeset(article, attrs) do
article
|> cast(attrs, [:title, :status])
|> validate_required([:title, :status])
end
end
The keyword-list form is the one worth knowing. values: [:draft, :review, :published] stores strings; values: [draft: 0, review: 1, published: 2] stores integers while your code still sees atoms:
changeset = Article.changeset(%Article{}, %{"title" => "Hi", "status" => "published"})
changeset.changes
# => %{status: :published, title: "Hi"}
Ecto.Type.dump(Article.__schema__(:type, :status), :published)
# => {:ok, 2}
A form posts "published", the changeset holds :published, the column stores 2. No conversion code in between.
Validation comes free
An unknown value is rejected by cast/3 itself — you do not need validate_inclusion/3:
changeset = Article.changeset(%Article{}, %{"title" => "Hi", "status" => "archived"})
changeset.valid?
# => false
changeset.errors
# => [status: {"is invalid", ...}]
That check also protects you from the atom-exhaustion problem the naive version has, because no atom is ever created from user input — the value is looked up against the declared set.
The introspection functions
This is the part that pays off in templates and tests:
Ecto.Enum.values(Article, :status)
# => [:draft, :review, :published]
Ecto.Enum.mappings(Article, :status)
# => [draft: 0, review: 1, published: 2]
Ecto.Enum.dump_values(Article, :status)
# => [0, 1, 2]
values/2 builds your select options straight from the schema, so adding a status to the field definition adds it to every form at once:
<.input field={@form[:status]} type="select" options={Ecto.Enum.values(Article, :status)} />
For a translated label per status, keep the atoms as the source of truth and translate at the edge rather than storing display strings in the column.
The migration side stays ordinary — the column is just an integer:
def change do
alter table(:articles) do
add :status, :integer, null: false, default: 0
end
end
One thing to plan for: the integers are a storage contract. Reordering the keyword list silently rewrites the meaning of every existing row, so append new values with new numbers rather than renumbering the existing ones.
copied to clipboard