defmodule Selecto.Diagnostics do
@moduledoc """
Query diagnostics helpers (EXPLAIN / EXPLAIN ANALYZE).
"""
alias Selecto.Error
@type explain_result :: %{
explain_sql: String.t(),
query_sql: String.t(),
params: list(),
columns: [String.t()],
rows: list(),
plan_lines: [String.t()]
}
@doc """
Run `EXPLAIN` for a Selecto query.
"""
@spec explain(Selecto.t(), keyword()) :: {:ok, explain_result()} | {:error, Selecto.Error.t()}
def explain(selecto, opts \\ []) do
run_explain(selecto, Keyword.put_new(opts, :analyze, false))
end
@doc """
Run `EXPLAIN ANALYZE` for a Selecto query.
"""
@spec explain_analyze(Selecto.t(), keyword()) ::
{:ok, explain_result()} | {:error, Selecto.Error.t()}
def explain_analyze(selecto, opts \\ []) do
run_explain(selecto, Keyword.put(opts, :analyze, true))
end
@doc """
Build the explain SQL wrapper.
"""
@spec build_explain_sql(String.t(), keyword()) :: String.t()
def build_explain_sql(query_sql, opts \\ []) when is_binary(query_sql) do
flags =
[]
|> maybe_true_flag("ANALYZE", Keyword.get(opts, :analyze, false))
|> maybe_true_flag("VERBOSE", Keyword.get(opts, :verbose, false))
|> maybe_true_flag("BUFFERS", Keyword.get(opts, :buffers, false))
|> maybe_true_flag("SETTINGS", Keyword.get(opts, :settings, false))
|> maybe_true_flag("WAL", Keyword.get(opts, :wal, false))
|> maybe_false_flag("TIMING", Keyword.get(opts, :timing, true))
|> maybe_false_flag("COSTS", Keyword.get(opts, :costs, true))
|> maybe_false_flag("SUMMARY", Keyword.get(opts, :summary, true))
|> maybe_format_flag(Keyword.get(opts, :format))
prefix =
case flags do
[] -> "EXPLAIN"
_ -> "EXPLAIN (#{Enum.join(flags, ", ")})"
end
"#{prefix} #{query_sql}"
end
defp run_explain(selecto, opts) do
to_sql_opts = Keyword.get(opts, :to_sql_opts, [])
{query_sql, params} = Selecto.to_sql(selecto, to_sql_opts)
explain_sql = build_explain_sql(query_sql, opts)
case execute_raw(selecto, explain_sql, params) do
{:ok, rows, columns} ->
plan_lines =
rows
|> Enum.map(fn
[line | _] when is_binary(line) -> line
other -> inspect(other)
end)
{:ok,
%{
explain_sql: explain_sql,
query_sql: query_sql,
params: params,
columns: columns,
rows: rows,
plan_lines: plan_lines
}}
{:error, %Error{} = error} ->
{:error, error}
{:error, other} ->
{:error, Error.from_reason(other)}
end
end
defp execute_raw(selecto, sql, params) do
cond do
selecto.adapter && selecto.adapter != Selecto.DB.PostgreSQL ->
case selecto.adapter.execute(selecto.connection, sql, params, []) do
{:ok, result} ->
{:ok, Map.get(result, :rows, []), Map.get(result, :columns, [])}
{:error, reason} ->
{:error, Error.from_reason(reason)}
end
is_atom(selecto.postgrex_opts) && not is_nil(selecto.postgrex_opts) ->
case apply(Ecto.Adapters.SQL, :query, [selecto.postgrex_opts, sql, params]) do
{:ok, result} -> {:ok, result.rows, result.columns}
{:error, reason} -> {:error, Error.from_reason(reason)}
end
match?({:pool, _}, selecto.postgrex_opts) ->
case Selecto.ConnectionPool.execute(selecto.postgrex_opts, sql, params, prepared: false) do
{:ok, result} -> {:ok, result.rows, result.columns}
{:error, reason} -> {:error, Error.from_reason(reason)}
end
true ->
case Postgrex.query(selecto.postgrex_opts, sql, params) do
{:ok, result} -> {:ok, result.rows, result.columns}
{:error, reason} -> {:error, Error.from_reason(reason)}
end
end
rescue
e ->
{:error, Error.from_reason(e)}
end
defp maybe_true_flag(flags, _name, nil), do: flags
defp maybe_true_flag(flags, _name, false), do: flags
defp maybe_true_flag(flags, name, true), do: flags ++ [name]
defp maybe_false_flag(flags, _name, nil), do: flags
defp maybe_false_flag(flags, _name, true), do: flags
defp maybe_false_flag(flags, name, false), do: flags ++ ["#{name} false"]
defp maybe_format_flag(flags, nil), do: flags
defp maybe_format_flag(flags, format) when format in [:text, :json, :yaml, :xml] do
flags ++ ["FORMAT #{String.upcase(to_string(format))}"]
end
defp maybe_format_flag(flags, _), do: flags
end