Ecto.Type behaviour
Defines functions and the Ecto.Type
behaviour for implementing
custom types.
A custom type expects 4 functions to be implemented, all documented and described below. We also provide two examples of how custom types can be used in Ecto to augment existing types or providing your own types.
Augmenting types
Imagine you want to support your id field to be looked up as a permalink. For example, you want the following query to work:
permalink = "10-how-to-be-productive-with-elixir"
from p in Post, where: p.id == ^permalink
If id
is an integer field, Ecto will fail in the query above
because it cannot cast the string to an integer. By using a
custom type, we can provide special casting behaviour while
still keeping the underlying Ecto type the same:
defmodule Permalink do
def type, do: :integer
# Provide our own casting rules.
def cast(string) when is_binary(string) do
case Integer.parse(string) do
{int, _} -> {:ok, int}
:error -> :error
end
end
# We should still accept integers
def cast(integer) when is_integer(integer), do: {:ok, integer}
# Everything else is a failure though
def cast(_), do: :error
# When loading data from the database, we are guaranteed to
# receive an integer (as database are stricts) and we will
# just return it to be stored in the model struct.
def load(integer) when is_integer(integer), do: {:ok, integer}
# When dumping data to the database, we *expect* an integer
# but any value could be inserted into the struct, so we need
# guard against them.
def dump(integer) when is_integer(integer), do: {:ok, integer}
def dump(_), do: :error
end
Now, we can use our new field above as our primary key type in models:
defmodule Post do
use Ecto.Model
@primary_key {:id, Permalink, autogenerate: true}
schema "posts" do
...
end
end
New types
In the previous example, we say we were augmenting an existing type because we were keeping the underlying representation the same, the value stored in the struct and the database was always an integer.
However, sometimes, we want to completely replace Ecto data types
stored in the models. This is for example how Ecto provides the
Ecto.DateTime
struct as a replacement for the :datetime
type.
Check the Ecto.DateTime
implementation for an example on how
to implement such types.
Summary↑
base?(atom) | Checks if the given atom can be used as base type |
cast!(type, term) | Same as |
cast(type, term) | Casts a value to the given type |
composite?(atom) | Checks if the given atom can be used as composite type |
dump!(type, term) | Same as |
dump(type, value) | Dumps a value to the given type |
load!(type, term) | Same as |
load(type, value) | Loads a value with the given type |
match?(schema_type, query_type) | Checks if a given type matches with a primitive type that can be found in queries |
normalize(type, arg2) | Normalizes a type |
primitive?(base) | Checks if we have a primitive type |
type(type) | Retrieves the underlying type of a given type |
Types ↑
primitive :: base | composite
custom :: atom
Functions
Specs:
- base?(atom) :: boolean
Checks if the given atom can be used as base type.
iex> base?(:string)
true
iex> base?(:array)
false
iex> base?(Custom)
false
Specs:
- cast(t, term) :: {:ok, term} | :error
Casts a value to the given type.
cast/2
is used by the finder queries and changesets
to cast outside values to specific types.
Note that nil can be cast to all primitive types as data stores allow nil to be set on any column. Custom data types may want to handle nil specially though.
iex> cast(:any, "whatever")
{:ok, "whatever"}
iex> cast(:any, nil)
{:ok, nil}
iex> cast(:string, nil)
{:ok, nil}
iex> cast(:integer, 1)
{:ok, 1}
iex> cast(:integer, "1")
{:ok, 1}
iex> cast(:integer, "1.0")
:error
iex> cast(:id, 1)
{:ok, 1}
iex> cast(:id, "1")
{:ok, 1}
iex> cast(:id, "1.0")
:error
iex> cast(:float, 1.0)
{:ok, 1.0}
iex> cast(:float, 1)
{:ok, 1.0}
iex> cast(:float, "1")
{:ok, 1.0}
iex> cast(:float, "1.0")
{:ok, 1.0}
iex> cast(:float, "1-foo")
:error
iex> cast(:boolean, true)
{:ok, true}
iex> cast(:boolean, false)
{:ok, false}
iex> cast(:boolean, "1")
{:ok, true}
iex> cast(:boolean, "0")
{:ok, false}
iex> cast(:boolean, "whatever")
:error
iex> cast(:string, "beef")
{:ok, "beef"}
iex> cast(:binary, "beef")
{:ok, "beef"}
iex> cast(:decimal, Decimal.new(1.0))
{:ok, Decimal.new(1.0)}
iex> cast(:decimal, Decimal.new("1.0"))
{:ok, Decimal.new(1.0)}
iex> cast({:array, :integer}, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> cast({:array, :integer}, ["1", "2", "3"])
{:ok, [1, 2, 3]}
iex> cast({:array, :string}, [1, 2, 3])
:error
iex> cast(:string, [1, 2, 3])
:error
Specs:
- cast!(t, term) :: term | no_return
Same as cast/2
but raises if value can’t be cast.
Specs:
- composite?(atom) :: boolean
Checks if the given atom can be used as composite type.
iex> composite?(:array)
true
iex> composite?(:string)
false
Specs:
- dump(t, term) :: {:ok, term} | :error
Dumps a value to the given type.
Opposite to casting, dumping requires the returned value to be a valid Ecto type, as it will be sent to the underlying data store.
iex> dump(:string, nil)
{:ok, %Ecto.Query.Tagged{value: nil, type: :string}}
iex> dump(:string, "foo")
{:ok, "foo"}
iex> dump(:integer, 1)
{:ok, 1}
iex> dump(:integer, "10")
:error
iex> dump(:binary, "foo")
{:ok, %Ecto.Query.Tagged{value: "foo", type: :binary}}
iex> dump(:binary, 1)
:error
iex> dump({:array, :integer}, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> dump({:array, :integer}, [1, "2", 3])
:error
iex> dump({:array, :binary}, ["1", "2", "3"])
{:ok, %Ecto.Query.Tagged{value: ["1", "2", "3"], type: {:array, :binary}}}
Specs:
- dump!(t, term) :: term | no_return
Same as dump/2
but raises if value can’t be dumped.
Specs:
- load(t, term) :: {:ok, term} | :error
Loads a value with the given type.
Load is invoked when loading database native types into a struct.
iex> load(:string, nil)
{:ok, nil}
iex> load(:string, "foo")
{:ok, "foo"}
iex> load(:integer, 1)
{:ok, 1}
iex> load(:integer, "10")
:error
Specs:
- load!(t, term) :: term | no_return
Same as load/2
but raises if value can’t be loaded.
Specs:
Checks if a given type matches with a primitive type that can be found in queries.
iex> match?(:whatever, :any)
true
iex> match?(:any, :whatever)
true
iex> match?(:string, :string)
true
iex> match?({:array, :string}, {:array, :any})
true
iex> match?(Ecto.DateTime, :datetime)
true
iex> match?(Ecto.DateTime, :string)
false
Normalizes a type.
The only type normalizable is binary_id which comes from the adapter.
Specs:
- primitive?(t) :: boolean
Checks if we have a primitive type.
iex> primitive?(:string)
true
iex> primitive?(Another)
false
iex> primitive?({:array, :string})
true
iex> primitive?({:array, Another})
true
Specs:
Retrieves the underlying type of a given type.
iex> type(:string)
:string
iex> type(Ecto.DateTime)
:datetime
iex> type({:array, :string})
{:array, :string}
iex> type({:array, Ecto.DateTime})
{:array, :datetime}
Callbacks
Specs:
- cast(term) :: {:ok, term} | :error
Casts the given input to the custom type.
This callback is called on external input and can return any type,
as long as the dump/1
function is able to convert the returned
value back into an Ecto native type. There are two situations where
this callback is called:
- When casting values by
Ecto.Changeset
- When passing arguments to
Ecto.Query
Specs:
- dump(term) :: {:ok, term} | :error
Dumps the given term into an Ecto native type.
This callback is called with any term that was stored in the struct and it needs to validate them and convert it to an Ecto native type.
Specs:
- load(term) :: {:ok, term} | :error
Loads the given term into a custom type.
This callback is called when loading data from the database and
receive an Ecto native type. It can return any type, as long as
the dump/1
function is able to convert the returned value back
into an Ecto native type.
Specs:
- type :: base | custom
Returns the underlying schema type for the custom type.
For example, if you want to provide your own datetime
structures, the type function should return :datetime
.