defmodule QueryBuilder.Query.Preload do @moduledoc false require Ecto.Query def preload(query, _assoc_list, []), do: query def preload(ecto_query, assoc_list) do flattened_assoc_data = flatten_assoc_data(assoc_list) # Firstly, give `Ecto.Query.preload/3` the list of associations that have been joined, such as: # `Ecto.Query.preload(query, [articles: a, user: u, role: r], [articles: {a, [user: {u, [role: r]}]}])` ecto_query = flattened_assoc_data # Filter only the associations that have been joined |> Enum.map(fn assoc_data_list -> Enum.flat_map(assoc_data_list, fn %{has_joined: false} -> [] assoc_data -> [{assoc_data.assoc_binding, assoc_data.assoc_field}] end) end) # Get rid of the associations' lists that are redundant; # for example for the 4 lists below: # `[{:binding1, :field1}]` # `[{:binding1, :field1}, {:binding2, :field2}]` # `[{:binding1, :field1}, {:binding2, :field2}]` # `[{:binding1, :field1}, {:binding2, :field2}, {:binding3, :field3}]` # only the last list should be preserved. |> Enum.uniq() |> (fn lists -> Enum.filter( lists, &(!Enum.any?(lists -- [&1], fn list -> Keyword.equal?(&1, Enum.slice(list, 0, length(&1))) end)) ) end).() |> Enum.reduce(ecto_query, fn list, ecto_query -> do_preload_with_bindings(ecto_query, list) end) # Secondly, give `Ecto.Query.preload/3` the list of associations that have not # been joined, such as: # `Ecto.Query.preload(query, [articles: [comments: :comment_likes]])` ecto_query = flattened_assoc_data |> Enum.map(fn assoc_data_list -> Enum.reverse(assoc_data_list) |> Enum.drop_while(& &1.has_joined) |> Enum.map(& &1.assoc_field) |> Enum.reverse() end) |> Enum.reject(&Enum.empty?(&1)) |> Enum.map(&convert_list_to_nested_keyword_list(&1)) |> Enum.reduce(ecto_query, fn list, ecto_query -> atom_or_tuple = hd(list) Ecto.Query.preload(ecto_query, ^atom_or_tuple) end) ecto_query end defp flatten_assoc_data(assoc_list) do assoc_list |> Enum.flat_map(&do_flatten_assoc_data/1) |> Enum.filter(&(!is_nil(&1))) end defp do_flatten_assoc_data(%{nested_assocs: [], preload: preload} = assoc_data) do if preload do [[Map.delete(assoc_data, :nested_assocs)]] else [] end end defp do_flatten_assoc_data(%{nested_assocs: nested_assocs, preload: preload} = assoc_data) do for nested_assoc_data <- nested_assocs, rest <- do_flatten_assoc_data(nested_assoc_data) do if preload do [Map.delete(assoc_data, :nested_assocs) | rest] else rest end end end defp convert_list_to_nested_keyword_list(list) do do_convert_list_to_nested_keyword_list(list) |> List.wrap() end defp do_convert_list_to_nested_keyword_list([]), do: [] defp do_convert_list_to_nested_keyword_list([e]), do: e defp do_convert_list_to_nested_keyword_list([head | [penultimate, last]]), do: [{head, [{penultimate, last}]}] defp do_convert_list_to_nested_keyword_list([head | tail]), do: [{head, do_convert_list_to_nested_keyword_list(tail)}] defp do_preload_with_bindings(query, []), do: query defp do_preload_with_bindings(query, [{assoc_binding, assoc_field}]) do Ecto.Query.preload(query, [{^assoc_binding, x}], [ {^assoc_field, x} ]) end defp do_preload_with_bindings(query, [ {assoc_binding1, assoc_field1}, {assoc_binding2, assoc_field2} ]) do Ecto.Query.preload(query, [{^assoc_binding1, x}, {^assoc_binding2, y}], [ {^assoc_field1, {x, [{^assoc_field2, y}]}} ]) end defp do_preload_with_bindings(query, [ {assoc_binding1, assoc_field1}, {assoc_binding2, assoc_field2}, {assoc_binding3, assoc_field3} ]) do Ecto.Query.preload( query, [{^assoc_binding1, x}, {^assoc_binding2, y}, {^assoc_binding3, z}], [ {^assoc_field1, {x, [{^assoc_field2, {y, [{^assoc_field3, z}]}}]}} ] ) end defp do_preload_with_bindings(query, [ {assoc_binding1, assoc_field1}, {assoc_binding2, assoc_field2}, {assoc_binding3, assoc_field3}, {assoc_binding4, assoc_field4} ]) do Ecto.Query.preload( query, [{^assoc_binding1, x}, {^assoc_binding2, y}, {^assoc_binding3, z}, {^assoc_binding4, a}], [ {^assoc_field1, {x, [{^assoc_field2, {y, [{^assoc_field3, {z, [{^assoc_field4, a}]}}]}}]}} ] ) end defp do_preload_with_bindings(query, [ {assoc_binding1, assoc_field1}, {assoc_binding2, assoc_field2}, {assoc_binding3, assoc_field3}, {assoc_binding4, assoc_field4}, {assoc_binding5, assoc_field5} ]) do Ecto.Query.preload( query, [ {^assoc_binding1, x}, {^assoc_binding2, y}, {^assoc_binding3, z}, {^assoc_binding4, a}, {^assoc_binding5, b} ], [ {^assoc_field1, {x, [ {^assoc_field2, {y, [{^assoc_field3, {z, [{^assoc_field4, {a, [{^assoc_field5, b}]}}]}}]}} ]}} ] ) end defp do_preload_with_bindings(query, [ {assoc_binding1, assoc_field1}, {assoc_binding2, assoc_field2}, {assoc_binding3, assoc_field3}, {assoc_binding4, assoc_field4}, {assoc_binding5, assoc_field5}, {assoc_binding6, assoc_field6} ]) do Ecto.Query.preload( query, [ {^assoc_binding1, x}, {^assoc_binding2, y}, {^assoc_binding3, z}, {^assoc_binding4, a}, {^assoc_binding5, b}, {^assoc_binding6, c} ], [ {^assoc_field1, {x, [ {^assoc_field2, {y, [ {^assoc_field3, {z, [{^assoc_field4, {a, [{^assoc_field5, {b, [{^assoc_field6, c}]}}]}}]}} ]}} ]}} ] ) end end