Packages
ecto
1.1.2
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/schema.ex
defmodule Ecto.Repo.Schema 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
do_insert(repo, adapter, changeset, opts)
end
def insert(repo, adapter, %{__struct__: _} = struct, opts) when is_list(opts) do
changeset = Ecto.Changeset.change(struct)
do_insert(repo, adapter, changeset, opts)
end
defp do_insert(repo, adapter, %Changeset{valid?: true, prepare: prepare} = 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 = update_changeset(changeset, :insert, repo)
changeset = insert_changes(struct, fields, embeds, assocs, changeset)
wrap_in_transaction(repo, adapter, model, opts, embeds, assocs, prepare,
~w(before_insert after_insert)a, fn ->
changeset = run_prepare(changeset, prepare)
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, extra} = dump_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, extra, 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, update_changeset(changeset, :insert, repo)}
end
@doc """
Implementation for `Ecto.Repo.update/2`.
"""
def update(repo, adapter, %Changeset{} = changeset, opts) when is_list(opts) do
do_update(repo, adapter, changeset, opts)
end
def update(repo, adapter, %{__struct__: model} = struct, opts) when is_list(opts) do
IO.puts :stderr, "warning: giving a struct to #{inspect repo}.update/2 is deprecated. " <>
"Ecto is unable to properly track changes when a struct is given and " <>
"an Ecto.Changeset must be given instead\n#{Exception.format_stacktrace}"
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)
do_update(repo, adapter, changeset, opts)
end
defp do_update(repo, adapter, %Changeset{valid?: true, prepare: prepare} = 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.
changeset = update_changeset(changeset, :update, repo)
if changeset.changes != %{} or opts[:force] do
wrap_in_transaction(repo, adapter, model, opts, embeds, assocs, prepare,
~w(before_update after_update)a, fn ->
changeset = run_prepare(changeset, prepare)
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, extra} = dump_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, extra, 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, update_changeset(changeset, :update, repo)}
end
@doc """
Implementation for `Ecto.Repo.insert_or_update/2`.
"""
def insert_or_update(repo, adapter, changeset, opts) do
case get_state(changeset) do
:built -> insert repo, adapter, changeset, opts
:loaded -> update repo, adapter, changeset, opts
state -> raise ArgumentError, "the changeset has an invalid state " <>
"for Repo.insert_or_update/2: #{state}"
end
end
@doc """
Implementation for `Ecto.Repo.insert_or_update!/2`.
"""
def insert_or_update!(repo, adapter, changeset, opts) do
case get_state(changeset) do
:built -> insert! repo, adapter, changeset, opts
:loaded -> update! repo, adapter, changeset, opts
state -> raise ArgumentError, "the changeset has an invalid state " <>
"for Repo.insert_or_update!/2: #{state}"
end
end
defp get_state(%Changeset{model: model}), do: model.__meta__.state
defp get_state(%{__struct__: _model}) do
raise ArgumentError, "giving a struct to Repo.insert_or_update/2 or " <>
"Repo.insert_or_update!/2 is not supported. " <>
"Please use an Ecto.Changeset"
end
@doc """
Implementation for `Ecto.Repo.delete/2`.
"""
def delete(repo, adapter, %Changeset{} = changeset, opts) when is_list(opts) do
do_delete(repo, adapter, changeset, opts)
end
def delete(repo, adapter, %{__struct__: _} = struct, opts) when is_list(opts) do
changeset = Ecto.Changeset.change(struct)
do_delete(repo, adapter, changeset, opts)
end
defp do_delete(repo, adapter, %Changeset{valid?: true, prepare: prepare} = changeset, opts) do
struct = struct_from_changeset!(:delete, changeset)
model = struct.__struct__
embeds = model.__schema__(:embeds)
assocs = model.__schema__(:associations)
changeset = update_changeset(changeset, :delete, repo)
changeset = %{changeset | changes: %{}}
autogen = get_autogenerate_id(changeset, model)
wrap_in_transaction(repo, adapter, model, opts, embeds, assocs, prepare,
~w(before_delete after_delete)a, fn ->
changeset = run_prepare(changeset, prepare)
delete_assocs(changeset, repo, model, assocs)
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, update_changeset(changeset, :delete, repo)}
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}, action, repo) 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, action, repo),
do: %{changeset | action: action, repo: repo}
defp run_prepare(changeset, prepare) do
Enum.reduce(Enum.reverse(prepare), changeset, fn fun, acc ->
case fun.(acc) do
%Ecto.Changeset{} = acc -> acc
other ->
raise "expected function #{inspect fun} given to Ecto.Changeset.prepare_changes/2 " <>
"to return an Ecto.Changeset, got: `#{inspect other}`"
end
end)
end
defp metadata(%{__struct__: model, __meta__: meta}) do
meta
|> Map.delete(:__struct__)
|> Map.put(:model, model)
end
defp apply(changeset, _adapter, :noop, _extra, _args) do
{:ok, changeset}
end
defp apply(changeset, adapter, action, extra, args) do
case apply(adapter, action, args) do
{:ok, values} ->
{:ok, load_changes(changeset, action, extra ++ 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
|> load_each(values, types, adapter)
model = put_in(model.__meta__.state, action_to_state(action))
Map.put(changeset, :model, model)
end
defp load_each(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 action_to_state(:insert), do: :loaded
defp action_to_state(:update), do: :loaded
defp action_to_state(:delete), do: :deleted
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.fetch!(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
# TODO: In the future, once we support belongs_to associations,
# we will want to optimize this as some associations must be
# acted on before delete and some after and we likely won't
# want to traverse it twice.
defp delete_assocs(%{model: model}, repo, struct, assocs) do
for assoc_name <- assocs do
case struct.__schema__(:association, assoc_name) do
# TODO: fetch_and_delete is deprecated. Removed by 2.0
%{on_delete: :fetch_and_delete, field: field} ->
assocs = repo.all Ecto.assoc(model, field)
Enum.each assocs, &repo.delete!(&1)
%{on_delete: :delete_all, field: field} ->
repo.delete_all Ecto.assoc(model, field)
%{on_delete: :nilify_all, related_key: related_key, field: field} ->
query = Ecto.assoc(model, field)
repo.update_all query, set: [{related_key, nil}]
_ ->
:ok
end
end
:ok
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 dump_changes(action, changes, model, fields, adapter) do
changes = Map.take(changes, fields)
{leftover, autogen} = autogenerate_changes(model, action, changes)
dumped = Planner.fields(model, action, leftover, adapter)
{autogen ++ dumped, autogen}
end
defp autogenerate_changes(model, action, changes) do
Enum.reduce model.__schema__(:autogenerate, action), {changes, []},
fn {k, mod, args}, {acc_changes, acc_autogen} ->
case Map.get(acc_changes, k) do
nil -> {Map.delete(acc_changes, k), [{k, apply(mod, :autogenerate, args)}|acc_autogen]}
_ -> {acc_changes, acc_autogen}
end
end
end
defp add_pk_filter!(filters, struct) do
Enum.reduce Ecto.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, prepare, callbacks, fun) do
if transaction_required?(model, embeds, assocs, prepare, 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, prepare, callbacks) do
embeds != [] or assocs != [] or prepare != [] or
Enum.any?(callbacks, &function_exported?(model, &1, 1))
end
end