lib/clickhouse/format/tsv.ex

if Code.ensure_loaded?(NimbleCSV) do
  defmodule ClickHouse.Format.TSV do
    @moduledoc """
    An implementation of the ClickHouse `TSV` format.
    """

    @behaviour ClickHouse.Format

    import ClickHouse.Utils, only: [escape: 1]

    alias ClickHouse.DataType

    @parser __MODULE__.Parser

    NimbleCSV.define(@parser, moduledoc: false, separator: "\t", escape: "\"'")

    ################################
    # ClickHouse.Format Callbacks
    ################################

    @impl ClickHouse.Format
    @spec names() :: [binary()]
    def names, do: ["TabSeparated", "TSV"]

    @impl ClickHouse.Format
    @spec decode(raw :: iodata()) :: {ClickHouse.Result.columns(), ClickHouse.Result.rows()}
    def decode(raw) do
      rows = @parser.parse_string(raw, skip_headers: false)
      {nil, rows}
    end

    @impl ClickHouse.Format
    @spec encode(ClickHouse.data_types(), rows :: list()) :: iodata()
    def encode(_types, rows) do
      rows
      |> Enum.map(&encode_row/1)
      |> @parser.dump_to_iodata()
    end

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

    defp encode_row(row), do: Enum.map(row, &encode_value/1)

    defp encode_value(data) when is_binary(data), do: escape(data)
    defp encode_value(data) when is_list(data), do: DataType.encode(data)
    defp encode_value(nil), do: "\\N"
    defp encode_value(true), do: 1
    defp encode_value(false), do: 0
    defp encode_value(data) when is_atom(data), do: data |> to_string() |> escape()
    defp encode_value(data) when is_float(data), do: to_string(data)
    defp encode_value(data) when is_integer(data), do: to_string(data)
    defp encode_value(data) when is_bitstring(data), do: data |> to_string() |> escape()
    defp encode_value(%Date{} = data), do: to_string(data)

    defp encode_value(%DateTime{} = data) do
      data
      |> DateTime.truncate(:second)
      |> DateTime.to_iso8601()
      |> String.replace("Z", "")
    end
  end
end