Ecto v2.0.0-rc.1 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
  @behaviour Ecto.Type
  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 databases are strict) 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.Schema

  @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.

Ecto types also allow developers to dump and load new types. In order for this to work, callbacks should take care of encoding your custom Ecto type into its DB representation, as well as decoding it from the DB back into the Ecto type. Each callback should behave as follows:

  • type should output the name of the DB type
  • cast should receive any type and output your custom Ecto type
  • load should receive the DB type and output your custom Ecto type
  • dump should receive your custom Ecto type and output the DB type

Ecto.DateTime is an example of a custom type. Developers often use Ecto.DateTime in their schemas and Ecto takes care of converting between types whenever the schema information is available. Developers may also implement Ecto.DataType for Ecto.DateTime, allowing Ecto.DateTime to behave as the database :datetime even in the absence of schema information.

Summary

Types

Custom types are represented by user-defined modules

Primitive Ecto types (handled by Ecto)

t()

An Ecto type, primitive or custom

Functions

Checks if the given atom can be used as base type

Casts a value to the given type

Checks if the given atom can be used as composite type

Dumps a value to the given type

Loads a value with the given type

Checks if a given type matches with a primitive type that can be found in queries

Checks if we have a primitive type

Retrieves the underlying schema type for the given, possibly custom, type

Callbacks

Casts the given input to the custom type

Dumps the given term into an Ecto native type

Loads the given term into a custom type

Returns the underlying schema type for the custom type

Types

custom :: atom

Custom types are represented by user-defined modules.

primitive :: base | composite

Primitive Ecto types (handled by Ecto).

t :: primitive | custom

An Ecto type, primitive or custom.

Functions

base?(atom)

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
cast(type, value)

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
composite?(atom)

Specs

composite?(atom) :: boolean

Checks if the given atom can be used as composite type.

iex> composite?(:array)
true
iex> composite?(:string)
false
dump(type, value, dumper \\ &dump/2)

Specs

dump(t, term, (t, term -> {:ok, term} | :error)) ::
  {: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}}}

A dumper function may be given for handling recursive types.

load(type, value, loader \\ &load/2)

Specs

load(t, term, (t, term -> {:ok, term} | :error)) ::
  {:ok, term} |
  :error

Loads a value with the given type.

iex> load(:string, nil)
{:ok, nil}
iex> load(:string, "foo")
{:ok, "foo"}

iex> load(:integer, 1)
{:ok, 1}
iex> load(:integer, "10")
:error

A loader function may be given for handling recursive types.

match?(schema_type, query_type)

Specs

match?(t, primitive) :: boolean

Checks if a given type matches with a primitive type that can be found in queries.

iex> match?(:string, :any)
true
iex> match?(:any, :string)
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
primitive?(base)

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
type(type)

Specs

type(t) :: t

Retrieves the underlying schema type for the given, possibly custom, type.

iex> type(:string)
:string
iex> type(Ecto.DateTime)
:datetime

iex> type({:array, :string})
{:array, :string}
iex> type({:array, Ecto.DateTime})
{:array, :datetime}

Callbacks

cast(term)

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:

  1. When casting values by Ecto.Changeset
  2. When passing arguments to Ecto.Query
dump(term)

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.

load(term)

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.

type()

Specs

type :: t

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.

Note this function is not required to return Ecto primitive types, the type is only required to be known by the adapter.