lib/ecto_mysql_extras.ex

defmodule EctoMySQLExtras do
  @moduledoc """
  Documentation for `EctoMySQLExtras`.
  """
  import EctoMySQLExtras.Output

  @callback info :: %{
              required(:title) => String.t(),
              required(:columns) => [%{name: atom(), type: atom()}],
              optional(:order_by) => [{atom(), :ASC | :DESC}],
              optional(:args) => [atom()],
              optional(:default_args) => list()
            }

  @type repo() :: module() | {module(), node()}

  @check_database [:db_settings, :long_running_queries]

  @spec queries(repo()) :: map()
  def queries(_repo \\ nil) do
    %{
      db_settings: EctoMySQLExtras.DbSettings,
      db_status: EctoMySQLExtras.DbStatus,
      index_size: EctoMySQLExtras.IndexSize,
      long_running_queries: EctoMySQLExtras.LongRunningQueries,
      plugins: EctoMySQLExtras.Plugins,
      records_rank: EctoMySQLExtras.RecordsRank,
      table_indexes_size: EctoMySQLExtras.TableIndexesSize,
      table_size: EctoMySQLExtras.TableSize,
      total_index_size: EctoMySQLExtras.TotalIndexSize,
      total_table_size: EctoMySQLExtras.TotalTableSize,
      unused_indexes: EctoMySQLExtras.UnusedIndexes
    }
  end

  @doc """
  Run a query with `name`, on `repo`, in the given `format`.
  The `repo` can be a module name or a tuple like `{module, node}`.

  ## Options
    * `:format` - The format that results will return. Accepts `:ascii` or `:raw`.
      If `:ascii` a nice table printed in ASCII - a string will be returned.
      Otherwise a result struct will be returned. This option is required.

    * `:args` - Overwrites the default arguments for the given query. You can
      check the defaults of each query in its modules defined in this project.
  """
  @spec query(atom(), repo(), keyword()) :: :ok | MyXQL.Result.t()
  def query(query_name, repo, opts \\ []) do
    query_module = Map.fetch!(queries(), query_name)
    opts = default_opts(opts, query_module.info[:default_args])

    result =
      query!(
        repo,
        query_module.query(Keyword.fetch!(opts, :args) |> database_opts(repo, query_name))
      )

    format(
      Keyword.fetch!(opts, :format),
      query_module.info,
      result
    )
  end

  defp query!({repo, node}, query) do
    case :rpc.call(node, repo, :query!, [query]) do
      {:badrpc, {:EXIT, {:undef, _}}} ->
        raise "repository is not defined on remote node"

      {:badrpc, error} ->
        raise "cannot send query to remote node #{inspect(node)}. Reason: #{inspect(error)}"

      result ->
        result
    end
  end

  defp query!(repo, query) do
    repo.query!(query)
  end

  # Not sure if this is the best way to retreive the database
  defp which_database(repo) do
    version =
      query!(repo, "SHOW VARIABLES LIKE 'version'")
      |> (&Enum.at(&1.rows, 0)).()
      |> (&Enum.at(&1, 1)).()
      |> String.downcase()

    if String.contains?(version, "mariadb") do
      [db: :mariadb, version: version]
    else
      [db: :mysql, version: version]
    end
  end

  @spec db_settings(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def db_settings(repo, opts \\ []), do: query(:db_settings, repo, opts)

  @spec db_status(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def db_status(repo, opts \\ []), do: query(:db_status, repo, opts)

  @spec index_size(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def index_size(repo, opts \\ []), do: query(:index_size, repo, opts)

  @spec long_running_queries(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def long_running_queries(repo, opts \\ []), do: query(:long_running_queries, repo, opts)

  @spec records_rank(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def records_rank(repo, opts \\ []), do: query(:records_rank, repo, opts)

  @spec plugins(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def plugins(repo, opts \\ []), do: query(:plugins, repo, opts)

  @spec table_indexes_size(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def table_indexes_size(repo, opts \\ []), do: query(:table_indexes_size, repo, opts)

  @spec table_size(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def table_size(repo, opts \\ []), do: query(:table_size, repo, opts)

  @spec total_index_size(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def total_index_size(repo, opts \\ []), do: query(:total_index_size, repo, opts)

  @spec total_table_size(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def total_table_size(repo, opts \\ []), do: query(:total_table_size, repo, opts)

  @spec unused_indexes(repo(), keyword()) :: :ok | MyXQL.Result.t()
  def unused_indexes(repo, opts \\ []), do: query(:unused_indexes, repo, opts)

  defp default_opts(opts, nil), do: default_opts(opts, [])

  defp default_opts(opts, default_args) do
    format = Keyword.get(opts, :format, :raw)

    args =
      Keyword.merge(
        default_args || [],
        opts[:args] || []
      )

    [
      format: format,
      args: args
    ]
  end

  defp database_opts(opts, repo, query) when query in @check_database do
    database = which_database(repo)
    Keyword.merge(database, opts)
  end

  defp database_opts(opts, _repo, _query), do: opts
end