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()}
| {:execution_mode, :prepare_execute | :executesql}
| 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