Current section

Files

Jump to
archeometer lib archeometer query serializer.ex
Raw

lib/archeometer/query/serializer.ex

defmodule Archeometer.Query.Serializer do
@moduledoc false
alias Archeometer.Query.{Term, Symbol, JoinExpr, Subquery}
require EEx
@doc ~S"""
Takes a `Archeometer.Query` and returns a string containing an equivalent SQL
query
## Examples
- `like` operator with named columns
iex> import Archeometer.Query
iex> q = from f in Archeometer.Schema.Function,
...> where: f.num_args > 3,
...> where: like(f.name, "Kamaji.Web.%"),
...> select: [name: f.name, arity: f.num_args]
iex> {:ok, query_str} = Archeometer.Query.Serializer.to_sql(q)
iex> query_str
"SELECT u0.name,u0.num_args FROM functions u0 WHERE (u0.num_args > 3) AND (u0.name LIKE 'Kamaji.Web.%\') ; "
- `exists` that generates a subquery
iex> import Archeometer.Query
iex> q = from f in Archeometer.Schema.Function,
...> where: not exists(f.module.name),
...> select: [name: f.name, arity: f.num_args]
iex> {:ok, query_str} = Archeometer.Query.Serializer.to_sql(q)
iex> query_str
"SELECT u0.name,u0.num_args FROM functions u0 WHERE NOT EXISTS (SELECT u1.name FROM functions INNER JOIN modules u1 ON u1.id = u0.module_id ) ; "
- A more complex query
iex> import Archeometer.Query
iex> q = from m in Archeometer.Schema.Module,
...> select: [m.name, sum(m.functions.cc)],
...> where: m.num_lines > 10 and m.coverage < 0.9,
...> order_by: [desc: sum(m.functions.cc), asc: avg(m.num_lines)],
...> limit: 10
iex> {:ok, query_str} = Archeometer.Query.Serializer.to_sql(q)
iex> query_str
"SELECT u0.name,sum(u1.cc) FROM modules u0 INNER JOIN functions u1 ON u1.module_id = u0.id WHERE ((u0.num_lines > 10) AND (u0.coverage < 0.9)) ORDER BY sum(u1.cc) DESC,avg(u0.num_lines) ASC LIMIT 10 ; "
"""
def to_sql(query)
def to_sql({:ok, q} = query) when is_map(q) do
query
|> serialize_query()
|> ok_do(&embed_query/1)
|> ok_do(&String.replace(&1, ~r{\n+}, "\n"))
|> ok_do(&String.replace(&1, ~r{\n}, " "))
end
def to_sql({:error, _} = error), do: error
@sql_select_template """
SELECT
<%= if query.distinct do %> DISTINCT <% end %>
<%= format_grouping(query.select) %>
FROM <%= query.source.module %>
<%= unless subquery?(query), do: query.source.alias %>
<%= for %{source: s, dest: d} <- query.tables do %>
INNER JOIN <%= d.module %> <%= d.alias %>
ON <%= d.alias%>.<%= d.key%> = <%= s.alias%>.<%= s.key %>
<% end %>
<%= unless Enum.empty?(query.where) do %>
WHERE <%= Enum.intersperse(query.where, " AND ")%>
<% end %>
<%= unless Enum.empty?(query.group_by) do %>
GROUP BY <%= format_grouping(query.group_by)%>
<% end %>
<%= unless Enum.empty?(query.having) do %>
HAVING <%= Enum.intersperse(query.having, " AND ")%>
<% end %>
<%= unless Enum.empty?(query.order_by) do %>
ORDER BY <%= format_order(query.order_by)%>
<% end %>
<%= if not is_nil(query.limit) do %>
LIMIT <%= query.limit %>
<% end %>
<%= unless subquery?(query) do %>;<% end %>
"""
EEx.function_from_string(:defp, :embed_query, @sql_select_template, [:query])
defp subquery?(%Subquery{}), do: true
defp subquery?(_), do: false
defp format_grouping(expr_list),
do: format_nested_list_pairs(expr_list, &elem(&1, 1))
defp format_order(expr_list),
do:
format_nested_list_pairs(expr_list, fn {key, val} ->
[val, " ", order_keyword(key)]
end)
defp format_nested_list_pairs(list, fun) do
list
|> Enum.concat()
|> Enum.map(fn val ->
case val do
{key, v} -> fun.({key, v})
_ -> val
end
end)
|> Enum.intersperse(",")
end
defp order_keyword(:asc), do: "ASC"
defp order_keyword(:desc), do: "DESC"
@serializable_fields [:select, :where, :order_by, :group_by, :having]
@doc """
Transform each field of the query struct into either io_data or a map where
the elements are io_data.
Similar to io_data, but with maps instead of lists.
"""
def serialize_query(query)
def serialize_query({:ok, _} = query) do
Enum.reduce(@serializable_fields, query, &serialize_section(&2, &1))
|> ok_do(&serialize_source/1)
|> ok_do(&serialize_joins/1)
end
def serialize_query({:error, _} = error), do: error
defp serialize_source(query),
do: %{query | source: JoinExpr.Table.serialize(query.source)}
defp serialize_joins(query) do
Map.put(
query,
:tables,
for {_name, %JoinExpr{} = join} <- query.tables do
JoinExpr.serialize(join)
end
)
end
defp serialize_section({:ok, query}, field) do
serialize_expr(query, Map.get(query, field, []))
|> ok_do(&Map.put(query, field, &1))
end
defp serialize_section({:error, _} = e, _), do: e
defp serialize_expr(query, expr_list) when is_list(expr_list) do
expr_list
|> Enum.reduce({:ok, []}, &serialize_container_arg(query, &1, &2))
|> ok_do(&Enum.reverse/1)
end
defp serialize_expr(query, expr) do
with interpolated <- interpolate(expr, query.bindings),
{:ok, aliased} <- Symbol.replace_aliases(interpolated, query.tables),
without_subq <- serialize_subqueries(aliased) do
Term.to_iodata(without_subq)
end
end
defp serialize_container_arg(query, {key, val}, {:ok, prevs}) do
serialize_expr(query, val)
|> ok_do(fn ser_arg -> [{key, ser_arg} | prevs] end)
end
defp serialize_container_arg(query, arg, {:ok, prevs}) do
serialize_expr(query, arg)
|> ok_do(fn ser_arg -> [ser_arg | prevs] end)
end
defp serialize_subqueries(expr) do
Macro.prewalk(expr, fn
%{} = subquery ->
{:ok, subquery}
|> to_sql()
|> elem(1)
other ->
other
end)
end
defp interpolate(expr, bindings) do
Macro.prewalk(expr, fn
{:^, _, [idx]} ->
bindings |> Enum.at(idx - 1) |> Term.validate() |> elem(1)
other ->
other
end)
end
defp ok_do({:ok, elem}, fun), do: {:ok, fun.(elem)}
defp ok_do({:error, _} = error, _fun), do: error
end