lib/clickhouse/query.ex

defmodule ClickHouse.Query do
  @moduledoc """
  A ClickHouse query.
  """

  alias ClickHouse.{DataType, Query.Statement}

  defstruct [
    :format,
    :statement
  ]

  @typedoc """
  A ClickHouse query
  """
  @type t :: %__MODULE__{
          format: ClickHouse.Format.t() | nil,
          statement: iolist()
        }

  ################################
  # Public API
  ################################

  @doc """
  Prepares a new ClickHouse query.
  """
  @spec prepare(
          ClickHouse.Client.t(),
          ClickHouse.statement(),
          ClickHouse.params()
        ) ::
          ClickHouse.Query.t()
  def prepare(client, statement, params \\ [])

  def prepare(client, statement, params) when is_binary(statement) do
    statement = Statement.new(statement)
    prepare(client, statement, params)
  end

  def prepare(client, %Statement{} = statement, params) do
    format = Map.get(client.formats, statement.format)
    {types, params} = _params(params)
    statement = encode(format, statement, types, params)
    %__MODULE__{format: format, statement: statement}
  end

  ################################
  # Private API
  ################################

  defp encode(nil, %Statement{command: :insert}, _, _) do
    raise ClickHouse.QueryError, """
    Insert statements must be provided a valid format.

    Please see ClickHouse.Format documentation for more information.
    """
  end

  defp encode(format, %Statement{command: :insert, tokens: tokens}, types, rows) do
    params = format.encode(types, rows)
    [tokens, "\n", params]
  end

  defp encode(_, %Statement{command: command} = statement, _, params)
       when command in [:select, :create, :alter, :unknown] do
    do_encode(statement.tokens, params)
  end

  defp do_encode(parts, params, acc \\ [])

  defp do_encode([], _, acc), do: acc

  defp do_encode(["?" | _], [], _acc) do
    raise ClickHouse.QueryError, """
    Query parameters do not match the number of embedded '?' in the query statement.
    """
  end

  defp do_encode(["?"], [data], acc) do
    [acc | [DataType.encode(data)]]
  end

  defp do_encode(["?" | parts], [data | params], acc) do
    do_encode(parts, params, [acc | [DataType.encode(data)]])
  end

  defp do_encode([part], _params, acc) do
    [acc | [part]]
  end

  defp do_encode([part | parts], params, acc) do
    do_encode(parts, params, [acc | [part]])
  end

  defp _params(params) when is_list(params), do: {[], params}
  defp _params({types, params}), do: {types, params}

  defimpl String.Chars do
    def to_string(%{statement: statement}) do
      IO.iodata_to_binary(statement)
    end
  end

  defimpl Inspect do
    def inspect(%{statement: statement}, _) do
      "#ClickHouse.Query<\"#{IO.iodata_to_binary(statement)}\">"
    end
  end
end