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