Current section
Files
Jump to
Current section
Files
lib/archeometer/query/builder.ex
defmodule Archeometer.Query.Builder do
@moduledoc false
alias Archeometer.Query.{Term, Symbol, JoinExpr, Subquery}
alias Archeometer.Query.Term.Container
@doc """
Take an initial query and a keyword list of query sections and fills the
initial query with all the information from the options.
Hopefully this results in a query structure with enough information to
create a full SQL query.
"""
def build(query, fields) do
ordered_fields = Enum.sort_by(fields, &elem(&1, 0), &field_process_order/2)
with {:error, error} <- do_build(query, ordered_fields) do
{:error, format_error(error)}
end
end
defp format_error({:unresolved_symb, {symb, mod}}) do
"Couldn't resolve `#{symb}` in the fields of `#{mod}`'s schema."
end
defp format_error({:invalid_exprs, ast}) do
pretty_ast = ast |> Term.to_ast() |> Macro.to_string()
"Invalid expression `#{pretty_ast}`. Please check the `Archeometer.Query.Term` API for a list of valid terms"
end
defp format_error({:invalid_limit, ast}) do
pretty_ast = ast |> Term.to_ast() |> Macro.to_string()
"Expected integer limit but found `#{pretty_ast}`"
end
defp format_error({:invalid_bool, ast}) do
pretty_ast = ast |> Term.to_ast() |> Macro.to_string()
"Expected boolean value but found `#{pretty_ast}`"
end
defp format_error({:invalid_field, field}) do
"`#{field}` is an invalid query field"
end
defp format_error({err, {op, arity, args}}) when err in [:not_enough_args, :too_many_args] do
"`#{op}` expected #{arity} arguments but received #{count_args(args)}"
end
defp format_error({:top_level_non_alias, arg}) do
"The first argument of a lookup must be an alias, but found `#{arg}`"
end
defp format_error({:table_not_selectable, arg}) do
"`#{arg}` was used as a table field, but represents a full table"
end
defp format_error({:non_table_prefix, arg}) do
"`#{arg}` was used as a prefix but doesn't represent a table"
end
defp format_error(error), do: error
defp count_args(nil), do: 0
defp count_args(args) when is_list(args), do: length(args)
defp field_process_order(:select, _), do: true
defp field_process_order(field0, field1), do: field0 <= field1
@query_list_fields [:select, :where, :order_by, :group_by, :having]
defp do_build({:ok, final_query}, []) do
{:ok,
Enum.reduce(@query_list_fields, final_query, fn field, q ->
Map.update!(q, field, &Enum.reverse/1)
end)}
end
@query_fields [:select, :distinct, :where, :order_by, :group_by, :having, :limit]
defp do_build({:ok, query}, [{field, expr} | rest])
when field in @query_fields do
field
|> dispatch_by_field(query, expr)
|> ok_do(fn {expr, q} ->
Map.update!(q, field, fn
rest when is_list(rest) -> [expr | rest]
_ -> expr
end)
end)
|> do_build(rest)
end
defp do_build({:ok, _query}, [{field, _} | _]), do: {:error, {:invalid_field, field}}
defp do_build({:error, _} = error, _fields), do: error
defp dispatch_by_field(field, query, expr) when field in [:where, :having],
do: process_expression(query, expr)
defp dispatch_by_field(:limit, query, expr), do: process_limit(query, expr)
defp dispatch_by_field(:distinct, query, expr), do: process_distinct(query, expr)
defp dispatch_by_field(:order_by, query, expr), do: process_order(query, expr)
defp dispatch_by_field(:select, query, expr), do: process_select(query, expr)
defp dispatch_by_field(:group_by, query, expr),
do: process_container(query, expr)
defp process_limit(q, lim) when is_integer(lim), do: {:ok, {lim, q}}
defp process_limit(_q, lim), do: {:error, {:invalid_limit, lim}}
defp process_distinct(q, p) when is_boolean(p), do: {:ok, {p, q}}
defp process_distinct(_q, p), do: {:error, {:invalid_bool, p}}
defp process_order(query, expr) do
query
|> process_container(expr)
|> ensure_args_keys_in([:asc, :desc])
end
defp ensure_args_keys_in({:ok, {args, _q}} = query, orders) do
if Keyword.keyword?(args) do
args
|> Keyword.keys()
|> Enum.all?(&(&1 in orders))
|> if(do: query, else: {:error, {:invalid_keys, args}})
else
query
end
end
defp ensure_args_keys_in({:error, _} = error, _orders), do: error
defp process_select(query, container) do
process_container(query, container)
|> ok_do(fn {args, q} -> {args, update_query_aliases(q, args)} end)
end
defp update_query_aliases(q, new_aliases) do
if Keyword.keyword?(new_aliases) do
alias_map =
Map.new(new_aliases, fn {name, ast} ->
{name, {:expr_alias, ast}}
end)
%{q | aliases: Map.merge(q.aliases, alias_map, fn _n, _prev, new -> new end)}
else
q
end
end
defp process_container(query, container) do
container
|> Container.reduce({:ok, {[], query}}, &process_elem/2)
|> ok_do(fn {args, q} -> {Enum.reverse(args), q} end)
end
defp process_elem({key, val}, {:ok, _} = acc) do
process_elem(val, acc)
|> ok_do(fn {[arg | rest], q} -> {[{key, arg} | rest], q} end)
end
defp process_elem(arg, {:ok, {valid_args, query}}) do
query
|> process_expression(arg)
|> ok_do(fn {arg, q} -> {[arg | valid_args], q} end)
end
defp process_elem(_arg, {:error, _} = error), do: error
defp process_expression(query, expr) do
with {:ok, valid_ast} <- Term.validate(expr),
{:ok, resolved} <- Symbol.resolve_symbols(valid_ast, query.aliases),
escaped <- Subquery.escape_subqueries(query, resolved),
{updated_ast, new_joins} <- JoinExpr.generate_joins(escaped, query.tables) do
{:ok, {updated_ast, %{query | tables: new_joins}}}
end
end
defp ok_do({:ok, elem}, fun), do: {:ok, fun.(elem)}
defp ok_do({:error, _} = error, _fun), do: error
def escape(ast, bindings \\ []) do
{esc_ast, n_binds} = do_escape(ast, Enum.reverse(bindings))
{esc_ast, Enum.reverse(n_binds)}
end
defp do_escape({:^, meta, [expr]}, bindings),
do: {{:^, meta, [length(bindings) + 1]}, [expr | bindings]}
defp do_escape(elems, bindings) when is_list(elems) do
{escaped_args, new_bindings} = Enum.reduce(elems, {[], bindings}, &escape_arg/2)
{Enum.reverse(escaped_args), new_bindings}
end
defp do_escape({elem0, elem1}, bindings) do
{[esc1, esc0], new_bindings} = Enum.reduce([elem0, elem1], {[], bindings}, &escape_arg/2)
{{esc0, esc1}, new_bindings}
end
defp do_escape({_symb, _meta, module} = ast, bindings) when is_atom(module),
do: {ast, bindings}
defp do_escape({op, meta, args}, bindings) do
{escaped_args, new_bindings} = Enum.reduce(args, {[], bindings}, &escape_arg/2)
{{op, meta, Enum.reverse(escaped_args)}, new_bindings}
end
defp do_escape(ast, bindings), do: {ast, bindings}
defp escape_arg(arg, {prev_args, bindings}) do
{esc, new_bindings} = do_escape(arg, bindings)
{[esc | prev_args], new_bindings}
end
end