Packages
ecto
3.0.0-rc.0
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/embedded.ex
defmodule Ecto.Embedded do
@moduledoc false
alias __MODULE__
alias Ecto.Changeset
@type t :: %Embedded{cardinality: :one | :many,
on_replace: :raise | :mark_as_invalid | :delete,
field: atom,
owner: atom,
on_cast: nil | fun,
related: atom,
unique: boolean}
@behaviour Ecto.Changeset.Relation
@on_replace_opts [:raise, :mark_as_invalid, :delete]
@embeds_one_on_replace_opts @on_replace_opts ++ [:update]
defstruct [:cardinality, :field, :owner, :related, :on_cast, on_replace: :raise, unique: true]
@doc """
Builds the embedded struct.
## Options
* `:cardinality` - tells if there is one embedded schema or many
* `:related` - name of the embedded schema
* `:on_replace` - the action taken on embeds when the embed is replaced
"""
def struct(module, name, opts) do
opts = Keyword.put_new(opts, :on_replace, :raise)
cardinality = Keyword.fetch!(opts, :cardinality)
on_replace_opts = if cardinality == :one, do: @embeds_one_on_replace_opts, else: @on_replace_opts
unless opts[: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
struct(%Embedded{field: name, owner: module}, opts)
end
@doc """
Callback invoked by repository to prepare embeds.
It replaces the changesets for embeds inside changes
by actual structs so it can be dumped by adapters and
loaded into the schema struct afterwards.
"""
def prepare(changeset, embeds, adapter, repo_action) do
%{changes: changes, types: types, repo: repo} = changeset
prepare(Map.take(changes, embeds), types, adapter, repo, repo_action)
end
defp prepare(embeds, _types, _adapter, _repo, _repo_action) when embeds == %{} do
embeds
end
defp prepare(embeds, types, adapter, repo, repo_action) do
Enum.reduce embeds, embeds, fn {name, changeset_or_changesets}, acc ->
{:embed, embed} = Map.get(types, name)
Map.put(acc, name, prepare_each(embed, changeset_or_changesets, adapter, repo, repo_action))
end
end
defp prepare_each(%{cardinality: :one}, nil, _adapter, _repo, _repo_action) do
nil
end
defp prepare_each(%{cardinality: :one} = embed, changeset, adapter, repo, repo_action) do
action = check_action!(changeset.action, repo_action, embed)
changeset = run_prepare(changeset, repo)
to_struct(changeset, action, embed, adapter)
end
defp prepare_each(%{cardinality: :many} = embed, changesets, adapter, repo, repo_action) do
for changeset <- changesets,
action = check_action!(changeset.action, repo_action, embed),
changeset = run_prepare(changeset, repo),
prepared = to_struct(changeset, action, embed, adapter),
do: prepared
end
defp to_struct(%Changeset{valid?: false}, _action,
%{related: schema}, _adapter) do
raise ArgumentError, "changeset for embedded #{inspect schema} is invalid, " <>
"but the parent changeset was not marked as invalid"
end
defp to_struct(%Changeset{data: %{__struct__: actual}}, _action,
%{related: expected}, _adapter) when actual != expected do
raise ArgumentError, "expected changeset for embedded schema `#{inspect expected}`, " <>
"got: #{inspect actual}"
end
defp to_struct(%Changeset{changes: changes, data: schema}, :update,
_embed, _adapter) when changes == %{} do
schema
end
defp to_struct(%Changeset{}, :delete, _embed, _adapter) do
nil
end
defp to_struct(%Changeset{} = changeset, action, %{related: schema}, adapter) do
%{data: struct, changes: changes} = changeset
embeds = prepare(changeset, schema.__schema__(:embeds), adapter, action)
changes
|> Map.merge(embeds)
|> autogenerate_id(struct, action, schema, adapter)
|> autogenerate(action, schema)
|> apply_embeds(struct)
end
defp run_prepare(changeset, repo) do
changeset = %{changeset | repo: repo}
Enum.reduce(Enum.reverse(changeset.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 apply_embeds(changes, struct) do
struct(struct, changes)
end
defp check_action!(:replace, action, %{on_replace: :delete} = embed),
do: check_action!(:delete, action, embed)
defp check_action!(:update, :insert, %{related: schema}),
do: raise(ArgumentError, "got action :update in changeset for embedded #{inspect schema} while inserting")
defp check_action!(:delete, :insert, %{related: schema}),
do: raise(ArgumentError, "got action :delete in changeset for embedded #{inspect schema} while inserting")
defp check_action!(action, _, _), do: action
defp autogenerate_id(changes, _struct, :insert, schema, adapter) do
case schema.__schema__(:autogenerate_id) do
{key, _source, :binary_id} ->
Map.put_new_lazy(changes, key, fn -> adapter.autogenerate(:embed_id) end)
{_key, :id} ->
raise ArgumentError, "embedded schema `#{inspect schema}` cannot autogenerate `:id` primary keys, " <>
"those are typically used for auto-incrementing constraints. " <>
"Maybe you meant to use `:binary_id` instead?"
nil ->
changes
end
end
defp autogenerate_id(changes, struct, :update, _schema, _adapter) do
for {_, nil} <- Ecto.primary_key(struct) do
raise Ecto.NoPrimaryKeyValueError, struct: struct
end
changes
end
defp autogenerate(changes, action, schema) do
autogen_fields = action |> action_to_auto() |> schema.__schema__()
Enum.reduce(autogen_fields, changes, fn {fields, {mod, fun, args}}, acc ->
case Enum.reject(fields, &Map.has_key?(changes, &1)) do
[] ->
acc
fields ->
generated = apply(mod, fun, args)
Enum.reduce(fields, acc, &Map.put(&2, &1, generated))
end
end)
end
defp action_to_auto(:insert), do: :autogenerate
defp action_to_auto(:update), do: :autoupdate
@impl true
def build(%Embedded{related: related}) do
related.__struct__
end
end