Current section
Files
Jump to
Current section
Files
lib/tds_ecto.ex
defmodule Tds.Ecto do
@moduledoc """
Adapter module for MSSQL.
It uses `tds` for communicating to the database
and manages a connection pool with `poolboy`.
## Features
* Full query support (including joins, preloads and associations)
* Support for transactions
* Support for data migrations
* Support for ecto.create and ecto.drop operations
* Support for transactional tests via `Ecto.Adapters.SQL`
## Options
Mssql options split in different categories described
below. All options should be given via the repository
configuration.
### Compile time options
Those options should be set in the config file and require
recompilation in order to make an effect.
* `:adapter` - The adapter name, in this case, `Tds.Ecto`
* `:timeout` - The default timeout to use on queries, defaults to `5000`
### Repo options
* `:filter_null_on_unique_indexes` - Allows unique indexes to filter out null and only match on NOT NULL values
### Connection options
* `:hostname` - Server hostname
* `:port` - Server port (default: 1433)
* `:username` - Username
* `:password` - User password
* `:parameters` - Keyword list of connection parameters
* `:ssl` - Set to true if ssl should be used (default: false)
* `:ssl_opts` - A list of ssl options, see Erlang's `ssl` docs
### Pool options
* `:size` - The number of connections to keep in the pool
* `:max_overflow` - The maximum overflow of connections (see poolboy docs)
* `:lazy` - If false all connections will be started immediately on Repo startup (default: true)
### Storage options
* `:encoding` - the database encoding (default: "UTF8")
* `:template` - the template to create the database from
* `:lc_collate` - the collation order
* `:lc_ctype` - the character classification
"""
require Tds
use Ecto.Adapters.SQL, :tds
@behaviour Ecto.Adapter.Storage
## Custom MSSQL types
@doc false
def autogenerate(:binary_id) do
Tds.UUID.bingenerate()
end
def autogenerate(type), do: super(type)
@doc false
def loaders(:map, type), do: [&json_decode/1, type]
def loaders({:map, _}, type), do: [&json_decode/1, type]
def loaders(:boolean, type), do: [&bool_decode/1, type]
def loaders(:binary_id, type), do: [&Tds.UUID.load/1, type]
def loaders({:embed, _} = type, _),
do: [&json_decode/1, &Ecto.Adapters.SQL.load_embed(type, &1)]
def loaders(_, type), do: [type]
def dumpers({:embed, _} = type, _), do: [&Ecto.Adapters.SQL.dump_embed(type, &1)]
def dumpers(:binary_id, type), do: [type, &Tds.UUID.dump/1]
def dumpers(_, type), do: [type]
defp bool_decode(<<0>>), do: {:ok, false}
defp bool_decode(<<1>>), do: {:ok, true}
defp bool_decode(0), do: {:ok, false}
defp bool_decode(1), do: {:ok, true}
defp bool_decode(x) when is_boolean(x), do: {:ok, x}
defp json_decode(x) when is_binary(x), do: {:ok, json_library().decode!(x)}
defp json_decode(x), do: {:ok, x}
defp json_library(), do: Application.get_env(:ecto, :json_library)
# Storage API
@doc false
def storage_up(opts) do
database =
Keyword.fetch!(opts, :database) || raise ":database is nil in repository configuration"
command =
~s(CREATE DATABASE [#{database}])
|> concat_if(opts[:lc_collate], &"COLLATE=#{&1}")
case run_query(Keyword.put(opts, :database, "master"), command) do
{:ok, _} ->
:ok
{:error, %Tds.Error{mssql: %{number: 1801}}} ->
{:error, :already_up}
{:error, error} ->
{:error, Exception.message(error)}
end
end
defp concat_if(content, nil, _fun), do: content
defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
@doc false
def storage_down(opts) do
database =
Keyword.fetch!(opts, :database) || raise ":database is nil in repository configuration"
case run_query(Keyword.put(opts, :database, "master"), "DROP DATABASE [#{database}]") do
{:ok, _} ->
:ok
{:error, %Tds.Error{mssql: %{number: 3701}}} ->
{:error, :already_down}
{:error, error} ->
{:error, Exception.message(error)}
end
end
def select_versions(table, opts) do
case run_query(opts, ~s(SELECT version FROM [#{table}] ORDER BY version)) do
{:ok, %{rows: rows}} -> {:ok, Enum.map(rows, &hd/1)}
{:error, _} = error -> error
end
end
def insert_all(
repo,
%{source: {prefix, source}} = schema,
header,
rows,
{_, conflict_params, _} = on_conflict,
returning,
opts
) do
# remove any autogenerated fields with explicit identity insert since they can't be
# inserted as values (DEFAULT, ...) or VALUES (NULL, ...)
# so below will remove them before unzip them for insert statement
# this works only for primary keys
{rows, header} =
case Map.get(schema, :autogenerate_id) do
{field, type} when type in [:id, :serial, :bigserial] ->
h = Enum.reject(header, &(&1 == field))
r =
rows
|> Enum.map(
&Enum.reject(&1, fn
{^field, _} -> true
_ -> false
end)
)
{r, h}
_ ->
{rows, header}
end
{rows, params} = Ecto.Adapters.SQL.unzip_inserts(header, rows)
sql = Tds.Ecto.Connection.insert(prefix, source, header, rows, on_conflict, returning)
%{rows: rows, num_rows: num} =
Ecto.Adapters.SQL.query!(repo, sql, Enum.reverse(params) ++ conflict_params, opts)
{num, rows}
end
defp run_query(opts, sql_command) do
{:ok, _} = Application.ensure_all_started(:tds)
hostname = Keyword.get(opts, :hostname) || System.get_env("MSSQLHOST") || "localhost"
timeout = Keyword.get(opts, :timeout, 15_000)
opts =
opts
|> Keyword.delete(:name)
|> Keyword.put(:hostname, hostname)
|> Keyword.put(:pool, DBConnection.Connection)
|> Keyword.put(:backoff_type, :stop)
{:ok, pid} = Task.Supervisor.start_link()
task =
Task.Supervisor.async_nolink(pid, fn ->
{:ok, conn} = Tds.start_link(opts)
value = Tds.Ecto.Connection.execute(conn, sql_command, [], opts)
GenServer.stop(conn)
value
end)
case Task.yield(task, timeout) || Task.shutdown(task) do
{:ok, {:ok, result}} ->
{:ok, result}
{:ok, {:error, error}} ->
{:error, error}
{:exit, {%{__struct__: struct} = error, _}}
when struct in [Tds.Error, DBConnection.Error] ->
{:error, error}
{:exit, reason} ->
{:error, RuntimeError.exception(Exception.format_exit(reason))}
nil ->
{:error, RuntimeError.exception("command timed out")}
end
end
def supports_ddl_transaction? do
true
end
end