lib/tds.ex

defmodule Tds do
  @moduledoc """
  Microsoft SQL Server driver for Elixir.

  Tds is partial implementation of the Micorosoft SQL Server
  [MS-TDS](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds)
  Tabular Data Stream Protocol.

  A Tds query is performed in separate server-side prepare and execute stages.

  At the moment query handle is not reused, but there is plan to cahce handles in
  near feature. It uses [RPC](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/619c43b6-9495-4a58-9e49-a4950db245b3)
  requests by default to `Sp_Prepare` (ProcId=11) and `Sp_Execute` (ProcId=12)
  query, but it is possible to configure driver to use only Sp_ExecuteSql.

  Please consult with [configuration](readme.html#configuration) how to do this.
  """
  alias Tds.Query
  alias Tds.Types.UUID

  @timeout 5000
  @execution_mode :prepare_execute

  @type start_option ::
          {:hostname, String.t()}
          | {:port, :inet.port_number()}
          | {:database, String.t()}
          | {:username, String.t()}
          | {:password, String.t()}
          | {:timeout, timeout()}
          | {:connect_timeout, timeout()}
          | DBConnection.start_option()

  @type isolation_level ::
          :read_uncommitted
          | :read_committed
          | :repeatable_read
          | :serializable
          | :snapshot
          | :no_change

  @type conn :: DBConnection.conn()

  @type resultset :: list(Tds.Result.t())

  @type option :: DBConnection.option()

  @type transaction_option ::
          {:mode, :transaction | :savepoint}
          | {:isolation_level, isolation_level()}
          | option()

  @type execute_option ::
          {:decode_mapper, (list -> term)}
          | {:resultset, boolean()}
          | option

  @spec start_link([start_option]) ::
          {:ok, conn} | {:error, Tds.Error.t() | term}
  def start_link(opts \\ []) do
    DBConnection.start_link(Tds.Protocol, default(opts))
  end

  @spec query(conn, iodata, list, [execute_option]) ::
          {:ok, Tds.Result.t()} | {:error, Exception.t()}
  def query(conn, statement, params, opts \\ []) do
    query = %Query{statement: statement}
    opts = Keyword.put_new(opts, :parameters, params)

    case DBConnection.prepare_execute(conn, query, params, opts) do
      {:ok, _query, result} -> {:ok, result}
      {:error, err} -> {:error, err}
    end
  end

  @spec query!(conn, iodata, list, [execute_option]) ::
          Tds.Result.t() | no_return()
  def query!(conn, statement, params, opts \\ []) do
    query = %Query{statement: statement}
    opts = Keyword.put_new(opts, :parameters, params)

    case DBConnection.prepare_execute(conn, query, params, opts) do
      {:ok, _query, result} -> result
      {:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
      {:error, err} -> raise err
    end
  end

  @doc """
  Executes statement that can contain multiple SQL batches, result will contain
  all results that server yield for each batch.
  """
  @spec query_multi(conn, iodata, list, [execute_option]) ::
          {:ok, resultset()}
          | {:error, Exception.t()}
  def query_multi(conn, statemnt, params, opts \\ []) do
    query = %Query{statement: statemnt}

    opts =
      opts
      |> Keyword.put_new(:parameters, params)
      |> Keyword.put_new(:resultset, true)

    case DBConnection.prepare_execute(conn, query, params, opts) do
      {:ok, _query, resultset} -> {:ok, resultset}
      {:error, err} -> {:error, err}
    end
  end

  @spec prepare(conn, iodata, [option]) ::
          {:ok, Tds.Query.t()} | {:error, Exception.t()}
  def prepare(conn, statement, opts \\ []) do
    query = %Query{statement: statement}

    case DBConnection.prepare(conn, query, opts) do
      {:ok, query} -> {:ok, query}
      {:error, err} -> {:error, err}
    end
  end

  @spec prepare!(conn, iodata, [option]) :: Tds.Query.t() | no_return()
  def prepare!(conn, statement, opts \\ []) do
    query = %Query{statement: statement}

    case DBConnection.prepare(conn, query, opts) do
      {:ok, query} -> query
      {:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
      {:error, err} -> raise err
    end
  end

  @spec execute(conn, Tds.Query.t(), list, [execute_option]) ::
          {:ok, Tds.Query.t(), Tds.Result.t()}
          | {:error, Tds.Error.t()}
  def execute(conn, query, params, opts \\ []) do
    case DBConnection.execute(conn, query, params, opts) do
      {:ok, q, result} -> {:ok, q, result}
      {:error, err} -> {:error, err}
    end
  end

  @spec execute!(conn, Tds.Query.t(), list, [execute_option]) ::
          Tds.Result.t()
  def execute!(conn, query, params, opts \\ []) do
    case DBConnection.execute(conn, query, params, opts) do
      {:ok, _q, result} -> result
      {:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
      {:error, err} -> raise err
    end
  end

  @spec close(conn, Tds.Query.t(), [option]) :: :ok | {:error, Exception.t()}
  def close(conn, query, opts \\ []) do
    case DBConnection.close(conn, query, opts) do
      {:ok, result} -> {:ok, result}
      {:error, err} -> {:error, err}
    end
  end

  @spec close!(conn, Tds.Query.t(), [option]) :: :ok
  def close!(conn, query, opts \\ []) do
    case DBConnection.close(conn, query, opts) do
      {:ok, result} -> result
      {:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
      {:error, err} -> raise err
    end
  end

  @spec transaction(conn, (DBConnection.t() -> result), [transaction_option()]) ::
          {:ok, result} | {:error, any}
        when result: var
  def transaction(conn, fun, opts \\ []) do
    DBConnection.transaction(conn, fun, opts)
  end

  @spec rollback(DBConnection.t(), reason :: any) :: no_return
  defdelegate rollback(conn, any), to: DBConnection

  @spec child_spec([start_option]) :: Supervisor.Spec.spec()
  def child_spec(opts) do
    DBConnection.child_spec(Tds.Protocol, default(opts))
  end

  defp default(opts) do
    opts
    |> Keyword.put_new(:idle_timeout, @timeout)
    |> Keyword.put_new(:execution_mode, @execution_mode)
  end

  @doc """
  Returns the configured JSON library.

  To customize the JSON library, include the following in your `config/config.exs`:

      config :tds, json_library: SomeJSONModule

  Defaults to `Jason`.
  """
  @spec json_library :: module()
  def json_library do
    Application.fetch_env!(:tds, :json_library)
  end

  @doc """
  Generates a version 4 (random) UUID in the MS uniqueidentifier binary format.
  """
  @spec generate_uuid :: <<_::128>>
  def generate_uuid, do: UUID.bingenerate()

  @doc """
  Decodes MS uniqueidentifier binary to its string representation.
  """
  def decode_uuid(uuid), do: UUID.load(uuid)

  @doc """
  Same as `decode_uuid/1` but raises `ArgumentError` if value is invalid.
  """
  def decode_uuid!(uuid) do
    case UUID.load(uuid) do
      {:ok, value} ->
        value

      :error ->
        raise ArgumentError, "Invalid uuid binary #{inspect(uuid)}"
    end
  end

  @doc """
  Encodes UUID string into MS uniqueidentifier binary.
  """
  @spec encode_uuid(any) :: :error | {:ok, <<_::128>>}
  def encode_uuid(value), do: UUID.dump(value)

  @doc """
  Same as `encode_uuid/1` but raises `ArgumentError` if value is invalid.
  """
  @spec encode_uuid!(any) :: <<_::128>>
  def encode_uuid!(value), do: UUID.dump!(value)
end