lib/connection/odbc.ex


defmodule Connection.Odbc do
    @moduledoc"""
    Module for working with the ODBC library. It frees you from having to know how Erlang's :odbc library works.
    """

    require Logger
    import Type.Type, only: [convert: 2]
    use Decoratorm


    ################
    ### Functions
    ################

    @doc"""
    Start the ODBC application

    ### Return:

        - :ok | exception. If the ODBC application has not been started or if it has already been started, return the atom. Otherwise, it throws an exception with the error.

    """
    def start() do
        :odbc.start()
        |> case do
            :ok ->
                Logger.debug("Connection started with ODBC")
                :ok
            {:error, {:already_started, :odbc}} ->
                Logger.debug("Previously started connection with ODBC")
                :ok
            {:error, error} ->
                raise("Unhandled error trying to \":odbc.start/0\", Error: #{inspect error}")
        end
    end

    @doc"""
    Stop the ODBC application

    ### Return:

        - :ok | exception. If the ODBC application has not been stopped or if it has already been stopped, return the atom. Otherwise, it throws an exception with the error.

    """
    def stop() do
        :odbc.stop()
        |> case do
            :ok ->
                Logger.debug("Connection stop with ODBC")
                :ok
            {:error, {:not_started, :odbc}} ->
                Logger.debug("Previously stopped connection with ODBC")
                :ok
            {:error, error} ->
                raise("Unhandled error trying to \":odbc.stop/0\", Error: #{inspect error}")
        end
    end

    @doc"""
    Establishes the connection to Data Source

    ### Parameter:

        - data_source: List. Data source information.

    ### Return:

        - PID | Exception. If the connection can be established with the parameters defined in the configuration files, process identifier is returned. Otherwise, it throws an exception with the error.

    """
    def connect(data_source) when is_list(data_source) do
        data_source
        |> Enum.reduce("", fn {key, value}, acc -> acc <> "#{key}=#{value};" end)
        |> Kernel.to_charlist()
        |> :odbc.connect([])
        |> case do
            {:ok, pid} -> pid
            {:error, error} ->
                raise("Unhandled error trying to \":odbc.connect/2\", Error: #{inspect error}")
        end
    end

    @doc"""
    Genera un UUID

    ### Parameter:

        - pid: Process.

    ### Return:

        - string | Exception. If the query succeeds, it returns the UUID; otherwise, it raises an exception from `:odbc.sql_query/2` statement.

    """
    def get_uuid(pid) when is_pid(pid) do
        query = Statement.Sql.generate_uuid() |> Kernel.to_charlist()

        :odbc.sql_query(pid, query)
        |> case do
            {_, _, [{uuid}]} -> List.to_string(uuid)
            {:error, error} -> raise(inspect error)
            unknown -> raise("Not match. Entity: #{inspect __MODULE__}. Environment: #{inspect __ENV__.function}. Value: #{inspect unknown}.")
        end
    end

    @doc"""
    Inserts an object into the specified table

    ### Parameters:

        - pid: Process. Process connecting Elixir and ODBC.

        - statement: String. SQL Statement.

    ### Return:

        - {:inserted, 1} | Exception

    """
    def insert(pid, statement)
        when is_pid(pid) and is_binary(statement)
        do
            query = Kernel.to_charlist(statement)

            :odbc.sql_query(pid, query)
            |> case do
                {:error, error} -> raise("#{inspect(error)}. Statement: #{statement}")
                result -> result
            end
    end

    @doc"""
    Create and execute an SQL statement of type `SELECT xx FROM x WHERE xxx. It is assumed that WHERE condition will only have the AND operator. Para

    ### Parameters:

        - pid: Process. Process connecting Elixir and ODBC.

        - statement: String. SQL Statement.

    ### Return:

        - list of (list of tuples ({:atom, :string})) | Exception. Fields with null values are ignored.

    """
    def select(pid, statement) when is_pid(pid) and is_binary(statement) do
        query = Kernel.to_charlist(statement)

        :odbc.sql_query(pid, query)
        |> build_format()
    end

    @doc"""
    Update an object into the specified table. It uses the `retry` decorator to retry the execution of the statement, in case of a concurrency error issued by BigQuery.
    ### Parameters:

        - pid: Process. Process connecting Elixir and ODBC.

        - statement: String. SQL Statement.

    ### Return:

        - Exception | {:updated, n} where n is the number of records updated

    """
    @decorate retry()
    def update(pid, statement) when is_pid(pid) and is_binary(statement) do
        query = Kernel.to_charlist(statement)

        :odbc.sql_query(pid, query)
        |> case do
            {:error, error} -> raise(inspect error)
            result -> result
        end
    end

    @doc"""
    Delete rows from the specified table

    ### Parameters:

        - pid: Process. Process connecting Elixir and ODBC.

        - statement: String. SQL Statement.

    ### Return:

        - Exception | {:updated, n} where n is the number of records deleted

    """
    def delete(pid, statement) when is_pid(pid) and is_binary(statement) do
        query = Kernel.to_charlist(statement)

        :odbc.sql_query(pid, query)
        |> case do
            {:error, error} -> raise(inspect error)
            result -> result
        end
    end


    ################
    ### Helper functions
    ################

    #
    # Given the return of a query, build the structure: list of (list of tuples ({:atom, :string})), or return the exception of having made the query. Tuples with null values are ignored.
    #
    #
    defp build_format(rq) do
        case rq do
            {_, cols, result} ->
                cols = cols
                        |> Enum.map(fn c ->
                            convert(c, :string)
                            |> convert(:atom)
                        end)

                result
                |> Enum.map(fn r ->
                    tmp = r
                        |> Tuple.to_list()
                        |> Enum.map(fn
                            :null -> nil
                            e -> convert(e, :string)
                        end)
                    Enum.zip(cols, tmp)
                end)
                |> Enum.map(fn list ->
                    list
                    |> Enum.filter(fn {_, value} -> not is_nil(value) end)
                end)

            {:error, error} ->
                raise(inspect error)

            unknown ->
                raise("Not match. Value: #{inspect unknown }. Entity: #{inspect __MODULE__}")
        end
    end


end