Current section

Files

Jump to
outerfaces_ex_ecto_api lib query_engine query_joiner.ex
Raw

lib/query_engine/query_joiner.ex

defmodule OuterfacesEctoApi.QueryEngine.QueryJoiner do
@moduledoc """
Provides macros for dynamically constructing Ecto queries with filters and nested joins.
"""
import Ecto.Query
@spec ensure_joins(Ecto.Query.t(), list({atom(), module(), atom(), atom()})) :: Ecto.Query.t()
defmacro ensure_joins(query, chain) do
quote do
Enum.reduce(unquote(chain), {unquote(query), nil}, fn
{assoc_name, related_schema, owner_key, related_key}, {acc_query, parent_alias} ->
expected_alias =
if parent_alias, do: String.to_atom("#{parent_alias}_#{assoc_name}"), else: assoc_name
already_joined? = Enum.any?(acc_query.joins, fn j -> j.as == expected_alias end)
if already_joined? do
{acc_query, expected_alias}
else
new_query =
OuterfacesEctoApi.QueryEngine.QueryJoiner.do_join(
acc_query,
assoc_name,
related_schema,
owner_key,
related_key,
parent_alias
)
{new_query, expected_alias}
end
end)
|> elem(0)
end
end
@spec do_join(Ecto.Query.t(), atom(), module(), atom(), atom(), atom()) ::
Ecto.Query.t()
defmacro do_join(
query,
assoc_name,
related_schema,
owner_key,
related_key,
parent_alias \\ nil
) do
quote bind_quoted: [
query: query,
assoc_name: assoc_name,
related_schema: related_schema,
owner_key: owner_key,
related_key: related_key,
parent_alias: parent_alias
] do
new_alias =
if parent_alias do
String.to_atom("#{parent_alias}_#{assoc_name}")
else
assoc_name
end
if parent_alias do
from([{^parent_alias, parent}] in query,
left_join: assoc in ^related_schema,
as: ^new_alias,
on: field(parent, ^owner_key) == field(assoc, ^related_key)
)
else
from(e0 in query,
left_join: assoc in ^related_schema,
as: ^new_alias,
on: field(e0, ^owner_key) == field(assoc, ^related_key)
)
end
end
end
defmodule NamedBinding do
@moduledoc """
Helper functions for working with named bindings in Ecto queries.
"""
@spec find_all(Ecto.Query.t(), list(atom())) :: list({atom(), integer(), Ecto.Query.Join.t()})
def find_all(query, binding_names) when is_list(binding_names) do
binding_names
|> Enum.reduce({[], nil}, fn binding_name, {acc, parent_alias} ->
alias_to_find =
if parent_alias do
String.to_atom("#{parent_alias}_#{binding_name}")
else
binding_name
end
case find(query, alias_to_find) do
nil -> {acc, alias_to_find}
{idx, join} -> {[{alias_to_find, idx, join} | acc], alias_to_find}
end
end)
|> elem(0)
|> Enum.reverse()
end
@spec find(Ecto.Query.t(), atom()) :: {integer(), Ecto.Query.Join.t()} | nil
def find(query, binding_name) do
query.joins
|> Enum.with_index(1)
|> Enum.find_value(fn {join, idx} ->
if join.as == binding_name, do: {idx, join}, else: nil
end)
end
end
end