lib/prestige.ex

defmodule Prestige do
  @moduledoc """
  An elixir client for [Prestodb](http://prestodb.github.io/).
  """

  alias Prestige.Client
  alias Prestige.Session

  defmodule Error do
    @moduledoc false

    defexception [:message, :code, :location, :name, :type, :stack]
  end

  defmodule BadRequestError do
    @moduledoc false

    defexception [:message]
  end

  defmodule ConnectionError do
    @moduledoc false

    defexception [:message, :code]
  end

  @type transaction_return :: :commit | {:commit, term} | :rollback | {:rollback, term}

  @type query_opts :: [
          name: String.t()
        ]

  @spec new_session(keyword) :: Session.t()
  defdelegate new_session(opts), to: Session, as: :new

  @spec prepare(session :: Session.t(), name :: String.t(), statement :: String.t()) ::
          {:ok, Session.t()} | {:error, term}
  def prepare(%Session{} = session, name, statement) do
    new_session = prepare!(session, name, statement)
    {:ok, new_session}
  rescue
    error -> {:error, error}
  end

  @spec prepare!(session :: Session.t(), name :: String.t(), statement :: String.t()) :: Session.t()
  def prepare!(%Session{} = session, name, statement) do
    Client.prepare_statement(session, name, statement)
  end

  @spec execute(session :: Session.t(), name :: String.t(), args :: list) :: {:ok, Prestige.Result.t()} | {:error, term}
  def execute(%Session{} = session, name, args) do
    result = execute!(session, name, args)
    {:ok, result}
  rescue
    error -> {:error, error}
  end

  @spec execute!(session :: Session.t(), name :: String.t(), args :: list) :: Prestige.Result.t()
  def execute!(%Session{} = session, name, args) do
    Client.execute_statement(session, name, args)
    |> Enum.to_list()
    |> collapse_results()
  end

  @spec execute(session :: Session.t(), statement :: String.t()) :: {:ok, Prestige.Result.t()} | {:error, term}
  def execute(%Session{} = session, statement) do
    result = execute!(session, statement)
    {:ok, result}
  rescue
    error -> {:error, error}
  end

  @spec execute!(session :: Session.t(), statement :: String.t()) :: Prestige.Result.t()
  def execute!(%Session{} = session, statement) do
    Client.execute(session, statement)
    |> Enum.to_list()
    |> collapse_results()
  end

  @spec close(session :: Session.t(), name :: String.t()) :: {:ok, Session.t()} | {:error, term}
  def close(%Session{} = session, name) do
    new_session = close!(session, name)
    {:ok, new_session}
  rescue
    error -> {:error, error}
  end

  @spec close!(session :: Session.t(), name :: String.t()) :: Session.t()
  def close!(%Session{} = session, name) do
    Client.close_statement(session, name)
  end

  @spec query(session :: Session.t(), statement :: String.t(), args :: list, opts :: query_opts) ::
          {:ok, Prestige.Result.t()} | {:error, term}
  def query(%Session{} = session, statement, args \\ [], opts \\ []) do
    result = query!(session, statement, args, opts)
    {:ok, result}
  rescue
    error -> {:error, error}
  end

  @spec query!(session :: Session.t(), statement :: String.t(), args :: list, opts :: query_opts) :: Prestige.Result.t()
  def query!(%Session{} = session, statement, args \\ [], opts \\ []) do
    name = Keyword.get(opts, :name, "stmt")

    Client.execute(session, name, statement, args)
    |> Enum.to_list()
    |> collapse_results()
  end

  @spec stream!(session :: Session.t(), statement :: String.t(), args :: list, opts :: query_opts) :: Enumerable.t()
  def stream!(%Session{} = session, statement, args \\ [], opts \\ []) do
    name = Keyword.get(opts, :name, "stmt")

    Client.execute(session, name, statement, args)
  end

  @spec transaction(session :: Session.t(), function :: (session :: Session.t() -> transaction_return())) :: term
  def transaction(%Session{} = session, function) when is_function(function, 1) do
    session = Client.start_transaction(session)

    result =
      try do
        function.(session)
      rescue
        e ->
          Client.rollback(session)
          reraise e, __STACKTRACE__
      end

    case result do
      :commit ->
        Client.commit(session)
        :ok

      {:commit, value} ->
        Client.commit(session)
        value

      :rollback ->
        Client.rollback(session)
        :ok

      {:rollback, value} ->
        Client.rollback(session)
        value
    end
  end

  defp collapse_results(results) do
    columns = List.first(results).columns
    rows = flatten(results)

    %Prestige.Result{
      columns: columns,
      rows: rows,
      presto_headers: []
    }
  end

  defp flatten(results) do
    Enum.reduce(results, [], fn result, acc ->
      Enum.reduce(result.rows, acc, fn row, acc ->
        [row | acc]
      end)
    end)
    |> Enum.reverse()
  end
end