Current section

Files

Jump to
ecto lib ecto associations assoc.ex
Raw

lib/ecto/associations/assoc.ex

defmodule Ecto.Associations.Assoc do
@moduledoc """
This module provides the assoc selector merger and utilities around it.
"""
alias Ecto.Query.QueryExpr
alias Ecto.Query.Util
@doc """
Transforms a result set based on the assoc selector, loading the associations
onto their parent model. See `Ecto.Query.select/3`.
"""
@spec run([Ecto.Model.t], Ecto.Query.t) :: [Ecto.Model.t]
def run([], _query), do: []
def run(results, query) do
case query.select do
%QueryExpr{expr: {:assoc, _, [parent, fields]}} ->
merge(results, parent, fields, query)
_ ->
results
end
end
@doc """
Decomposes an `assoc(var, fields)` or `var` into `{var, fields}`.
"""
@spec decompose_assoc(Macro.t) :: {Macro.t, [Macro.t]}
def decompose_assoc({:&, _, [_]} = var), do: {var, []}
def decompose_assoc({:assoc, _, [var, fields]}), do: {var, fields}
defp merge(rows, var, fields, query) do
# Pre-create rose tree of reflections and accumulator dicts in the same
# structure as the fields tree
refls = create_refls(var, fields, query)
{_, _, acc} = create_acc(fields)
acc = {HashSet.new, [], acc}
# Populate tree of dicts of associated entities from the result set
{_keys, parents, children} = Enum.reduce(rows, acc, &merge_to_dict(&1, {nil, refls}, &2))
# Load associated entities onto their parents
parents = for parent <- parents, do: build_struct({0, parent}, children, refls) |> elem(1)
Enum.reverse(parents)
end
defp merge_to_dict({struct, sub_structs}, {refl, sub_refls}, {keys, dict, sub_dicts}) do
# We recurse down the tree of the row result, the reflections and the
# dict accumulators
if struct do
module = struct.__struct__
pk_field = module.__schema__(:primary_key)
pk_value = Map.get(struct, pk_field)
end
# The set makes sure that we don't add duplicated associated entities
if struct && not Set.member?(keys, pk_value) do
keys = Set.put(keys, pk_value)
if refl do
# Add associated model to dict with association key, we use to
# put the model on the right parent later
# Also store position so we can sort
assoc_key = Map.get(struct, refl.assoc_key)
item = {Dict.size(dict), struct}
dict = Dict.update(dict, assoc_key, [item], &[item|&1])
else
# If no reflection we are at the top-most parent
dict = [struct|dict]
end
end
# Recurse down
zipped = List.zip([sub_structs, sub_refls, sub_dicts])
sub_dicts = for {recs, refls, dicts} <- zipped do
merge_to_dict(recs, refls, dicts)
end
{keys, dict, sub_dicts}
end
defp build_struct({pos, parent}, children, refls) do
zipped = List.zip([children, refls])
# Load all associated children onto the parent
new_parent =
Enum.reduce(zipped, parent, fn {child, refl}, parent ->
{refl, refls} = refl
{_, children, sub_children} = child
# Get the children associated to the parent
struct_key = Map.get(parent, refl.key)
if struct_key do
my_children = Dict.get(children, struct_key) || []
# Recurse down and build the children
built_children = for child <- my_children, do: build_struct(child, sub_children, refls)
else
built_children = []
end
# Fix ordering that was shuffled by HashDict
sorted_children = built_children
|> Enum.sort(&compare/2)
|> Enum.map(&elem(&1, 1))
set_loaded(parent, refl, sorted_children)
end)
{pos, new_parent}
end
defp create_refls(var, fields, query) do
Enum.map(fields, fn {field, nested} ->
{inner_var, fields} = decompose_assoc(nested)
model = Util.find_source(query.sources, var) |> Util.model
refl = model.__schema__(:association, field)
{refl, create_refls(inner_var, fields, query)}
end)
end
defp create_acc(fields) do
acc = Enum.map(fields, fn {_field, nested} ->
{_, fields} = decompose_assoc(nested)
create_acc(fields)
end)
{HashSet.new, HashDict.new, acc}
end
defp compare({pos1, _}, {pos2, _}), do: pos1 < pos2
defp set_loaded(struct, refl, loaded) do
unless refl.__struct__ == Ecto.Reflections.HasMany do
loaded = List.first(loaded)
end
Ecto.Associations.load(struct, refl.field, loaded)
end
end