Packages
ecto
1.1.9
3.14.1
3.14.0
3.13.6
3.13.5
3.13.4
3.13.3
3.13.2
3.13.1
3.13.0
3.12.6
3.12.5
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.2
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.4
3.8.3
3.8.2
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.1
3.0.0-rc.0
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.2.0-rc.1
2.2.0-rc.0
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.1.0-rc.5
2.1.0-rc.4
2.1.0-rc.3
2.1.0-rc.2
2.1.0-rc.1
2.1.0-rc.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-beta.2
2.0.0-beta.1
2.0.0-beta.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.12.0-rc
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
A toolkit for data mapping and language integrated query for Elixir
Current section
Files
Jump to
Current section
Files
lib/ecto/association.ex
import Ecto.Query, only: [from: 2, join: 4, distinct: 3]
defmodule Ecto.Association.NotLoaded do
@moduledoc """
Struct returned by one to one associations when they are not loaded.
The fields are:
* `__field__` - the association field in `owner`
* `__owner__` - the model that owns the association
* `__cardinality__` - the cardinality of the association
"""
defstruct [:__field__, :__owner__, :__cardinality__]
defimpl Inspect do
def inspect(not_loaded, _opts) do
msg = "association #{inspect not_loaded.__field__} is not loaded"
~s(#Ecto.Association.NotLoaded<#{msg}>)
end
end
end
defmodule Ecto.Association do
@moduledoc false
@type t :: %{__struct__: atom, cardinality: :one | :many,
field: atom, owner_key: atom, owner: atom}
use Behaviour
@doc """
Builds the association struct.
The struct must be defined in the module that implements the
callback and it must contain at least the following keys:
* `:cardinality` - tells if the association is one to one
or one/many to many
* `:field` - tells the field in the owner struct where the
association should be stored
* `:owner` - the owner module of the association
* `:owner_key` - the key in the owner with the association value
"""
defcallback struct(module, field :: atom, opts :: Keyword.t) :: t
@doc """
Builds a model for the given association.
The struct to build from is given as argument in case default values
should be set in the struct.
Invoked by `Ecto.build_assoc/3`.
"""
defcallback build(t, Ecto.Schema.t, %{atom => term} | [Keyword.t]) :: Ecto.Schema.t
@doc """
Returns an association join query.
This callback receives the association struct and it must return
a query that retrieves all associated entries using joins up to
the owner association.
For example, a `has_many :comments` inside a `Post` module would
return:
from c in Comment, join: p in Post, on: c.post_id == p.id
Note all the logic must be expressed inside joins, as fields like
`where` and `order_by` won't be used by the caller.
This callback is invoked when `join: assoc(p, :comments)` is used
inside queries.
"""
defcallback joins_query(t) :: Ecto.Query.t
@doc """
Returns the association query.
This callback receives the association struct and it must return
a query that retrieves all associated entries with the given
values for the owner key.
This callback is used by `Ecto.assoc/2`.
"""
defcallback assoc_query(t, values :: [term]) :: Ecto.Query.t
@doc """
Returns the association query on top of the given query.
This callback receives the association struct and it must return
a query that retrieves all associated entries with the given
values for the owner key.
This callback is used by preloading.
"""
defcallback assoc_query(t, Ecto.Query.t, values :: [term]) :: Ecto.Query.t
@doc """
Returns information used by the preloader.
"""
defcallback preload_info(t) ::
{:assoc, t, atom} | {:through, t, [atom]}
@doc """
Retrieves the association from the given model.
"""
def association_from_model!(model, assoc) do
model.__schema__(:association, assoc) ||
raise ArgumentError, "model #{inspect model} does not have association #{inspect assoc}"
end
@doc """
Returns the association key for the given module with the given suffix.
## Examples
iex> Ecto.Association.association_key(Hello.World, :id)
:world_id
iex> Ecto.Association.association_key(Hello.HTTP, :id)
:http_id
iex> Ecto.Association.association_key(Hello.HTTPServer, :id)
:http_server_id
"""
def association_key(module, suffix) do
prefix = module |> Module.split |> List.last |> underscore
:"#{prefix}_#{suffix}"
end
# TODO: Use Elixir's underscore function on from Elixir v1.2 onwards
defp underscore(""), do: ""
defp underscore(<<h, t :: binary>>) do
<<to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<h, t, rest :: binary>>, _) when h in ?A..?Z and not t in ?A..?Z do
<<?_, to_lower_char(h), t>> <> do_underscore(rest, t)
end
defp do_underscore(<<h, t :: binary>>, prev) when h in ?A..?Z and not prev in ?A..?Z do
<<?_, to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<?-, t :: binary>>, _) do
<<?_>> <> do_underscore(t, ?-)
end
defp do_underscore(<< "..", t :: binary>>, _) do
<<"..">> <> underscore(t)
end
defp do_underscore(<<?.>>, _), do: <<?.>>
defp do_underscore(<<?., t :: binary>>, _) do
<<?/>> <> underscore(t)
end
defp do_underscore(<<h, t :: binary>>, _) do
<<to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<>>, _) do
<<>>
end
defp to_lower_char(char) when char in ?A..?Z, do: char + 32
defp to_lower_char(char), do: char
@doc """
Retrieves related module from queryable.
## Examples
iex> Ecto.Association.related_from_query({"custom_source", Model})
Model
iex> Ecto.Association.related_from_query(Model)
Model
iex> Ecto.Association.related_from_query("wrong")
** (ArgumentError) association queryable must be a model or {source, model}, got: "wrong"
"""
def related_from_query(atom) when is_atom(atom), do: atom
def related_from_query({source, model}) when is_binary(source) and is_atom(model), do: model
def related_from_query(queryable) do
raise ArgumentError, "association queryable must be a model " <>
"or {source, model}, got: #{inspect queryable}"
end
@doc """
Merges source from query into to the given model.
In case the query does not have a source, returns
the model unchanged.
"""
def merge_source(model, query)
def merge_source(struct, {source, _}) do
Ecto.put_meta(struct, source: source)
end
def merge_source(struct, _query) do
struct
end
end
defmodule Ecto.Association.Has do
@moduledoc """
The association struct for `has_one` and `has_many` associations.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the model
* `owner` - The model where the association was defined
* `related` - The model that is associated
* `owner_key` - The key on the `owner` model used for the association
* `related_key` - The key on the `related` model used for the association
* `queryable` - The real query to use for querying association
* `on_delete` - The action taken on associations when model is deleted
* `on_replace` - The action taken on associations when model is replaced
* `on_cast` - The changeset function to call during casting
* `defaults` - Default fields used when building the association
"""
@behaviour Ecto.Association
@on_delete_opts [:nothing, :fetch_and_delete, :nilify_all, :delete_all]
@on_replace_opts [:raise, :mark_as_invalid, :delete, :nilify]
defstruct [:cardinality, :field, :owner, :related, :owner_key, :related_key,
:queryable, :on_delete, :on_replace, on_cast: :changeset, defaults: []]
@doc false
def struct(module, name, opts) do
ref =
cond do
ref = opts[:references] ->
ref
primary_key = Module.get_attribute(module, :primary_key) ->
elem(primary_key, 0)
true ->
raise ArgumentError, "need to set :references option for " <>
"association #{inspect name} when model has no primary key"
end
unless Module.get_attribute(module, :ecto_fields)[ref] do
raise ArgumentError, "model does not have the field #{inspect ref} used by " <>
"association #{inspect name}, please set the :references option accordingly"
end
queryable = Keyword.fetch!(opts, :queryable)
related = Ecto.Association.related_from_query(queryable)
if opts[:through] do
raise ArgumentError, "invalid association #{inspect name}. When using the :through " <>
"option, the model should not be passed as second argument"
end
on_delete = Keyword.get(opts, :on_delete, :nothing)
on_replace = Keyword.get(opts, :on_replace, :raise)
on_cast = Keyword.get(opts, :on_cast, :changeset)
unless on_delete in @on_delete_opts do
raise ArgumentError, "invalid :on_delete option for #{inspect name}. " <>
"The only valid options are: " <>
Enum.map_join(@on_delete_opts, ", ", &"`#{inspect &1}`")
end
unless on_replace in @on_replace_opts do
raise ArgumentError, "invalid `:on_replace` option for #{inspect name}. " <>
"The only valid options are: " <>
Enum.map_join(@on_replace_opts, ", ", &"`#{inspect &1}`")
end
%__MODULE__{
field: name,
cardinality: Keyword.fetch!(opts, :cardinality),
owner: module,
related: related,
owner_key: ref,
related_key: opts[:foreign_key] || Ecto.Association.association_key(module, ref),
queryable: queryable,
on_delete: on_delete,
on_replace: on_replace,
on_cast: on_cast,
defaults: opts[:defaults] || []
}
end
@doc false
def build(%{owner_key: owner_key, related_key: related_key} = refl, struct, attributes) do
refl
|> build()
|> struct(attributes)
|> Map.put(related_key, Map.get(struct, owner_key))
end
@doc false
def joins_query(refl) do
from o in refl.owner,
join: q in ^refl.queryable,
on: field(q, ^refl.related_key) == field(o, ^refl.owner_key)
end
@doc false
def assoc_query(refl, values) do
assoc_query(refl, refl.queryable, values)
end
@doc false
def assoc_query(refl, query, values) do
from x in query,
where: field(x, ^refl.related_key) in ^values
end
@doc false
def preload_info(refl) do
{:assoc, refl, refl.related_key}
end
@behaviour Ecto.Changeset.Relation
@doc false
def build(%{related: related, queryable: queryable, defaults: defaults}) do
related
|> struct(defaults)
|> Ecto.Association.merge_source(queryable)
end
@doc false
def on_replace(%{on_replace: :delete}, changeset) do
{:delete, changeset}
end
def on_replace(%{on_replace: :nilify, related_key: related_key}, changeset) do
changeset = update_in changeset.changes, &Map.put(&1, related_key, nil)
{:update, changeset}
end
@doc false
def on_repo_action(assoc, changeset, parent, _adapter, repo, repo_action, opts) do
%{action: action, changes: changes} = changeset
check_action!(action, repo_action, assoc)
{key, value} = parent_key(assoc, parent)
changeset = update_parent_key(changeset, action, key, value)
case apply(repo, action, [changeset, opts]) do
{:ok, _} = ok ->
maybe_replace_one!(assoc, changeset, parent, repo, opts)
if action == :delete, do: {:ok, nil}, else: ok
{:error, changeset} ->
original = Map.get(changes, key)
{:error, update_in(changeset.changes, &Map.put(&1, key, original))}
end
end
defp update_parent_key(changeset, :delete, _key, _value),
do: changeset
defp update_parent_key(changeset, _action, key, value),
do: Ecto.Changeset.put_change(changeset, key, value)
defp parent_key(%{owner_key: owner_key, related_key: related_key}, owner) do
{related_key, Map.get(owner, owner_key)}
end
defp check_action!(:delete, :insert, %{related: model}),
do: raise(ArgumentError, "got action :delete in changeset for associated #{inspect model} while inserting")
defp check_action!(_, _, _), do: :ok
defp maybe_replace_one!(%{cardinality: :one, field: field} = assoc,
%{action: :insert}, parent, repo, opts) do
case Map.get(parent, field) do
%Ecto.Association.NotLoaded{} ->
:ok
nil ->
:ok
previous ->
# the case when this could return :error, was handled before
{:ok, changeset} = Ecto.Changeset.Relation.on_replace(assoc, previous)
case apply(repo, changeset.action, [changeset, opts]) do
{:ok, _} ->
:ok
{:error, changeset} ->
raise Ecto.InvalidChangesetError,
action: changeset.action, changeset: changeset
end
end
end
defp maybe_replace_one!(_, _, _, _, _), do: :ok
end
defmodule Ecto.Association.HasThrough do
@moduledoc """
The association struct for `has_one` and `has_many` through associations.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the model
* `owner` - The model where the association was defined
* `owner_key` - The key on the `owner` model used for the association
* `through` - The through associations
"""
alias Ecto.Query.JoinExpr
@behaviour Ecto.Association
defstruct [:cardinality, :field, :owner, :owner_key, :through]
@doc false
def struct(module, name, opts) do
through = Keyword.fetch!(opts, :through)
refl =
case through do
[h,_|_] ->
Module.get_attribute(module, :ecto_assocs)[h]
_ ->
raise ArgumentError, ":through expects a list with at least two entries: " <>
"the association in the current module and one step through, got: #{inspect through}"
end
unless refl do
raise ArgumentError, "model does not have the association #{inspect hd(through)} " <>
"used by association #{inspect name}, please ensure the association exists and " <>
"is defined before the :through one"
end
%__MODULE__{
field: name,
cardinality: Keyword.fetch!(opts, :cardinality),
through: through,
owner: module,
owner_key: refl.owner_key,
}
end
@doc false
def build(%{field: name}, %{__struct__: struct}, _attributes) do
raise ArgumentError,
"cannot build through association #{inspect name} for #{inspect struct}. " <>
"Instead build the intermediate steps explicitly."
end
@doc false
def preload_info(refl) do
{:through, refl, refl.through}
end
@doc false
def joins_query(%{owner: owner, through: through}) do
joins_query(owner, through, 0)
end
defp joins_query(query, through, counter) do
Enum.reduce(through, {query, counter}, fn current, {acc, counter} ->
{join(acc, :inner, [x: counter], assoc(x, ^current)), counter + 1}
end) |> elem(0)
end
@doc false
def assoc_query(refl, values) do
assoc_query(refl, %Ecto.Query{from: {"join expression", nil}}, values)
end
@doc false
def assoc_query(%{owner: owner, through: [h|t]}, %Ecto.Query{} = query, values) do
refl = owner.__schema__(:association, h)
# Find the position for upcoming joins
position = length(query.joins) + 1
# The first association must become a join,
# so we convert its where (that comes from assoc_query)
# to a join expression.
#
# Note we are being restrictive on the format
# expected from assoc_query.
join = assoc_to_join(refl.__struct__.assoc_query(refl, values), position)
# Add the new join to the query and traverse the remaining
# joins that will start counting from the added join position.
query =
%{query | joins: query.joins ++ [join]}
|> joins_query(t, position)
|> Ecto.Query.Planner.prepare_sources()
# Our source is going to be the last join after
# traversing them all.
{joins, [assoc]} = Enum.split(query.joins, -1)
# Update the mapping and start rewriting expressions
# to make the last join point to the new from source.
mapping = Map.put(%{}, length(joins) + 1, 0)
assoc_on = rewrite_expr(assoc.on, mapping)
%{query | wheres: [assoc_on|query.wheres], joins: joins,
from: merge_from(query.from, assoc.source), sources: nil}
|> distinct([x], true)
end
defp assoc_to_join(%{from: from, wheres: [on], order_bys: [], joins: []}, position) do
%JoinExpr{ix: position, qual: :inner, source: from,
on: rewrite_expr(on, %{0 => position}),
file: on.file, line: on.line}
end
defp merge_from({"join expression", _}, assoc_source), do: assoc_source
defp merge_from(from, _assoc_source), do: from
defp rewrite_expr(%{expr: expr, params: params} = part, mapping) do
expr =
Macro.prewalk expr, fn
{:&, meta, [ix]} ->
{:&, meta, [Map.get(mapping, ix, ix)]}
other ->
other
end
params =
Enum.map params, fn
{val, {composite, {ix, field}}} when is_integer(ix) ->
{val, {composite, {Map.get(mapping, ix, ix), field}}}
{val, {ix, field}} when is_integer(ix) ->
{val, {Map.get(mapping, ix, ix), field}}
val ->
val
end
%{part | expr: expr, params: params}
end
end
defmodule Ecto.Association.BelongsTo do
@moduledoc """
The association struct for a `belongs_to` association.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the model
* `owner` - The model where the association was defined
* `related` - The model that is associated
* `owner_key` - The key on the `owner` model used for the association
* `related_key` - The key on the `related` model used for the association
* `queryable` - The real query to use for querying association
* `defaults` - Default fields used when building the association
"""
@behaviour Ecto.Association
defstruct [:cardinality, :field, :owner, :related, :owner_key, :related_key, :queryable, defaults: []]
@doc false
def struct(module, name, opts) do
ref =
cond do
ref = opts[:references] ->
ref
primary_key = Module.get_attribute(module, :primary_key) ->
case elem(primary_key, 0) do
:id -> :id
key ->
IO.puts :stderr, "warning: #{inspect module} has a custom primary key and " <>
"invoked belongs_to(#{inspect name}). To avoid ambiguity, " <>
"please also specify the :references option in belongs_to " <>
"with the primary key name of the associated model, currently " <>
"it defaults to #{inspect key}\n#{Exception.format_stacktrace}"
key
end
true ->
:id
end
queryable = Keyword.fetch!(opts, :queryable)
related = Ecto.Association.related_from_query(queryable)
unless is_atom(related) do
raise ArgumentError, "association queryable must be a model, got: #{inspect related}"
end
%__MODULE__{
field: name,
cardinality: :one,
owner: module,
related: related,
owner_key: Keyword.fetch!(opts, :foreign_key),
related_key: ref,
queryable: queryable
}
end
@doc false
def build(%{field: name}, %{__struct__: struct}, _attributes) do
raise ArgumentError,
"cannot build belongs_to association #{inspect name} for #{inspect struct}. " <>
"Belongs to associations cannot be built with build/3, only the opposide side (has_one/has_many)"
end
@doc false
def joins_query(refl) do
from o in refl.owner,
join: q in ^refl.queryable,
on: field(q, ^refl.related_key) == field(o, ^refl.owner_key)
end
@doc false
def assoc_query(refl, values) do
assoc_query(refl, refl.queryable, values)
end
@doc false
def assoc_query(refl, query, values) do
from x in query,
where: field(x, ^refl.related_key) in ^values
end
@doc false
def preload_info(refl) do
{:assoc, refl, refl.related_key}
end
end