Packages
ecto
1.0.5
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/repo/model.ex
defmodule Ecto.Repo.Model do
# The module invoked by user defined repos
# for model related functionality.
@moduledoc false
alias Ecto.Query.Planner
alias Ecto.Model.Callbacks
alias Ecto.Changeset
@doc """
Implementation for `Ecto.Repo.insert!/2`.
"""
def insert!(repo, adapter, model_or_changeset, opts) do
case insert(repo, adapter, model_or_changeset, opts) do
{:ok, model} -> model
{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :insert, changeset: changeset
end
end
@doc """
Implementation for `Ecto.Repo.update!/2`.
"""
def update!(repo, adapter, model_or_changeset, opts) do
case update(repo, adapter, model_or_changeset, opts) do
{:ok, model} -> model
{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :update, changeset: changeset
end
end
@doc """
Implementation for `Ecto.Repo.delete!/2`.
"""
def delete!(repo, adapter, model_or_changeset, opts) do
case delete(repo, adapter, model_or_changeset, opts) do
{:ok, model} -> model
{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :delete, changeset: changeset
end
end
@doc """
Implementation for `Ecto.Repo.insert/2`.
"""
def insert(repo, adapter, %Changeset{} = changeset, opts) when is_list(opts) do
changeset = update_changeset(changeset, :changeset, :insert, repo, opts)
do_insert(repo, adapter, changeset, opts)
end
def insert(repo, adapter, %{__struct__: _} = struct, opts) when is_list(opts) do
changeset =
struct
|> Ecto.Changeset.change()
|> update_changeset(:model, :insert, repo, opts)
do_insert(repo, adapter, changeset, opts)
end
defp do_insert(repo, adapter, %Changeset{valid?: true} = changeset, opts) do
struct = struct_from_changeset!(:insert, changeset)
model = struct.__struct__
fields = model.__schema__(:fields)
embeds = model.__schema__(:embeds)
assocs = model.__schema__(:associations)
return = model.__schema__(:read_after_writes)
# On insert, we always merge the whole struct into the
# changeset as changes, except the primary key if it is nil.
# We also remove all embeds that are not in the changes
changeset = insert_changes(struct, fields, embeds, assocs, changeset)
wrap_in_transaction(repo, adapter, model, opts, embeds, assocs,
~w(before_insert after_insert)a, fn ->
user_changeset = Callbacks.__apply__(model, :before_insert, changeset)
changeset = Ecto.Embedded.prepare(user_changeset, embeds, adapter, :insert)
{assoc_changes, changeset} = pop_from_changes(changeset, assocs)
{autogen, changes} = pop_autogenerate_id(changeset.changes, model)
changes = validate_changes(:insert, changes, model, fields, adapter)
{embed_changes, changeset} = pop_from_changes(changeset, embeds)
args = [repo, metadata(struct), changes, autogen, return, opts]
case apply(changeset, adapter, :insert, args) do
{:ok, changeset} ->
opts = Keyword.put(opts, :skip_transaction, true)
changeset
|> process_embeds(embed_changes, adapter, repo, opts)
|> process_assocs(assoc_changes, adapter, repo, opts)
|> maybe_process_after(user_changeset, model, :after_insert)
{:invalid, constraints} ->
{:error, constraints_to_errors(user_changeset, :insert, constraints)}
end
end)
end
defp do_insert(_repo, _adapter, %Changeset{valid?: false} = changeset, _opts) do
{:error, %{changeset | action: :insert}}
end
@doc """
Implementation for `Ecto.Repo.update/2`.
"""
def update(repo, adapter, %Changeset{} = changeset, opts) when is_list(opts) do
changeset = update_changeset(changeset, :changeset, :update, repo, opts)
do_update(repo, adapter, changeset, opts)
end
def update(repo, adapter, %{__struct__: model} = struct, opts) when is_list(opts) do
changes =
struct
|> Map.take(model.__schema__(:fields))
|> Map.drop(model.__schema__(:primary_key))
|> Map.drop(model.__schema__(:embeds))
changeset =
struct
|> Ecto.Changeset.change()
|> Map.put(:changes, changes)
|> update_changeset(:model, :update, repo, opts)
do_update(repo, adapter, changeset, opts)
end
defp do_update(repo, adapter, %Changeset{valid?: true} = changeset, opts) do
struct = struct_from_changeset!(:update, changeset)
model = struct.__struct__
fields = model.__schema__(:fields)
embeds = model.__schema__(:embeds)
assocs = model.__schema__(:associations)
return = model.__schema__(:read_after_writes)
# Differently from insert, update does not copy the struct
# fields into the changeset. All changes must be in the
# changeset before hand.
if changeset.changes != %{} or opts[:force] do
wrap_in_transaction(repo, adapter, model, opts, embeds, assocs,
~w(before_update after_update)a, fn ->
user_changeset = Callbacks.__apply__(model, :before_update, changeset)
changeset = Ecto.Embedded.prepare(user_changeset, embeds, adapter, :update)
{assoc_changes, changeset} = pop_from_changes(changeset, assocs)
autogen = get_autogenerate_id(changeset.changes, model)
changes = validate_changes(:update, changeset.changes, model, fields, adapter)
{embed_changes, changeset} = pop_from_changes(changeset, embeds)
filters = add_pk_filter!(changeset.filters, struct)
filters = Planner.fields(model, :update, filters, adapter)
args = [repo, metadata(struct), changes, filters, autogen, return, opts]
action = if changes == [], do: :noop, else: :update
case apply(changeset, adapter, action, args) do
{:ok, changeset} ->
opts = Keyword.put(opts, :skip_transaction, true)
changeset
|> process_embeds(embed_changes, adapter, repo, opts)
|> process_assocs(assoc_changes, adapter, repo, opts)
|> maybe_process_after(user_changeset, model, :after_update)
{:invalid, constraints} ->
{:error, constraints_to_errors(user_changeset, :update, constraints)}
end
end)
else
{:ok, changeset.model}
end
end
defp do_update(_repo, _adapter, %Changeset{valid?: false} = changeset, _opts) do
{:error, %{changeset | action: :update}}
end
@doc """
Implementation for `Ecto.Repo.delete/2`.
"""
def delete(repo, adapter, %Changeset{} = changeset, opts) when is_list(opts) do
changeset = update_changeset(changeset, :changeset, :delete, repo, opts)
do_delete(repo, adapter, changeset, opts)
end
def delete(repo, adapter, %{__struct__: _} = struct, opts) when is_list(opts) do
changeset =
struct
|> Ecto.Changeset.change()
|> update_changeset(:model, :delete, repo, opts)
do_delete(repo, adapter, changeset, opts)
end
defp do_delete(repo, adapter, %Changeset{valid?: true} = changeset, opts) do
struct = struct_from_changeset!(:delete, changeset)
model = struct.__struct__
embeds = model.__schema__(:embeds)
changeset = %{changeset | changes: %{}}
autogen = get_autogenerate_id(changeset, model)
wrap_in_transaction(repo, adapter, model, opts, embeds, [],
~w(before_delete after_delete)a, fn ->
user_changeset = Callbacks.__apply__(model, :before_delete, changeset)
# We don't prepare the changeset on delete, so we just copy the user one
changeset = user_changeset
embeds =
changeset
|> Ecto.Embedded.prepare(embeds, adapter, :delete)
|> Map.fetch!(:changes)
|> Map.take(embeds)
filters = add_pk_filter!(changeset.filters, struct)
filters = Planner.fields(model, :delete, filters, adapter)
args = [repo, metadata(struct), filters, autogen, opts]
case apply(changeset, adapter, :delete, args) do
{:ok, changeset} ->
opts = Keyword.put(opts, :skip_transaction, true)
# We ignore the results because we still want to keep
# the embed values in the model. Also note we don't
# process associations because they are handled externally.
_ = process_embeds(changeset, embeds, adapter, repo, opts)
maybe_process_after({:ok, changeset}, user_changeset, model, :after_delete)
{:invalid, constraints} ->
{:error, constraints_to_errors(user_changeset, :delete, constraints)}
end
end)
end
defp do_delete(_repo, _adapter, %Changeset{valid?: false} = changeset, _opts) do
{:error, %{changeset | action: :delete}}
end
## Helpers
defp struct_from_changeset!(action, %{model: nil}),
do: raise(ArgumentError, "cannot #{action} a changeset without a model")
defp struct_from_changeset!(_action, %{model: struct}),
do: struct
defp update_changeset(%{action: given}, _source, action, repo, _opts) when given != nil and given != action,
do: raise(ArgumentError, "a changeset with action #{inspect given} was given to #{inspect repo}.#{action}/2")
defp update_changeset(changeset, source, action, repo, opts),
do: %{changeset | action: action, repo: repo, opts: [source: source] ++ opts}
defp metadata(%{__struct__: model, __meta__: meta}) do
meta
|> Map.delete(:__struct__)
|> Map.put(:model, model)
end
defp apply(changeset, _adapter, :noop, _args) do
{:ok, changeset}
end
defp apply(changeset, adapter, action, args) do
case apply(adapter, action, args) do
{:ok, values} ->
{:ok, load_changes(changeset, action, values, adapter)}
{:invalid, _} = constraints ->
constraints
{:error, :stale} ->
raise Ecto.StaleModelError, model: changeset.model, action: action
end
end
defp constraints_to_errors(%{constraints: user_constraints} = changeset, action, constraints) do
Enum.reduce constraints, changeset, fn {type, constraint}, acc ->
user_constraint =
Enum.find(user_constraints, fn c ->
c.type == type and c.constraint == constraint
end)
case user_constraint do
%{field: field, message: message} ->
Ecto.Changeset.add_error(acc, field, message)
nil ->
raise Ecto.ConstraintError, action: action, type: type,
constraint: constraint, changeset: changeset
end
end
end
defp load_changes(%{types: types} = changeset, action, values, adapter) do
# It is ok to use types from changeset because we have
# already filtered the results to be only about fields.
model =
changeset
|> Ecto.Changeset.apply_changes
|> do_load(values, types, adapter)
model = put_in(model.__meta__.state, action_to_state(action))
Map.put(changeset, :model, model)
end
defp action_to_state(:insert), do: :loaded
defp action_to_state(:update), do: :loaded
defp action_to_state(:delete), do: :deleted
defp do_load(struct, kv, types, adapter) do
Enum.reduce(kv, struct, fn
{k, v}, acc ->
type = Map.fetch!(types, k)
case adapter.load(type, v) do
{:ok, v} -> Map.put(acc, k, v)
:error -> raise ArgumentError, "cannot load `#{inspect v}` as type #{inspect type}"
end
end)
end
defp insert_changes(struct, fields, embeds, assocs, changeset) do
types = changeset.types
assert_empty_relation!(struct, embeds, types)
assert_empty_relation!(struct, assocs, types)
base =
Enum.reduce embeds, Map.take(struct, fields), fn field, acc ->
{:embed, embed} = Map.get(types, field)
Map.put(acc, field, Ecto.Changeset.Relation.empty(embed))
end
update_in changeset.changes, &Map.merge(base, &1)
end
defp assert_empty_relation!(struct, relation, types) do
Enum.each relation, fn field ->
case Map.get(types, field) do
{kind, relation} ->
value = Map.get(struct, field)
kind = kind |> Atom.to_string
unless Ecto.Changeset.Relation.empty?(relation, value) do
raise ArgumentError, "model #{inspect struct.__struct__} has value `#{inspect value}` " <>
"set for #{kind} named `#{field}`. #{String.capitalize kind}s can only be " <>
"manipulated via changesets, be it on insert, update or delete."
end
_ ->
:ok
end
end
end
defp pop_from_changes(changeset, fields) do
get_and_update_in(changeset.changes, &Map.split(&1, fields))
end
defp process_embeds(changeset, embeds, adapter, repo, opts) do
{:ok, changeset} =
Ecto.Changeset.Relation.on_repo_action(changeset, embeds, adapter, repo, opts)
changeset
end
defp process_assocs(changeset, assocs, adapter, repo, opts) do
Ecto.Changeset.Relation.on_repo_action(changeset, assocs, adapter, repo, opts)
end
defp maybe_process_after({:ok, changeset}, _user_changeset, model, callback) do
{:ok, Callbacks.__apply__(model, callback, changeset).model}
end
defp maybe_process_after({:error, %{changes: changes}}, user_changeset, _model, _callback) do
{:error, %{user_changeset | valid?: false, changes: changes}}
end
defp pop_autogenerate_id(changes, model) do
case model.__schema__(:autogenerate_id) do
{key, id} ->
case Map.pop(changes, key) do
{nil, changes} -> {{key, id, nil}, changes}
{value, _} -> {{key, id, value}, changes}
end
nil ->
{nil, changes}
end
end
defp get_autogenerate_id(changes, model) do
case model.__schema__(:autogenerate_id) do
{key, id} -> {key, id, Map.get(changes, key)}
nil -> nil
end
end
defp validate_changes(kind, changes, model, fields, adapter) do
Planner.fields(model, kind, Map.take(changes, fields), adapter)
end
defp add_pk_filter!(filters, struct) do
Enum.reduce Ecto.Model.primary_key!(struct), filters, fn
{_k, nil}, _acc ->
raise Ecto.NoPrimaryKeyValueError, struct: struct
{k, v}, acc ->
Map.put(acc, k, v)
end
end
defp wrap_in_transaction(repo, adapter, model, opts, embeds, assocs, callbacks, fun) do
if transaction_required?(model, embeds, assocs, callbacks) and
Keyword.get(opts, :skip_transaction) != true and
function_exported?(adapter, :transaction, 3) do
adapter.transaction(repo, opts, fn ->
case fun.() do
{:ok, model} -> model
{:error, changeset} -> adapter.rollback(repo, changeset)
end
end)
else
fun.()
end
end
defp transaction_required?(model, embeds, assocs, callbacks) do
embeds != [] or assocs != [] or
Enum.any?(callbacks, &function_exported?(model, &1, 1))
end
end