Current section
Files
Jump to
Current section
Files
lib/archeometer/query/join_expr.ex
defmodule Archeometer.Query.JoinExpr do
@moduledoc false
alias Archeometer.Query.Term
@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, 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({:lookup, _m, [prefix, {:lookup, _meta, _args} = ast]}, joins) do
{new_joins, _acc_symbols} = generate_join_for_symbol(ast, {joins, [prefix]})
new_joins
end
def generate_joins({_op, _meta, args}, info) when is_list(args),
do: Enum.reduce(args, info, &generate_joins/2)
def generate_joins(other_ast, info) do
with {:ok, _ast} <- Term.validate(other_ast) do
info
end
end
defp generate_join_for_symbol({:lookup, meta, [arg]}, {joins, partial_symbol}),
do: {
update_join_map(joins, meta, [arg | partial_symbol]),
[arg | partial_symbol]
}
defp generate_join_for_symbol({:lookup, meta, [arg, 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: @table_alias_prefix <> (joins |> map_size() |> Integer.to_string())
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