Packages
ecto
3.4.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 schema related functionality.
@moduledoc false
alias Ecto.Changeset
alias Ecto.Changeset.Relation
require Ecto.Query
@doc """
Implementation for `Ecto.Repo.insert_all/3`.
"""
def insert_all(_repo, name, schema, rows, opts) when is_atom(schema) do
do_insert_all(name, schema, schema.__schema__(:prefix),
schema.__schema__(:source), rows, opts)
end
def insert_all(_repo, name, table, rows, opts) when is_binary(table) do
do_insert_all(name, nil, nil, table, rows, opts)
end
def insert_all(_repo, name, {source, schema}, rows, opts) when is_atom(schema) do
do_insert_all(name, schema, schema.__schema__(:prefix), source, rows, opts)
end
defp do_insert_all(_name, _schema, _prefix, _source, [], opts) do
if opts[:returning] do
{0, []}
else
{0, nil}
end
end
defp do_insert_all(name, schema, prefix, source, rows, opts) when is_list(rows) do
{adapter, adapter_meta} = Ecto.Repo.Registry.lookup(name)
autogen_id = schema && schema.__schema__(:autogenerate_id)
dumper = schema && schema.__schema__(:dump)
{return_fields_or_types, return_sources} =
schema
|> returning(opts)
|> fields_to_sources(dumper)
{rows, header} = extract_header_and_fields(rows, schema, dumper, autogen_id, adapter)
counter = fn -> Enum.reduce(rows, 0, &length(&1) + &2) end
schema_meta = metadata(schema, prefix, source, autogen_id, nil, opts)
on_conflict = Keyword.get(opts, :on_conflict, :raise)
conflict_target = Keyword.get(opts, :conflict_target, [])
conflict_target = conflict_target(conflict_target, dumper)
on_conflict = on_conflict(on_conflict, conflict_target, schema_meta, counter, adapter)
{count, rows} =
adapter.insert_all(adapter_meta, schema_meta, Map.keys(header), rows, on_conflict, return_sources, opts)
{count, postprocess(rows, return_fields_or_types, adapter, schema, schema_meta)}
end
defp postprocess(nil, [], _adapter, _schema, _schema_meta) do
nil
end
defp postprocess(rows, fields, _adapter, nil, _schema_meta) do
for row <- rows, do: Map.new(Enum.zip(fields, row))
end
defp postprocess(rows, types, adapter, schema, %{prefix: prefix, source: source}) do
struct = Ecto.Schema.Loader.load_struct(schema, prefix, source)
for row <- rows do
{loaded, _} = Ecto.Repo.Queryable.struct_load!(types, row, [], false, struct, adapter)
loaded
end
end
defp extract_header_and_fields(rows, schema, dumper, autogen_id, adapter) do
mapper = init_mapper(schema, dumper, adapter)
{rows, {header, has_query?}} =
Enum.map_reduce(rows, {%{}, false}, fn fields, acc ->
{fields, {header, has_query?}} = Enum.map_reduce(fields, acc, mapper)
{fields, header} = autogenerate_id(autogen_id, fields, header, adapter)
{fields, {header, has_query?}}
end)
if has_query? do
rows = plan_query_in_rows(rows, header, adapter)
{rows, header}
else
{rows, header}
end
end
defp init_mapper(nil, _dumper, _adapter) do
fn {field, _} = tuple, {header, has_query?} ->
{tuple, {Map.put(header, field, true), has_query?}}
end
end
defp init_mapper(schema, dumper, adapter) do
fn {field, value}, {header, has_query?} ->
case dumper do
%{^field => {source, type}} ->
case value do
%Ecto.Query{} = query ->
{{source, query}, {Map.put(header, source, true), true}}
value ->
value = dump_field!(:insert_all, schema, field, type, value, adapter)
{{source, value}, {Map.put(header, source, true), has_query?}}
end
%{} ->
raise ArgumentError, "unknown field `#{inspect(field)}` in schema #{inspect(schema)} given to " <>
"insert_all. Note virtual fields and associations are not supported"
end
end
end
defp plan_query_in_rows(rows, header, adapter) do
{rows, _counter} =
Enum.map_reduce(rows, 0, fn fields, counter ->
Enum.flat_map_reduce(header, counter, fn {key, _}, counter ->
case :lists.keyfind(key, 1, fields) do
{^key, %Ecto.Query{} = query} ->
{query, params, _} = Ecto.Query.Planner.plan(query, :all, adapter)
{query, _} = Ecto.Query.Planner.normalize(query, :all, adapter, counter)
{[{key, {query, params}}], counter + length(params)}
{^key, value} ->
{[{key, value}], counter + 1}
false ->
{[], counter}
end
end)
end)
rows
end
defp autogenerate_id(nil, fields, header, _adapter) do
{fields, header}
end
defp autogenerate_id({key, source, type}, fields, header, adapter) do
case :lists.keyfind(key, 1, fields) do
{^key, _} ->
{fields, header}
false ->
if value = Ecto.Type.adapter_autogenerate(adapter, type) do
{[{source, value} | fields], Map.put(header, source, true)}
else
{fields, header}
end
end
end
@doc """
Implementation for `Ecto.Repo.insert!/2`.
"""
def insert!(repo, name, struct_or_changeset, opts) do
case insert(repo, name, struct_or_changeset, opts) do
{:ok, struct} ->
struct
{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :insert, changeset: changeset
end
end
@doc """
Implementation for `Ecto.Repo.update!/2`.
"""
def update!(repo, name, struct_or_changeset, opts) do
case update(repo, name, struct_or_changeset, opts) do
{:ok, struct} ->
struct
{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :update, changeset: changeset
end
end
@doc """
Implementation for `Ecto.Repo.delete!/2`.
"""
def delete!(repo, name, struct_or_changeset, opts) do
case delete(repo, name, struct_or_changeset, opts) do
{:ok, struct} ->
struct
{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :delete, changeset: changeset
end
end
@doc """
Implementation for `Ecto.Repo.insert/2`.
"""
def insert(repo, name, %Changeset{} = changeset, opts) when is_list(opts) do
do_insert(repo, name, changeset, opts)
end
def insert(repo, name, %{__struct__: _} = struct, opts) when is_list(opts) do
do_insert(repo, name, Ecto.Changeset.change(struct), opts)
end
defp do_insert(repo, name, %Changeset{valid?: true} = changeset, opts) do
{adapter, adapter_meta} = Ecto.Repo.Registry.lookup(name)
%{prepare: prepare, repo_opts: repo_opts} = changeset
opts = Keyword.merge(repo_opts, opts)
struct = struct_from_changeset!(:insert, changeset)
schema = struct.__struct__
dumper = schema.__schema__(:dump)
fields = schema.__schema__(:fields)
assocs = schema.__schema__(:associations)
embeds = schema.__schema__(:embeds)
{return_types, return_sources} =
schema
|> returning(opts)
|> add_read_after_writes(schema)
|> fields_to_sources(dumper)
on_conflict = Keyword.get(opts, :on_conflict, :raise)
conflict_target = Keyword.get(opts, :conflict_target, [])
conflict_target = conflict_target(conflict_target, dumper)
# On insert, we always merge the whole struct into the
# changeset as changes, except the primary key if it is nil.
changeset = put_repo_and_action(changeset, :insert, repo, opts)
changeset = surface_changes(changeset, struct, fields ++ assocs)
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
assoc_opts = assoc_opts(assocs, opts)
user_changeset = run_prepare(changeset, prepare)
{changeset, parents, children} = pop_assocs(user_changeset, assocs)
changeset = process_parents(changeset, parents, adapter, assoc_opts)
changeset = repo_changes(changeset)
if changeset.valid? do
embeds = Ecto.Embedded.prepare(changeset, embeds, adapter, :insert)
autogen_id = schema.__schema__(:autogenerate_id)
schema_meta = metadata(struct, autogen_id, opts)
changes = Map.merge(changeset.changes, embeds)
{changes, extra, return_types, return_sources} =
autogenerate_id(autogen_id, changes, return_types, return_sources, adapter)
{changes, autogen} =
dump_changes!(:insert, Map.take(changes, fields), schema, extra, dumper, adapter)
on_conflict =
on_conflict(on_conflict, conflict_target, schema_meta, fn -> length(changes) end, adapter)
args = [adapter_meta, schema_meta, changes, on_conflict, return_sources, opts]
case apply(user_changeset, adapter, :insert, args) do
{:ok, values} ->
values = extra ++ values
changeset
|> load_changes(:loaded, return_types, values, embeds, autogen, adapter, schema_meta)
|> process_children(children, user_changeset, adapter, assoc_opts)
{:error, _} = error ->
error
end
else
{:error, changeset}
end
end)
end
defp do_insert(repo, _name, %Changeset{valid?: false} = changeset, opts) do
{:error, put_repo_and_action(changeset, :insert, repo, opts)}
end
@doc """
Implementation for `Ecto.Repo.update/2`.
"""
def update(repo, name, %Changeset{} = changeset, opts) when is_list(opts) do
do_update(repo, name, changeset, opts)
end
def update(_repo, _name, %{__struct__: _}, opts) when is_list(opts) do
raise ArgumentError, "giving a struct to Ecto.Repo.update/2 is not supported. " <>
"Ecto is unable to properly track changes when a struct is given, " <>
"an Ecto.Changeset must be given instead"
end
defp do_update(repo, name, %Changeset{valid?: true} = changeset, opts) do
{adapter, adapter_meta} = Ecto.Repo.Registry.lookup(name)
%{prepare: prepare, repo_opts: repo_opts} = changeset
opts = Keyword.merge(repo_opts, opts)
struct = struct_from_changeset!(:update, changeset)
schema = struct.__struct__
dumper = schema.__schema__(:dump)
fields = schema.__schema__(:fields)
assocs = schema.__schema__(:associations)
embeds = schema.__schema__(:embeds)
force? = !!opts[:force]
filters = add_pk_filter!(changeset.filters, struct)
{return_types, return_sources} =
schema
|> returning(opts)
|> add_read_after_writes(schema)
|> fields_to_sources(dumper)
# Differently from insert, update does not copy the struct
# fields into the changeset. All changes must be in the
# changeset before hand.
changeset = put_repo_and_action(changeset, :update, repo, opts)
if changeset.changes != %{} or changeset.repo_changes != %{} or force? do
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
assoc_opts = assoc_opts(assocs, opts)
user_changeset = run_prepare(changeset, prepare)
{changeset, parents, children} = pop_assocs(user_changeset, assocs)
changeset = process_parents(changeset, parents, adapter, assoc_opts)
changeset = repo_changes(changeset)
if changeset.valid? do
embeds = Ecto.Embedded.prepare(changeset, embeds, adapter, :update)
original = changeset.changes |> Map.merge(embeds) |> Map.take(fields)
{changes, autogen} = dump_changes!(:update, original, schema, [], dumper, adapter)
schema_meta = metadata(struct, schema.__schema__(:autogenerate_id), opts)
filters = dump_fields!(:update, schema, filters, dumper, adapter)
args = [adapter_meta, schema_meta, changes, filters, return_sources, opts]
# If there are no changes or all the changes were autogenerated but not forced, we skip
{action, autogen} =
if original != %{} or (autogen != [] and force?),
do: {:update, autogen},
else: {:noop, []}
case apply(user_changeset, adapter, action, args) do
{:ok, values} ->
changeset
|> load_changes(:loaded, return_types, values, embeds, autogen, adapter, schema_meta)
|> process_children(children, user_changeset, adapter, assoc_opts)
{:error, _} = error ->
error
end
else
{:error, changeset}
end
end)
else
{:ok, changeset.data}
end
end
defp do_update(repo, _name, %Changeset{valid?: false} = changeset, opts) do
{:error, put_repo_and_action(changeset, :update, repo, opts)}
end
@doc """
Implementation for `Ecto.Repo.insert_or_update/2`.
"""
def insert_or_update(repo, name, changeset, opts) do
case get_state(changeset) do
:built -> insert(repo, name, changeset, opts)
:loaded -> update(repo, name, 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, name, changeset, opts) do
case get_state(changeset) do
:built -> insert!(repo, name, changeset, opts)
:loaded -> update!(repo, name, changeset, opts)
state -> raise ArgumentError, "the changeset has an invalid state " <>
"for Repo.insert_or_update!/2: #{state}"
end
end
defp get_state(%Changeset{data: %{__meta__: %{state: state}}}), do: state
defp get_state(%{__struct__: _}) 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, name, %Changeset{} = changeset, opts) when is_list(opts) do
do_delete(repo, name, changeset, opts)
end
def delete(repo, name, %{__struct__: _} = struct, opts) when is_list(opts) do
changeset = Ecto.Changeset.change(struct)
do_delete(repo, name, changeset, opts)
end
defp do_delete(repo, name, %Changeset{valid?: true} = changeset, opts) do
{adapter, adapter_meta} = Ecto.Repo.Registry.lookup(name)
%{prepare: prepare, repo_opts: repo_opts} = changeset
opts = Keyword.merge(repo_opts, opts)
struct = struct_from_changeset!(:delete, changeset)
schema = struct.__struct__
assocs = to_delete_assocs(schema)
dumper = schema.__schema__(:dump)
changeset = put_repo_and_action(changeset, :delete, repo, opts)
wrap_in_transaction(adapter, adapter_meta, opts, assocs != [], prepare, fn ->
changeset = run_prepare(changeset, prepare)
filters = add_pk_filter!(changeset.filters, struct)
filters = dump_fields!(:delete, schema, filters, dumper, adapter)
# Delete related associations
for %{__struct__: mod, on_delete: on_delete} = reflection <- assocs do
apply(mod, on_delete, [reflection, changeset.data, name, opts])
end
schema_meta = metadata(struct, schema.__schema__(:autogenerate_id), opts)
args = [adapter_meta, schema_meta, filters, opts]
case apply(changeset, adapter, :delete, args) do
{:ok, values} ->
changeset = load_changes(changeset, :deleted, [], values, %{}, [], adapter, schema_meta)
{:ok, changeset.data}
{:error, _} = error ->
error
end
end)
end
defp do_delete(repo, _name, %Changeset{valid?: false} = changeset, opts) do
{:error, put_repo_and_action(changeset, :delete, repo, opts)}
end
def load(adapter, schema_or_types, data) do
do_load(schema_or_types, data, &Ecto.Type.adapter_load(adapter, &1, &2))
end
defp do_load(schema, data, loader) when is_list(data),
do: do_load(schema, Map.new(data), loader)
defp do_load(schema, {fields, values}, loader) when is_list(fields) and is_list(values),
do: do_load(schema, Enum.zip(fields, values), loader)
defp do_load(schema, data, loader) when is_atom(schema),
do: Ecto.Schema.Loader.unsafe_load(schema, data, loader)
defp do_load(types, data, loader) when is_map(types),
do: Ecto.Schema.Loader.unsafe_load(%{}, types, data, loader)
## Helpers
defp returning(schema, opts) do
case Keyword.get(opts, :returning, false) do
[_ | _] = fields ->
fields
[] ->
raise ArgumentError, ":returning expects at least one field to be given, got an empty list"
true when is_nil(schema) ->
raise ArgumentError, ":returning option can only be set to true if a schema is given"
true ->
schema.__schema__(:fields)
false ->
[]
end
end
defp add_read_after_writes([], schema),
do: schema.__schema__(:read_after_writes)
defp add_read_after_writes(return, schema),
do: Enum.uniq(return ++ schema.__schema__(:read_after_writes))
defp fields_to_sources(fields, nil) do
{fields, fields}
end
defp fields_to_sources(fields, dumper) do
Enum.reduce(fields, {[], []}, fn field, {types, sources} ->
{source, type} = Map.fetch!(dumper, field)
{[{field, type} | types], [source | sources]}
end)
end
defp repo_changes(%{repo_changes: repo_changes} = changeset) do
if repo_changes == %{} do
changeset
else
update_in(changeset.changes, &Map.merge(&1, repo_changes))
end
end
defp struct_from_changeset!(action, %{data: nil}),
do: raise(ArgumentError, "cannot #{action} a changeset without :data")
defp struct_from_changeset!(_action, %{data: struct}),
do: struct
defp put_repo_and_action(%{action: :ignore, valid?: valid?} = changeset, action, repo, opts) do
if valid? do
raise ArgumentError, "a valid changeset with action :ignore was given to " <>
"#{inspect repo}.#{action}/2. Changesets can only be ignored " <>
"in a repository action if they are also invalid"
else
%{changeset | action: action, repo: repo, repo_opts: opts}
end
end
defp put_repo_and_action(%{action: given}, 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 put_repo_and_action(changeset, action, repo, opts),
do: %{changeset | action: action, repo: repo, repo_opts: opts}
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(schema, prefix, source, autogen_id, context, opts) do
%{
autogenerate_id: autogen_id,
context: context,
schema: schema,
source: source,
prefix: Keyword.get(opts, :prefix, prefix)
}
end
defp metadata(%{__struct__: schema, __meta__: %{context: context, source: source, prefix: prefix}},
autogen_id, opts) do
metadata(schema, prefix, source, autogen_id, context, opts)
end
defp metadata(%{__struct__: schema}, _, _) do
raise ArgumentError, "#{inspect(schema)} needs to be a schema with source"
end
defp conflict_target({:constraint, constraint}, _dumper) when is_atom(constraint) do
IO.warn "{:constraint, constraint} option for :conflict_target is deprecated, " <>
"use {:unsafe_fragment, \"ON CONSTRAINT #{constraint}\" instead"
{:constraint, constraint}
end
defp conflict_target({:unsafe_fragment, fragment}, _dumper) when is_binary(fragment) do
{:unsafe_fragment, fragment}
end
defp conflict_target(conflict_target, dumper) do
for target <- List.wrap(conflict_target) do
case dumper do
%{^target => {alias, _}} ->
alias
%{} when is_atom(target) ->
raise ArgumentError, "unknown field `#{inspect(target)}` in conflict_target"
_ ->
target
end
end
end
defp on_conflict(on_conflict, conflict_target, schema_meta, counter_fun, adapter) do
%{source: source, schema: schema, prefix: prefix} = schema_meta
case on_conflict do
:raise when conflict_target == [] ->
{:raise, [], []}
:raise ->
raise ArgumentError, ":conflict_target option is forbidden when :on_conflict is :raise"
:nothing ->
{:nothing, [], conflict_target}
{:replace, keys} when is_list(keys) and conflict_target == [] ->
raise ArgumentError, ":conflict_target option is required when :on_conflict is replace"
{:replace, keys} when is_list(keys) ->
fields = Enum.map(keys, &field_source!(schema, &1))
{fields, [], conflict_target}
:replace_all ->
{replace_all_fields!(:replace_all, schema, []), [], conflict_target}
{:replace_all_except, fields} ->
{replace_all_fields!(:replace_all_except, schema, fields), [], conflict_target}
:replace_all_except_primary_key ->
# TODO: Remove me in future versions
IO.warn ":replace_all_except_primary_key is deprecated, please use {:replace_all_except, [...]} instead"
fields = replace_all_fields!(:replace_all_except_primary_key, schema, schema && schema.__schema__(:primary_key))
{fields, [], conflict_target}
[_ | _] = on_conflict ->
from = if schema, do: {source, schema}, else: source
query = Ecto.Query.from from, update: ^on_conflict
on_conflict_query(query, {source, schema}, prefix, counter_fun, adapter, conflict_target)
%Ecto.Query{} = query ->
on_conflict_query(query, {source, schema}, prefix, counter_fun, adapter, conflict_target)
other ->
raise ArgumentError, "unknown value for :on_conflict, got: #{inspect other}"
end
end
defp replace_all_fields!(kind, nil, _to_remove) do
raise ArgumentError, "cannot use #{inspect(kind)} on operations without a schema"
end
defp replace_all_fields!(_kind, schema, to_remove) do
Enum.map(schema.__schema__(:fields) -- to_remove, &field_source!(schema, &1))
end
defp field_source!(nil, field) do
field
end
defp field_source!(schema, field) do
schema.__schema__(:field_source, field) ||
raise ArgumentError, "unknown field for :on_conflict, got: #{inspect(field)}"
end
defp on_conflict_query(query, from, prefix, counter_fun, adapter, conflict_target) do
{query, params, _} =
Ecto.Query.Planner.plan(%{query | prefix: prefix}, :update_all, adapter)
unless query.from.source == from do
raise ArgumentError, "cannot run on_conflict: query because the query " <>
"has a different {source, schema} pair than the " <>
"original struct/changeset/query. Got #{inspect query.from} " <>
"and #{inspect from} respectively"
end
{query, _} = Ecto.Query.Planner.normalize(query, :update_all, adapter, counter_fun.())
{query, params, conflict_target}
end
defp apply(_user_changeset, _adapter, :noop, _args) do
{:ok, []}
end
defp apply(user_changeset, adapter, action, args) do
case apply(adapter, action, args) do
{:ok, values} ->
{:ok, values}
{:invalid, constraints} ->
{:error, constraints_to_errors(user_changeset, action, constraints)}
{:error, :stale} ->
opts = List.last(args)
case Keyword.fetch(opts, :stale_error_field) do
{:ok, stale_error_field} when is_atom(stale_error_field) ->
stale_message = Keyword.get(opts, :stale_error_message, "is stale")
user_changeset = Changeset.add_error(user_changeset, stale_error_field, stale_message, [stale: true])
{:error, user_changeset}
_other ->
raise Ecto.StaleEntryError, struct: user_changeset.data, action: action
end
end
end
defp constraints_to_errors(%{constraints: user_constraints, errors: errors} = changeset, action, constraints) do
constraint_errors =
Enum.map constraints, fn {type, constraint} ->
user_constraint =
Enum.find(user_constraints, fn c ->
case {c.type, c.constraint, c.match} do
{^type, ^constraint, :exact} -> true
{^type, cc, :suffix} -> String.ends_with?(constraint, cc)
{^type, cc, :prefix} -> String.starts_with?(constraint, cc)
_ -> false
end
end)
case user_constraint do
%{field: field, error_message: error_message, error_type: error_type} ->
{field, {error_message, [constraint: error_type, constraint_name: constraint]}}
nil ->
raise Ecto.ConstraintError, action: action, type: type,
constraint: constraint, changeset: changeset
end
end
%{changeset | errors: constraint_errors ++ errors, valid?: false}
end
defp surface_changes(%{changes: changes, types: types} = changeset, struct, fields) do
{changes, errors} =
Enum.reduce fields, {changes, []}, fn field, {changes, errors} ->
case {struct, changes, types} do
# User has explicitly changed it
{_, %{^field => _}, _} ->
{changes, errors}
# Handle associations specially
{_, _, %{^field => {tag, embed_or_assoc}}} when tag in [:assoc, :embed] ->
# This is partly reimplementing the logic behind put_relation
# in Ecto.Changeset but we need to do it in a way where we have
# control over the current value.
value = Relation.load!(struct, Map.get(struct, field))
empty = Relation.empty(embed_or_assoc)
case Relation.change(embed_or_assoc, value, empty) do
{:ok, change, _} when change != empty ->
{Map.put(changes, field, change), errors}
{:error, error} ->
{changes, [{field, error}]}
_ -> # :ignore or ok with change == empty
{changes, errors}
end
# Struct has a non nil value
{%{^field => value}, _, %{^field => _}} when value != nil ->
{Map.put(changes, field, value), errors}
{_, _, _} ->
{changes, errors}
end
end
case errors do
[] -> %{changeset | changes: changes}
_ -> %{changeset | errors: errors ++ changeset.errors, valid?: false, changes: changes}
end
end
defp load_changes(changeset, state, types, values, embeds, autogen, adapter, schema_meta) do
%{data: data, changes: changes} = changeset
data =
data
|> merge_changes(changes)
|> Map.merge(embeds)
|> merge_autogen(autogen)
|> apply_metadata(state, schema_meta)
|> load_each(values, types, adapter)
Map.put(changeset, :data, data)
end
defp merge_changes(data, changes) do
changes =
Enum.reduce(changes, changes, fn {key, _value}, changes ->
if Map.has_key?(data, key), do: changes, else: Map.delete(changes, key)
end)
Map.merge(data, changes)
end
defp merge_autogen(data, autogen) do
Enum.reduce(autogen, data, fn {k, v}, acc -> %{acc | k => v} end)
end
defp apply_metadata(%{__meta__: meta} = data, state, %{source: source, prefix: prefix}) do
%{data | __meta__: %{meta | state: state, source: source, prefix: prefix}}
end
defp load_each(struct, [{_, value} | kv], [{key, type} | types], adapter) do
case Ecto.Type.adapter_load(adapter, type, value) do
{:ok, value} ->
load_each(%{struct | key => value}, kv, types, adapter)
:error ->
raise ArgumentError, "cannot load `#{inspect value}` as type #{inspect type} " <>
"for field `#{key}` in schema #{inspect struct.__struct__}"
end
end
defp load_each(struct, [], _types, _adapter) do
struct
end
defp pop_assocs(changeset, []) do
{changeset, [], []}
end
defp pop_assocs(%{changes: changes, types: types} = changeset, assocs) do
{changes, parent, child} =
Enum.reduce assocs, {changes, [], []}, fn assoc, {changes, parent, child} ->
case Map.fetch(changes, assoc) do
{:ok, value} ->
changes = Map.delete(changes, assoc)
case Map.fetch!(types, assoc) do
{:assoc, %{relationship: :parent} = refl} ->
{changes, [{refl, value}|parent], child}
{:assoc, %{relationship: :child} = refl} ->
{changes, parent, [{refl, value}|child]}
end
:error ->
{changes, parent, child}
end
end
{%{changeset | changes: changes}, parent, child}
end
# Don't mind computing options if there are no assocs
defp assoc_opts([], _opts), do: []
defp assoc_opts(_assocs, opts) do
Keyword.take(opts, [:timeout, :log, :telemetry_event, :prefix])
end
defp process_parents(%{changes: changes} = changeset, assocs, adapter, opts) do
case Ecto.Association.on_repo_change(changeset, assocs, adapter, opts) do
{:ok, struct} ->
changes = change_parents(changes, struct, assocs)
%{changeset | changes: changes, data: struct}
{:error, changes} ->
%{changeset | changes: changes, valid?: false}
end
end
defp change_parents(changes, struct, assocs) do
Enum.reduce assocs, changes, fn {refl, _}, acc ->
%{field: field, owner_key: owner_key, related_key: related_key} = refl
related = Map.get(struct, field)
value = related && Map.fetch!(related, related_key)
case Map.fetch(changes, owner_key) do
{:ok, current} when current != value ->
raise ArgumentError,
"cannot change belongs_to association `#{field}` because there is " <>
"already a change setting its foreign key `#{owner_key}` to `#{inspect current}`"
_ ->
Map.put(acc, owner_key, value)
end
end
end
defp process_children(changeset, assocs, user_changeset, adapter, opts) do
case Ecto.Association.on_repo_change(changeset, assocs, adapter, opts) do
{:ok, struct} -> {:ok, struct}
{:error, changes} ->
{:error, %{user_changeset | valid?: false, changes: changes}}
end
end
defp to_delete_assocs(schema) do
for assoc <- schema.__schema__(:associations),
reflection = schema.__schema__(:association, assoc),
match?(%{on_delete: on_delete} when on_delete != :nothing, reflection),
do: reflection
end
defp autogenerate_id(nil, changes, return_types, return_sources, _adapter) do
{changes, [], return_types, return_sources}
end
defp autogenerate_id({key, source, type}, changes, return_types, return_sources, adapter) do
cond do
Map.has_key?(changes, key) -> # Set by user
{changes, [], return_types, return_sources}
value = Ecto.Type.adapter_autogenerate(adapter, type) -> # Autogenerated now
{changes, [{source, value}], [{key, type} | return_types], return_sources}
true -> # Autogenerated in storage
{changes, [], [{key, type} | return_types], [source | List.delete(return_sources, source)]}
end
end
defp dump_changes!(action, changes, schema, extra, dumper, adapter) do
autogen = autogenerate_changes(schema, action, changes)
dumped =
dump_fields!(action, schema, changes, dumper, adapter) ++
dump_fields!(action, schema, autogen, dumper, adapter) ++
extra
{dumped, autogen}
end
defp autogenerate_changes(schema, action, changes) do
autogen_fields = action |> action_to_auto() |> schema.__schema__()
Enum.flat_map(autogen_fields, fn {fields, {mod, fun, args}} ->
case Enum.reject(fields, &Map.has_key?(changes, &1)) do
[] ->
[]
fields ->
generated = apply(mod, fun, args)
Enum.map(fields, &{&1, generated})
end
end)
end
defp action_to_auto(:insert), do: :autogenerate
defp action_to_auto(:update), do: :autoupdate
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(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fun) do
%{changes: changes} = changeset
changed = &Map.has_key?(changes, &1)
relations_changed? = Enum.any?(assocs, changed) or Enum.any?(embeds, changed)
wrap_in_transaction(adapter, adapter_meta, opts, relations_changed?, prepare, fun)
end
defp wrap_in_transaction(adapter, adapter_meta, opts, relations_changed?, prepare, fun) do
if (relations_changed? or prepare != []) and
function_exported?(adapter, :transaction, 3) and
not adapter.in_transaction?(adapter_meta) do
adapter.transaction(adapter_meta, opts, fn ->
case fun.() do
{:ok, struct} -> struct
{:error, changeset} -> adapter.rollback(adapter_meta, changeset)
end
end)
else
fun.()
end
end
defp dump_field!(action, schema, field, type, value, adapter) do
case Ecto.Type.adapter_dump(adapter, type, value) do
{:ok, value} ->
value
:error ->
raise Ecto.ChangeError,
"value `#{inspect(value)}` for `#{inspect(schema)}.#{field}` " <>
"in `#{action}` does not match type #{inspect type}"
end
end
defp dump_fields!(action, schema, kw, dumper, adapter) do
for {field, value} <- kw do
{alias, type} = Map.fetch!(dumper, field)
{alias, dump_field!(action, schema, field, type, value, adapter)}
end
end
end