lib/pillar/connection.ex

defmodule Pillar.Connection do
  @moduledoc """
  Structure with connection config, such as host, port, user, password and other
  """

  @boolean_to_clickhouse %{
    true => 1,
    false => 0
  }

  @type t() :: %{
          host: String.t(),
          port: integer,
          scheme: String.t(),
          password: String.t(),
          user: String.t(),
          database: String.t(),
          max_query_size: integer() | nil,
          allow_suspicious_low_cardinality_types: boolean() | nil
        }
  defstruct host: nil,
            port: nil,
            scheme: nil,
            password: nil,
            user: nil,
            database: nil,
            max_query_size: nil,
            allow_suspicious_low_cardinality_types: nil

  @doc """
  Generates Connection from typical connection string:

  ```
  %Pillar.Connection{} = Pillar.Connection.new("https://user:password@localhost:8123/some_database")

  # in this case "default" database is used
  %Pillar.Connection{} = Pillar.Connection.new("https://localhost:8123")
  ```
  """
  @spec new(String.t()) :: Pillar.Connection.t()
  def new(str) do
    uri = URI.parse(str)

    info = uri.userinfo

    [user, password] =
      cond do
        is_nil(info) -> [nil, nil]
        not String.contains?(info, ":") -> [info, nil]
        :else -> String.split(info, ":")
      end

    params = URI.decode_query(uri.query || "")

    %__MODULE__{
      host: uri.host,
      port: uri.port,
      scheme: uri.scheme,
      database: Path.basename(uri.path || "default"),
      user: user,
      password: password,
      max_query_size: nil_or_string_to_int(params["max_query_size"])
    }
  end

  def url_from_connection(%__MODULE__{} = connect_config, options \\ %{}) do
    params =
      reject_nils(%{
        password: connect_config.password,
        user: connect_config.user,
        database: connect_config.database,
        max_query_size: connect_config.max_query_size,
        allow_suspicious_low_cardinality_types:
          @boolean_to_clickhouse[connect_config.allow_suspicious_low_cardinality_types]
      })

    params = parse_options(params, options)

    uri_struct = %URI{
      host: connect_config.host,
      scheme: connect_config.scheme,
      port: connect_config.port,
      query: URI.encode_query(params),
      path: "/"
    }

    URI.to_string(uri_struct)
  end

  defp parse_options(params, %{db_side_batch_insertions: true} = options) do
    Map.put(params, "async_insert", 1)
    |> parse_options(Map.delete(options, :db_side_batch_insertions))
  end

  defp parse_options(params, %{allow_experimental_object_type: true} = options) do
    Map.put(params, "allow_experimental_object_type", 1)
    |> parse_options(Map.delete(options, :allow_experimental_object_type))
  end

  defp parse_options(params, _options), do: params

  defp nil_or_string_to_int(value) do
    if is_nil(value) do
      nil
    else
      String.to_integer(value)
    end
  end

  defp reject_nils(map) do
    map
    |> Enum.reject(fn {_k, value} -> is_nil(value) end)
    |> Map.new()
  end
end