Packages
ecto
0.12.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/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 model.
"""
@spec query([list], Ecto.Repo.t, Ecto.Query.t, fun) :: [list]
def query([], _repo, _query, _fun), do: []
def query(rows, _repo, %{preloads: []}, fun), do: Enum.map(rows, fun)
def query(rows, repo, query, fun) do
rows
|> extract
|> do_preload(repo, query.preloads, query.assocs)
|> 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(models, atom, atom | list) :: models when models: [Ecto.Model.t] | Ecto.Model.t
def preload(structs, repo, preloads) when is_list(structs) do
do_preload(structs, repo, preloads, nil)
end
def preload(struct, repo, preloads) when is_map(struct) do
do_preload([struct], repo, preloads, nil) |> hd()
end
defp do_preload(structs, repo, preloads, assocs) do
preloads = normalize(preloads, assocs, preloads)
preload_each(structs, repo, preloads)
rescue
e ->
# Reraise errors so we ignore the preload inner stacktrace
raise e
end
## Preloading
defp preload_each(structs, _repo, []), do: structs
defp preload_each([], _repo, _preloads), do: []
defp preload_each(structs, repo, preloads) do
module = hd(structs).__struct__
preloads = expand(module, preloads, [])
entries =
Enum.map preloads, fn
{_, {:assoc, assoc, assoc_key}, sub_preloads} ->
preload_assoc(structs, module, repo, assoc, assoc_key, sub_preloads)
{_, {:through, _, _} = info, []} ->
info
end
for struct <- structs do
Enum.reduce entries, struct, fn {kind, assoc, data}, acc ->
cond do
loaded?(acc, assoc.field) ->
acc
kind == :assoc ->
load_assoc(acc, assoc, data)
kind == :through ->
load_through(acc, assoc, data)
end
end
end
end
## Association preloading
defp preload_assoc(structs, module, repo, assoc, assoc_key, sub_preloads) do
case ids(structs, module, assoc) do
[] ->
{:assoc, assoc, HashDict.new}
ids ->
query = assoc.__struct__.assoc_query(assoc, ids)
card = assoc.cardinality
if card == :many do
query = Ecto.Query.from q in query, order_by: field(q, ^assoc_key)
end
loaded = preload_each(repo.all(query), repo, sub_preloads)
{:assoc, assoc, assoc_dict(card, assoc_key, loaded)}
end
end
def ids(structs, module, assoc) do
field = assoc.field
owner_key = assoc.owner_key
for struct <- structs,
assert_struct!(module, struct),
not loaded?(struct, field),
key = Map.fetch!(struct, owner_key),
do: key
end
defp loaded?(struct, field) do
case Map.get(struct, field) do
%Ecto.Association.NotLoaded{} -> false
_ -> true
end
end
defp assert_struct!(model, %{__struct__: struct}) do
if struct != model do
raise ArgumentError, "expected a homogeneous list containing the same struct, " <>
"got: #{inspect model} and #{inspect struct}"
else
true
end
end
defp assoc_dict(:one, key, structs) do
Enum.reduce structs, HashDict.new, fn x, acc ->
HashDict.put(acc, Map.fetch!(x, key), x)
end
end
defp assoc_dict(:many, key, structs) do
many_assoc_dict(structs, key, HashDict.new)
end
defp many_assoc_dict([], _key, dict) do
dict
end
defp many_assoc_dict([h|t], key, dict) do
current = Map.fetch!(h, key)
{t1, t2} = Enum.split_while(t, &(Map.fetch!(&1, key) == current))
many_assoc_dict(t2, key, HashDict.put(dict, current, [h|t1]))
end
## Load preloaded data
defp load_assoc(struct, assoc, dict) do
key = Map.fetch!(struct, assoc.owner_key)
loaded =
cond do
value = HashDict.get(dict, key) -> value
assoc.cardinality == :many -> []
true -> nil
end
Map.put(struct, assoc.field, loaded)
end
defp load_through(struct, assoc, [h|t]) do
initial = struct |> Map.fetch!(h) |> List.wrap
loaded = Enum.reduce(t, initial, &recur_through/2)
if assoc.cardinality == :one do
loaded = List.first(loaded)
end
Map.put(struct, assoc.field, loaded)
end
defp recur_through(assoc, structs) do
Enum.reduce(structs, {[], HashSet.new}, fn struct, acc ->
children = struct |> Map.fetch!(assoc) |> List.wrap
Enum.reduce children, acc, fn child, {fresh, set} ->
[{_, pk}] = Ecto.Model.primary_key!(child)
pk || raise Ecto.MissingPrimaryKeyError, struct: child
if HashSet.member?(set, pk) do
{fresh, set}
else
{[child|fresh], HashSet.put(set, pk)}
end
end
end) |> elem(0) |> Enum.reverse()
end
## Normalizer
def normalize(preload, assocs, original) do
normalize_each(List.wrap(preload), [], assocs, original)
end
defp normalize_each({atom, list}, acc, assocs, original) when is_atom(atom) do
no_assoc!(assocs, atom)
[{atom, normalize_each(List.wrap(list), [], assocs, original)}|acc]
end
defp normalize_each(atom, acc, assocs, _original) when is_atom(atom) do
no_assoc!(assocs, atom)
[{atom, []}|acc]
end
defp normalize_each(list, acc, assocs, original) when is_list(list) do
Enum.reduce(list, acc, &normalize_each(&1, &2, assocs, original))
end
defp normalize_each(other, _, _assocs, 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
defp no_assoc!(nil, _atom), do: nil
defp no_assoc!(assocs, atom) do
if assocs[atom] do
raise ArgumentError, "cannot preload association `#{inspect atom}` because " <>
"it has already been loaded with join association"
end
end
## Expand
def expand(model, preloads, acc) do
Enum.reduce(preloads, acc, fn {preload, sub_preloads}, acc ->
case List.keyfind(acc, preload, 0) do
{^preload, info, extra_preloads} ->
List.keyreplace(acc, preload, 0, {preload, info, sub_preloads ++ extra_preloads})
nil ->
assoc = Ecto.Association.association_from_model!(model, preload)
info = assoc.__struct__.preload_info(assoc)
case info do
{:assoc, _, _} ->
[{preload, info, sub_preloads}|acc]
{:through, _, through} ->
through = through |> Enum.reverse |> Enum.reduce(sub_preloads, &[{&1, &2}])
List.keystore(expand(model, through, acc), preload, 0, {preload, info, []})
end
end
end)
end
end