297 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
defmodule Ecto.Adapters.Tds do
 | 
						|
  @moduledoc """
 | 
						|
  Adapter module for MSSQL Server using the TDS protocol.
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
  Tds options split in different categories described
 | 
						|
  below. All options can be given via the repository
 | 
						|
  configuration.
 | 
						|
 | 
						|
  ### Connection options
 | 
						|
 | 
						|
    * `:hostname` - Server hostname
 | 
						|
    * `:port` - Server port (default: 1433)
 | 
						|
    * `:username` - Username
 | 
						|
    * `:password` - User password
 | 
						|
    * `:database` - the database to connect to
 | 
						|
    * `:pool` - The connection pool module, may be set to `Ecto.Adapters.SQL.Sandbox`
 | 
						|
    * `:ssl` - Set to true if ssl should be used (default: false)
 | 
						|
    * `:ssl_opts` - A list of ssl options, see Erlang's `ssl` docs
 | 
						|
    * `:show_sensitive_data_on_connection_error` - show connection data and
 | 
						|
      configuration whenever there is an error attempting to connect to the
 | 
						|
      database
 | 
						|
 | 
						|
  We also recommend developers to consult the `Tds.start_link/1` documentation
 | 
						|
  for a complete list of all supported options for driver.
 | 
						|
 | 
						|
  ### Storage options
 | 
						|
 | 
						|
    * `:collation` - the database collation. Used during database creation but
 | 
						|
      it is ignored later
 | 
						|
 | 
						|
  If you need collation other than Latin1, add `tds_encoding` as dependency to
 | 
						|
  your project `mix.exs` file then amend `config/config.ex` by adding:
 | 
						|
 | 
						|
      config :tds, :text_encoder, Tds.Encoding
 | 
						|
 | 
						|
  This should give you extended set of most encoding. For complete list check
 | 
						|
  `Tds.Encoding` [documentation](https://hexdocs.pm/tds_encoding).
 | 
						|
 | 
						|
  ### After connect flags
 | 
						|
 | 
						|
  After connecting to MSSQL server, TDS will check if there are any flags set in
 | 
						|
  connection options that should affect connection session behaviour. All flags are
 | 
						|
  MSSQL standard *SET* options. The following flags are currently supported:
 | 
						|
 | 
						|
    * `:set_language` - sets session language (consult stored procedure output
 | 
						|
       `exec sp_helplanguage` for valid values)
 | 
						|
    * `:set_datefirst` - number in range 1..7
 | 
						|
    * `:set_dateformat` - atom, one of `:mdy | :dmy | :ymd | :ydm | :myd | :dym`
 | 
						|
    * `:set_deadlock_priority` - atom, one of `:low | :high | :normal | -10..10`
 | 
						|
    * `:set_lock_timeout` - number in milliseconds > 0
 | 
						|
    * `:set_remote_proc_transactions` - atom, one of `:on | :off`
 | 
						|
    * `:set_implicit_transactions` - atom, one of `:on | :off`
 | 
						|
    * `:set_allow_snapshot_isolation` - atom, one of `:on | :off`
 | 
						|
       (required if `Repo.transaction(fn -> ... end, isolation_level: :snapshot)` is used)
 | 
						|
    * `:set_read_committed_snapshot` - atom, one of `:on | :off`
 | 
						|
 | 
						|
  ## Limitations
 | 
						|
 | 
						|
  ### UUIDs
 | 
						|
 | 
						|
  MSSQL server has slightly different binary storage format for UUIDs (`uniqueidentifier`).
 | 
						|
  If you use `:binary_id`, the proper choice is made. Otherwise you must use the `Tds.Ecto.UUID`
 | 
						|
  type. Avoid using `Ecto.UUID` since it may cause unpredictable application behaviour.
 | 
						|
 | 
						|
  ### SQL `Char`, `VarChar` and `Text` types
 | 
						|
 | 
						|
  When working with binaries and strings,there are some limitations you should be aware of:
 | 
						|
 | 
						|
    - Strings that should be stored in mentioned sql types must be encoded to column
 | 
						|
      codepage (defined in collation). If collation is different than database collation,
 | 
						|
      it is not possible to store correct value into database since the connection
 | 
						|
      respects the database collation. Ecto does not provide way to override parameter
 | 
						|
      codepage.
 | 
						|
 | 
						|
    - If you need other than Latin1 or other than your database default collation, as
 | 
						|
      mentioned in "Storage Options" section, then manually encode strings using
 | 
						|
      `Tds.Encoding.encode/2` into desired codepage and then tag parameter as `:binary`.
 | 
						|
      Please be aware that queries that use this approach in where clauses can be 10x slower
 | 
						|
      due increased logical reads in database.
 | 
						|
 | 
						|
    - You can't store VarChar codepoints encoded in one collation/codepage to column that
 | 
						|
      is encoded in different collation/codepage. You will always get wrong result. This is
 | 
						|
      not adapter or driver limitation but rather how string encoding works for single byte
 | 
						|
      encoded strings in MSSQL server. Don't be confused if you are always seeing latin1 chars,
 | 
						|
      they are simply in each codepoint table.
 | 
						|
 | 
						|
  In particular, if a field has the type `:text`, only raw binaries will be allowed.
 | 
						|
  To avoid above limitations always use `:string` (NVarChar) type for text if possible.
 | 
						|
  If you really need to use VarChar's column type, you can use the `Tds.Ecto.VarChar`
 | 
						|
  Ecto type.
 | 
						|
 | 
						|
  ### JSON support
 | 
						|
 | 
						|
  Even though the adapter will convert `:map` fields into JSON back and forth,
 | 
						|
  actual value is stored in NVarChar column.
 | 
						|
 | 
						|
  ### Query hints and table hints
 | 
						|
 | 
						|
  MSSQL supports both query hints and table hints: https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query
 | 
						|
 | 
						|
  For Ecto compatibility, the query hints must be given via the `lock` option, and they
 | 
						|
  will be translated to MSSQL's "OPTION". If you need to pass multiple options, you
 | 
						|
  can separate them by comma:
 | 
						|
 | 
						|
      from query, lock: "HASH GROUP, FAST 10"
 | 
						|
 | 
						|
  Table hints are specified as a list alongside a `from` or `join`:
 | 
						|
 | 
						|
      from query, hints: ["INDEX (IX_Employee_ManagerID)"]
 | 
						|
 | 
						|
  The `:migration_lock` will be treated as a table hint and defaults to "UPDLOCK".
 | 
						|
 | 
						|
  ### Multi Repo calls in transactions
 | 
						|
 | 
						|
  To avoid deadlocks in your app, we exposed `:isolation_level`  repo transaction option.
 | 
						|
  This will tell to SQL Server Transaction Manager how to begin transaction.
 | 
						|
  By default, if this option is omitted, isolation level is set to `:read_committed`.
 | 
						|
 | 
						|
  Any attempt to manually set the transaction isolation via queries, such as
 | 
						|
 | 
						|
      Ecto.Adapter.SQL.query("SET TRANSACTION ISOLATION LEVEL XYZ")
 | 
						|
 | 
						|
  will fail once explicit transaction is started using `c:Ecto.Repo.transaction/2`
 | 
						|
  and reset back to :read_committed.
 | 
						|
 | 
						|
  There is `Ecto.Query.lock/3` function can help by setting it to `WITH(NOLOCK)`.
 | 
						|
  This should allow you to do eventually consistent reads and avoid locks on given
 | 
						|
  table if you don't need to write to database.
 | 
						|
 | 
						|
  NOTE: after explicit transaction ends (commit or rollback) implicit transactions
 | 
						|
  will run as READ_COMMITTED.
 | 
						|
  """
 | 
						|
 | 
						|
  use Ecto.Adapters.SQL,
 | 
						|
    driver: :tds
 | 
						|
 | 
						|
  require Logger
 | 
						|
  require Ecto.Query
 | 
						|
 | 
						|
  @behaviour Ecto.Adapter.Storage
 | 
						|
 | 
						|
  @doc false
 | 
						|
  def autogenerate(:binary_id), do: Tds.Ecto.UUID.bingenerate()
 | 
						|
  def autogenerate(:embed_id), do: Tds.Ecto.UUID.generate()
 | 
						|
  def autogenerate(type), do: super(type)
 | 
						|
 | 
						|
  @doc false
 | 
						|
  @impl true
 | 
						|
  def loaders({:map, _}, type), do: [&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
 | 
						|
  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.Ecto.UUID, type]
 | 
						|
  def loaders(_, type), do: [type]
 | 
						|
 | 
						|
  @impl true
 | 
						|
  def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
 | 
						|
  def dumpers(:binary_id, type), do: [type, Tds.Ecto.UUID]
 | 
						|
  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), do: {:ok, x}
 | 
						|
 | 
						|
  defp json_decode(x) when is_binary(x), do: {:ok, Tds.json_library().decode!(x)}
 | 
						|
  defp json_decode(x), do: {:ok, x}
 | 
						|
 | 
						|
  # Storage API
 | 
						|
  @doc false
 | 
						|
  @impl true
 | 
						|
  def storage_up(opts) do
 | 
						|
    database = Keyword.fetch!(opts, :database)
 | 
						|
 | 
						|
    command =
 | 
						|
      ~s(CREATE DATABASE [#{database}])
 | 
						|
      |> concat_if(opts[:collation], &"COLLATE=#{&1}")
 | 
						|
 | 
						|
    case run_query(Keyword.put(opts, :database, "master"), command) do
 | 
						|
      {:ok, _} ->
 | 
						|
        :ok
 | 
						|
 | 
						|
      {: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
 | 
						|
  @impl true
 | 
						|
  def storage_down(opts) do
 | 
						|
    database = Keyword.fetch!(opts, :database)
 | 
						|
 | 
						|
    case run_query(Keyword.put(opts, :database, "master"), "DROP DATABASE [#{database}]") do
 | 
						|
      {:ok, _} ->
 | 
						|
        :ok
 | 
						|
 | 
						|
      {:error, %{mssql: %{number: 3701}}} ->
 | 
						|
        {:error, :already_down}
 | 
						|
 | 
						|
      {:error, error} ->
 | 
						|
        {:error, Exception.message(error)}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @impl Ecto.Adapter.Storage
 | 
						|
  def storage_status(opts) do
 | 
						|
    database = Keyword.fetch!(opts, :database)
 | 
						|
 | 
						|
    opts = Keyword.put(opts, :database, "master")
 | 
						|
 | 
						|
    check_database_query =
 | 
						|
      "SELECT [name] FROM [master].[sys].[databases] WHERE [name] = '#{database}'"
 | 
						|
 | 
						|
    case run_query(opts, check_database_query) do
 | 
						|
      {:ok, %{num_rows: 0}} -> :down
 | 
						|
      {:ok, %{num_rows: _}} -> :up
 | 
						|
      other -> {:error, other}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp run_query(opts, sql_command) do
 | 
						|
    {:ok, _} = Application.ensure_all_started(:ecto_sql)
 | 
						|
    {:ok, _} = Application.ensure_all_started(:tds)
 | 
						|
 | 
						|
    timeout = Keyword.get(opts, :timeout, 15_000)
 | 
						|
 | 
						|
    opts =
 | 
						|
      opts
 | 
						|
      |> Keyword.drop([:name, :log, :pool, :pool_size])
 | 
						|
      |> Keyword.put(:backoff_type, :stop)
 | 
						|
      |> Keyword.put(:max_restarts, 0)
 | 
						|
 | 
						|
    {:ok, pid} = Task.Supervisor.start_link()
 | 
						|
 | 
						|
    task =
 | 
						|
      Task.Supervisor.async_nolink(pid, fn ->
 | 
						|
        {:ok, conn} = Tds.start_link(opts)
 | 
						|
        value = Ecto.Adapters.Tds.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
 | 
						|
 | 
						|
  @impl true
 | 
						|
  def supports_ddl_transaction? do
 | 
						|
    true
 | 
						|
  end
 | 
						|
 | 
						|
  @impl true
 | 
						|
  def lock_for_migrations(meta, opts, fun) do
 | 
						|
    %{opts: adapter_opts, repo: repo} = meta
 | 
						|
 | 
						|
    if Keyword.fetch(adapter_opts, :pool_size) == {:ok, 1} do
 | 
						|
      Ecto.Adapters.SQL.raise_migration_pool_size_error()
 | 
						|
    end
 | 
						|
 | 
						|
    opts = Keyword.merge(opts, timeout: :infinity, telemetry_options: [schema_migration: true])
 | 
						|
 | 
						|
    {:ok, result} =
 | 
						|
      transaction(meta, opts, fn ->
 | 
						|
        query =
 | 
						|
          "exec sp_getapplock @Resource = 'ecto_#{inspect(repo)}', @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = -1"
 | 
						|
 | 
						|
        Ecto.Adapters.SQL.query!(meta, query, [], opts)
 | 
						|
        fun.()
 | 
						|
      end)
 | 
						|
 | 
						|
    result
 | 
						|
  end
 | 
						|
end
 |