Current section
Files
Jump to
Current section
Files
lib/query_engine/query_joiner.ex
defmodule OuterfacesEctoApi.QueryEngine.QueryJoiner do
@moduledoc """
Provides functions 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()
def ensure_joins(query, chain) when is_list(chain) do
chain
|> Enum.reduce({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
if Enum.any?(acc_query.joins, fn j -> j.as == expected_alias end) do
{acc_query, expected_alias}
else
new_query =
do_join(acc_query, assoc_name, related_schema, owner_key, related_key, parent_alias)
{new_query, expected_alias}
end
end)
|> elem(0)
end
@spec do_join(Ecto.Query.t(), atom(), module(), atom(), atom(), atom() | nil) :: Ecto.Query.t()
def do_join(query, assoc_name, related_schema, owner_key, related_key, parent_alias \\ nil) 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
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