lib/selecto/db/mysql.ex

defmodule Selecto.DB.MySQL do
  @moduledoc """
  MySQL adapter for Selecto.

  Uses `MyXQL` when available. If `MyXQL` is not present, connect/execute
  return structured dependency errors.
  """

  @behaviour Selecto.DB.Adapter

  @missing_dependency {:adapter_dependency_missing, :myxql}

  @impl true
  def name, do: :mysql

  @impl true
  def connect(connection) when is_pid(connection) or is_atom(connection), do: {:ok, connection}
  def connect(opts) when is_map(opts), do: connect(Map.to_list(opts))

  def connect(opts) when is_list(opts) do
    if dependency_available?() do
      case Kernel.apply(MyXQL, :start_link, [opts]) do
        {:ok, conn} -> {:ok, conn}
        {:error, reason} -> {:error, reason}
      end
    else
      {:error, @missing_dependency}
    end
  end

  def connect(other), do: {:error, {:invalid_connection_options, other}}

  @impl true
  def execute(connection, query, params, opts) do
    if is_map(connection) and Map.has_key?(connection, :adapter) and
         Map.has_key?(connection, :connection) do
      execute_direct(Map.get(connection, :connection), query, params, opts)
    else
      execute_direct(connection, query, params, opts)
    end
  end

  defp execute_direct(connection, query, params, opts) do
    if dependency_available?() do
      case Kernel.apply(MyXQL, :query, [connection, normalize_query(query), params, opts]) do
        {:ok, result} -> {:ok, normalize_result(result)}
        {:error, reason} -> {:error, reason}
      end
    else
      {:error, @missing_dependency}
    end
  end

  @impl true
  def placeholder(_index), do: "?"

  @impl true
  def quote_identifier(identifier) when is_binary(identifier) do
    escaped = String.replace(identifier, "`", "``")
    "`#{escaped}`"
  end

  def quote_identifier(identifier), do: identifier |> to_string() |> quote_identifier()

  @impl true
  def supports?(feature) do
    feature in [:cte, :window_functions, :transactions]
  end

  defp dependency_available? do
    Code.ensure_loaded?(MyXQL) and function_exported?(MyXQL, :start_link, 1) and
      function_exported?(MyXQL, :query, 4)
  end

  defp normalize_query(query) when is_binary(query), do: query
  defp normalize_query(query), do: IO.iodata_to_binary(query)

  defp normalize_result(%{rows: rows} = result) do
    columns =
      result
      |> Map.get(:columns, [])
      |> Enum.map(&normalize_column_name/1)

    %{
      rows: rows || [],
      columns: columns
    }
  end

  defp normalize_column_name(%{name: name}) when is_binary(name), do: name
  defp normalize_column_name(name) when is_binary(name), do: name
  defp normalize_column_name(name) when is_atom(name), do: Atom.to_string(name)
  defp normalize_column_name(other), do: to_string(other)
end