Ecto v3.0.0 Ecto.Type behaviour View Source

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.

Note: nil values are always bypassed and cannot be handled by custom types.

Example

Imagine you want to store an URI struct as part of a schema in an url-shortening service. There isn’t an Ecto field type to support that value at runtime, therefore a custom one is needed.

You also want to query not only by the full url, but for example by specific ports used. This is possible by putting the URI data into a map field instead of just storing the plain string representation.

from s in ShortUrl,
  where: fragment("?->>? ILIKE ?", s.original_url, "port", "443")

So the custom type does need to handle the conversion from external data to runtime data (cast/1) as well as transforming that runtime data into the :map Ecto native type and back (dump/1 and load/1).

defmodule EctoURI do
  @behaviour Ecto.Type
  def type, do: :map

  # Provide custom casting rules.
  # Cast strings into the URI struct to be used at runtime
  def cast(uri) when is_binary(uri) do
    {:ok, URI.parse(uri)}
  end

  # Accept casting of URI structs as well
  def cast(%URI{} = uri), do: {:ok, uri}

  # Everything else is a failure though
  def cast(_), do: :error

  # When loading data from the database, we are guaranteed to
  # receive a map (as databases are strict) and we will
  # just put the data back into an URI struct to be stored
  # in the loaded schema struct.
  def load(data) when is_map(data) do
    data =
      for {key, val} <- data do
        {String.to_existing_atom(key), val}
      end
    {:ok, struct!(URI, data)}
  end

  # When dumping data to the database, we *expect* an URI struct
  # but any value could be inserted into the schema struct at runtime,
  # so we need to guard against them.
  def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
  def dump(_), do: :error
end

Now we can use our new field type above in our schemas:

defmodule ShortUrl do
  use Ecto.Schema

  schema "posts" do
    field :original_url, EctoURI
  end
end

Link to this section 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

Dumps a value to the given type

Checks if two terms are equal

Loads a value with 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

Checks if two terms are semantically equal

Loads the given term into a custom type

Returns the underlying schema type for the custom type

Link to this section Types

Custom types are represented by user-defined modules.

Link to this type primitive() View Source
primitive() :: base() | composite()

Primitive Ecto types (handled by Ecto).

An Ecto type, primitive or custom.

Link to this section Functions

Link to this function adapter_load(adapter, type, value) View Source

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

iex> base?(:string)
true
iex> base?(:array)
false
iex> base?(Custom)
false
Link to this function cast(type, value) View Source
cast(t(), term()) :: {:ok, term()} | {:error, keyword()} | :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.

NaN and infinite decimals are not supported, use custom types instead.

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({: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
Link to this function composite?(atom) View Source
composite?(atom()) :: boolean()

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

iex> composite?(:array)
true
iex> composite?(:string)
false
Link to this function dump(type, value) View Source
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, nil}
iex> dump(:string, "foo")
{:ok, "foo"}

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

iex> dump(:binary, "foo")
{:ok, "foo"}
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, ["1", "2", "3"]}
Link to this function dump(type, value, dumper) View Source
dump(t(), term(), (t(), term() -> {:ok, term()} | :error)) ::
  {:ok, term()} | :error

Dumps a value to the given type.

This function behaves the same as dump/2, except for composite types the given dumper function is used.

Link to this function equal?(type, term1, term2) View Source
equal?(t(), term(), term()) :: boolean()

Checks if two terms are equal.

Depending on the given type performs a structural or semantical comparison.

Examples

iex> equal?(:integer, 1, 1)
true
iex> equal?(:decimal, Decimal.new("1"), Decimal.new("1.00"))
true
Link to this function load(type, value) View Source
load(t(), term()) :: {: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
Link to this function load(type, value, loader) View Source
load(t(), term(), (t(), term() -> {:ok, term()} | :error)) ::
  {:ok, term()} | :error

Loads a value with the given type.

This function behaves the same as load/2, except for composite types the given loader function is used.

Link to this function match?(schema_type, query_type) View Source
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.UUID, :uuid)
true
iex> match?(Ecto.UUID, :string)
false
Link to this function primitive?(base) View Source
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

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

iex> type(:string)
:string
iex> type(Ecto.UUID)
:uuid

iex> type({:array, :string})
{:array, :string}
iex> type({:array, Ecto.UUID})
{:array, :uuid}

iex> type({:map, Ecto.UUID})
{:map, :uuid}

Link to this section Callbacks

Link to this callback cast(term) View Source
cast(term()) :: {:ok, term()} | {:error, keyword()} | :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 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

When returning {:error, keyword()}, the returned keyword list will be preserved in the changeset errors, similar to Changeset.add_error/4. Passing a :message key, will override the default message. It is not possible to override the :type key.

For {:array, CustomType} or {:map, CustomType} the returned keyword list will be erased and the default error will be shown.

Link to this callback dump(term) View Source
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.

Link to this callback equal?(term, term) View Source (optional)
equal?(term(), term()) :: boolean()

Checks if two terms are semantically equal.

Link to this callback load(term) View Source
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.

Returns the underlying schema type for the custom type.

For example, if you want to provide your own date structures, the type function should return :date.

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