Packages
ecto
2.1.3
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/preloader.ex
defmodule Ecto.Repo.Preloader do
# The module invoked by user defined repos
# for preload related functionality.
@moduledoc false
require Ecto.Query
@doc """
Transforms a result set based on query preloads, loading
the associations onto their parent schema.
"""
@spec query([list], Ecto.Repo.t, list, Access.t, fun, Keyword.t) :: [list]
def query([], _repo, _preloads, _take, _fun, _opts), do: []
def query(rows, _repo, [], _take, fun, _opts), do: Enum.map(rows, fun)
def query(rows, repo, preloads, take, fun, opts) do
rows
|> extract
|> do_preload(repo, preloads, take, opts)
|> unextract(rows, fun)
end
defp extract([[nil|_]|t2]), do: extract(t2)
defp extract([[h|_]|t2]), do: [h|extract(t2)]
defp extract([]), do: []
defp unextract(structs, [[nil|_] = h2|t2], fun), do: [fun.(h2)|unextract(structs, t2, fun)]
defp unextract([h1|structs], [[_|t1]|t2], fun), do: [fun.([h1|t1])|unextract(structs, t2, fun)]
defp unextract([], [], _fun), do: []
@doc """
Implementation for `Ecto.Repo.preload/2`.
"""
@spec preload(structs, atom, atom | list, Keyword.t) ::
structs when structs: [Ecto.Schema.t] | Ecto.Schema.t
def preload(structs, repo, preloads, opts) when is_list(structs) do
do_preload(structs, repo, preloads, opts[:take], opts)
end
def preload(struct, repo, preloads, opts) when is_map(struct) do
do_preload([struct], repo, preloads, opts[:take], opts) |> hd()
end
defp do_preload(structs, repo, preloads, take, opts) do
preloads = normalize(preloads, take, preloads)
preload_each(structs, repo, preloads, opts)
rescue
e ->
# Reraise errors so we ignore the preload inner stacktrace
reraise e
end
## Preloading
defp preload_each(structs, _repo, [], _opts), do: structs
defp preload_each([], _repo, _preloads, _opts), do: []
defp preload_each([sample|_] = structs, repo, preloads, opts) do
module = sample.__struct__
prefix = preload_prefix(opts, sample)
{assocs, throughs} = expand(module, preloads, {%{}, %{}})
assocs =
maybe_pmap Map.values(assocs), repo, opts, fn
{{:assoc, assoc, related_key}, take, query, sub_preloads}, opts ->
preload_assoc(structs, module, repo, prefix, assoc, related_key,
query, sub_preloads, take, opts)
end
throughs =
Map.values(throughs)
for struct <- structs do
struct = Enum.reduce assocs, struct, &load_assoc/2
struct = Enum.reduce throughs, struct, &load_through/2
struct
end
end
defp preload_prefix(opts, sample) do
case Keyword.fetch(opts, :prefix) do
{:ok, prefix} ->
prefix
:error ->
%{__meta__: %{source: {prefix, _}}} = sample
prefix
end
end
## Association preloading
defp maybe_pmap(assocs, repo, opts, fun) do
if match?([_,_|_], assocs) and not repo.in_transaction? and
Keyword.get(opts, :in_parallel, true) do
# We pass caller: self() so pools like the ownership
# pool knows where to fetch the connection from and
# set the proper timeouts.
opts = Keyword.put_new(opts, :caller, self())
assocs
|> Enum.map(&Task.async(:erlang, :apply, [fun, [&1, opts]]))
|> Enum.map(&Task.await(&1, :infinity))
else
Enum.map(assocs, &fun.(&1, opts))
end
end
defp preload_assoc(structs, module, repo, prefix, %{cardinality: card} = assoc,
related_key, query, preloads, take, opts) do
{fetch_ids, loaded_ids, loaded_structs} =
fetch_ids(structs, module, assoc, opts)
{fetch_ids, fetch_structs} =
fetch_query(fetch_ids, assoc, repo, query, prefix, related_key, take, opts)
all = preload_each(Enum.reverse(loaded_structs, fetch_structs), repo, preloads, opts)
{:assoc, assoc, assoc_map(card, Enum.reverse(loaded_ids, fetch_ids), all)}
end
defp fetch_ids(structs, module, assoc, opts) do
%{field: field, owner_key: owner_key, cardinality: card} = assoc
force? = Keyword.get(opts, :force, false)
Enum.reduce structs, {[], [], []}, fn struct, {fetch_ids, loaded_ids, loaded_structs} ->
assert_struct!(module, struct)
%{^owner_key => id, ^field => value} = struct
cond do
card == :one and not is_nil(value) and Ecto.assoc_loaded?(value) and not force? ->
{fetch_ids, [id|loaded_ids], [value|loaded_structs]}
card == :many and Ecto.assoc_loaded?(value) and not force? ->
{fetch_ids,
List.duplicate(id, length(value)) ++ loaded_ids,
value ++ loaded_structs}
is_nil(id) ->
{fetch_ids, loaded_ids, loaded_structs}
true ->
{[id|fetch_ids], loaded_ids, loaded_structs}
end
end
end
defp fetch_query([], _assoc, _repo, _query, _prefix, _related_key, _take, _opts) do
{[], []}
end
defp fetch_query(ids, _assoc, _repo, query, _prefix, {_, key}, _take, _opts) when is_function(query, 1) do
data = ids |> Enum.uniq |> query.() |> Enum.map(&{Map.fetch!(&1, key), &1}) |> Enum.sort
unzip_ids data, [], []
end
defp fetch_query(ids, %{cardinality: card} = assoc, repo, query, prefix, related_key, take, opts) do
query = assoc.__struct__.assoc_query(assoc, query, Enum.uniq(ids))
field = related_key_to_field(query, related_key)
# Normalize query
query = %{Ecto.Query.Planner.returning(query, take || true) | prefix: prefix}
# Add the related key to the query results
query = update_in query.select.expr, &{:{}, [], [field, &1]}
# If we are returning many results, we must sort by the key too
query =
case card do
:many ->
update_in query.order_bys, fn order_bys ->
[%Ecto.Query.QueryExpr{expr: [asc: field], params: [],
file: __ENV__.file, line: __ENV__.line}|order_bys]
end
:one ->
query
end
unzip_ids repo.all(query, opts), [], []
end
defp related_key_to_field(query, {pos, key}) do
{{:., [], [{:&, [], [related_key_pos(query, pos)]}, key]}, [], []}
end
defp related_key_pos(_query, pos) when pos >= 0, do: pos
defp related_key_pos(query, pos), do: Ecto.Query.Builder.count_binds(query) + pos
defp unzip_ids([{k, v}|t], acc1, acc2), do: unzip_ids(t, [k|acc1], [v|acc2])
defp unzip_ids([], acc1, acc2), do: {acc1, acc2}
defp assert_struct!(mod, %{__struct__: mod}), do: true
defp assert_struct!(mod, %{__struct__: struct}) do
raise ArgumentError, "expected a homogeneous list containing the same struct, " <>
"got: #{inspect mod} and #{inspect struct}"
end
defp assoc_map(:one, ids, structs) do
one_assoc_map(ids, structs, %{})
end
defp assoc_map(:many, ids, structs) do
many_assoc_map(ids, structs, %{})
end
defp one_assoc_map([id|ids], [struct|structs], map) do
one_assoc_map(ids, structs, Map.put(map, id, struct))
end
defp one_assoc_map([], [], map) do
map
end
defp many_assoc_map([id|ids], [struct|structs], map) do
{ids, structs, acc} = split_while(ids, structs, id, [struct])
many_assoc_map(ids, structs, Map.put(map, id, acc))
end
defp many_assoc_map([], [], map) do
map
end
defp split_while([id|ids], [struct|structs], id, acc),
do: split_while(ids, structs, id, [struct|acc])
defp split_while(ids, structs, _id, acc),
do: {ids, structs, acc}
## Load preloaded data
defp load_assoc({:assoc, assoc, ids}, struct) do
%{field: field, owner_key: owner_key, cardinality: cardinality} = assoc
key = Map.fetch!(struct, owner_key)
loaded =
case ids do
%{^key => value} -> value
_ when cardinality == :many -> []
_ -> nil
end
Map.put(struct, field, loaded)
end
defp load_through({:through, assoc, throughs}, struct) do
%{cardinality: cardinality, field: field, owner: owner} = assoc
{loaded, _} = Enum.reduce(throughs, {[struct], owner}, &recur_through/2)
Map.put(struct, field, maybe_first(loaded, cardinality))
end
defp maybe_first(list, :one), do: List.first(list)
defp maybe_first(list, _), do: list
defp recur_through(field, {structs, owner}) do
assoc = owner.__schema__(:association, field)
case assoc.__struct__.preload_info(assoc) do
{:assoc, %{related: related}, _} ->
pks = related.__schema__(:primary_key)
{children, _} =
Enum.reduce(structs, {[], %{}}, fn struct, acc ->
children = struct |> Map.fetch!(field) |> List.wrap
Enum.reduce children, acc, fn child, {fresh, set} ->
keys = through_pks(child, pks, assoc)
case set do
%{^keys => true} ->
{fresh, set}
_ ->
{[child|fresh], Map.put(set, keys, true)}
end
end
end)
{Enum.reverse(children), related}
{:through, _, through} ->
Enum.reduce(through, {structs, owner}, &recur_through/2)
end
end
defp through_pks(map, pks, assoc) do
Enum.map pks, fn pk ->
case map do
%{^pk => value} -> value
_ ->
raise ArgumentError,
"cannot preload through association `#{assoc.field}` on `#{inspect assoc.owner}`. " <>
"Ecto expected a map/struct with the key `#{pk}` but got: #{inspect map}"
end
end
end
## Normalizer
def normalize(preload, take, original) do
normalize_each(wrap(preload, original), [], take, original)
end
defp normalize_each({atom, {query, list}}, acc, take, original)
when is_atom(atom) and (is_map(query) or is_function(query, 1)) do
fields = take(take, atom)
[{atom, {fields, query!(query), normalize_each(wrap(list, original), [], fields, original)}}|acc]
end
defp normalize_each({atom, query}, acc, take, _original)
when is_atom(atom) and (is_map(query) or is_function(query, 1)) do
[{atom, {take(take, atom), query!(query), []}}|acc]
end
defp normalize_each({atom, list}, acc, take, original) when is_atom(atom) do
fields = take(take, atom)
[{atom, {fields, nil, normalize_each(wrap(list, original), [], fields, original)}}|acc]
end
defp normalize_each(atom, acc, take, _original) when is_atom(atom) do
[{atom, {take(take, atom), nil, []}}|acc]
end
defp normalize_each(other, acc, take, original) do
Enum.reduce(wrap(other, original), acc, &normalize_each(&1, &2, take, original))
end
defp query!(query) when is_function(query, 1), do: query
defp query!(%Ecto.Query{} = query), do: query
defp take(take, field) do
case Access.fetch(take, field) do
{:ok, fields} -> List.wrap(fields)
:error -> nil
end
end
defp wrap(list, _original) when is_list(list),
do: list
defp wrap(atom, _original) when is_atom(atom),
do: atom
defp wrap(other, original) do
raise ArgumentError, "invalid preload `#{inspect other}` in `#{inspect original}`. " <>
"preload expects an atom, a (nested) keyword or a (nested) list of atoms"
end
## Expand
def expand(schema, preloads, acc) do
Enum.reduce(preloads, acc, fn {preload, {fields, query, sub_preloads}}, {assocs, throughs} ->
assoc = Ecto.Association.association_from_schema!(schema, preload)
info = assoc.__struct__.preload_info(assoc)
case info do
{:assoc, _, _} ->
value = {info, fields, query, sub_preloads}
assocs = Map.update(assocs, preload, value, &merge_preloads(preload, value, &1))
{assocs, throughs}
{:through, _, through} ->
through =
through
|> Enum.reverse()
|> Enum.reduce({fields, query, sub_preloads}, &{nil, nil, [{&1, &2}]})
|> elem(2)
expand(schema, through, {assocs, Map.put(throughs, preload, info)})
end
end)
end
defp merge_preloads(_preload, {info, _, nil, left}, {info, take, query, right}),
do: {info, take, query, left ++ right}
defp merge_preloads(_preload, {info, take, query, left}, {info, _, nil, right}),
do: {info, take, query, left ++ right}
defp merge_preloads(preload, {info, _, left, _}, {info, _, right, _}) do
raise ArgumentError, "cannot preload `#{preload}` as it has been supplied more than once " <>
"with different queries: #{inspect left} and #{inspect right}"
end
defp reraise(exception) do
reraise exception, Enum.reject(System.stacktrace, &match?({__MODULE__, _, _, _}, &1))
end
end