Current section
Files
Jump to
Current section
Files
lib/archeometer/query/join_expr.ex
defmodule Archeometer.Query.JoinExpr do
@moduledoc false
@table_alias_prefix "u"
defmodule Table do
@moduledoc false
defstruct [:module, :alias, :key]
@doc """
Transform each field in the structure into io_data.
"""
def serialize(%__MODULE__{module: m, alias: {a, _, nil}, key: k}) do
%__MODULE__{
module: m.__archeometer_name__() |> to_string(),
alias: to_string(a),
key: if(is_list(k), do: Enum.map(k, &to_string/1), else: to_string(k))
}
end
end
defstruct [:source, :dest]
@doc """
Take an AST with resolved symbols (the output of `Archeometer.Query.Symbol)`
and returns a map of `{symbol, join_expr}` values, for every `symbol` that
represents an implicit join.
Each `join_expr` has enough information to be translated into an SQL
expression.
"""
def generate_joins(ast)
def generate_joins(ast), do: generate_joins(ast, %{})
def generate_joins(ast, module, alias: mod_alias) when is_atom(module) do
generate_joins(
ast,
%{
Atom.to_string(mod_alias) => %{
dest: %Table{
module: module,
alias: next_alias_id(%{}),
key: module.__archeometer_keys__()
}
}
}
)
end
def generate_joins(asts, info) when is_list(asts),
do: Enum.reduce(asts, {[], info}, &generate_joins_arg/2)
def generate_joins({:symb, _m, [{prefix, _, nil} | rest]} = ast, joins) do
{new_joins, _acc_symbols} = generate_join_for_symbol(rest, {joins, [prefix]})
{ast, new_joins}
end
def generate_joins({op, meta, args}, info) when is_list(args) do
with {processed_args, new_info} <- Enum.reduce(args, {[], info}, &generate_joins_arg/2) do
{{op, meta, Enum.reverse(processed_args)}, new_info}
end
end
def generate_joins(%{} = subquery, info) do
with {_subq_ast, new_info} <- generate_joins(subquery.select, subquery.tables) do
{%{subquery | tables: new_info}, info}
end
end
def generate_joins(other_ast, info) do
{other_ast, info}
end
def generate_joins_arg(ast, {processed_ast, info}) do
with {new_ast, new_info} <- generate_joins(ast, info) do
{[new_ast | processed_ast], new_info}
end
end
defp generate_join_for_symbol([{arg, meta, nil}], {joins, partial_symbol}),
do: {
update_join_map(joins, meta, [arg | partial_symbol]),
[arg | partial_symbol]
}
defp generate_join_for_symbol([{arg, meta, nil} | next], {joins, partial_symbol}) do
new_joins = update_join_map(joins, meta, [arg | partial_symbol])
generate_join_for_symbol(next, {new_joins, [arg | partial_symbol]})
end
defp join_symbols(atoms), do: atoms |> Enum.reverse() |> Enum.join(".")
defp update_join_map(joins, meta, [_arg | partial_symbol] = symbols_acc) do
parent_symbol = join_symbols(partial_symbol)
full_symbol = join_symbols(symbols_acc)
if meta[:key?] && !Map.get(joins, full_symbol) do
new_alias = next_alias_id(joins)
new_join = %__MODULE__{
source: %Table{
module: meta[:source],
alias: get_symbol_alias(joins, parent_symbol),
key: elem(meta[:on], 0)
},
dest: %Table{
module: meta[:dest],
alias: new_alias,
key: elem(meta[:on], 1)
}
}
Map.put(joins, full_symbol, new_join)
else
joins
end
end
@doc """
Create a new name to use during the query serialization. Each table in the
query must have its own name.
"""
def next_alias_id(joins) do
sym = @table_alias_prefix <> (joins |> map_size() |> Integer.to_string())
{String.to_atom(sym), [], nil}
end
defp get_symbol_alias(joins, symbol) do
joins[symbol].dest.alias
end
@doc """
Transform each field in the structure into io_data.
"""
def serialize(%__MODULE__{
source: %Table{} = s,
dest: %Table{} = d
}) do
%__MODULE__{source: Table.serialize(s), dest: Table.serialize(d)}
end
end