zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

tds.ex (10661B)


      1 defmodule Ecto.Adapters.Tds do
      2   @moduledoc """
      3   Adapter module for MSSQL Server using the TDS protocol.
      4 
      5   ## Options
      6 
      7   Tds options split in different categories described
      8   below. All options can be given via the repository
      9   configuration.
     10 
     11   ### Connection options
     12 
     13     * `:hostname` - Server hostname
     14     * `:port` - Server port (default: 1433)
     15     * `:username` - Username
     16     * `:password` - User password
     17     * `:database` - the database to connect to
     18     * `:pool` - The connection pool module, may be set to `Ecto.Adapters.SQL.Sandbox`
     19     * `:ssl` - Set to true if ssl should be used (default: false)
     20     * `:ssl_opts` - A list of ssl options, see Erlang's `ssl` docs
     21     * `:show_sensitive_data_on_connection_error` - show connection data and
     22       configuration whenever there is an error attempting to connect to the
     23       database
     24 
     25   We also recommend developers to consult the `Tds.start_link/1` documentation
     26   for a complete list of all supported options for driver.
     27 
     28   ### Storage options
     29 
     30     * `:collation` - the database collation. Used during database creation but
     31       it is ignored later
     32 
     33   If you need collation other than Latin1, add `tds_encoding` as dependency to
     34   your project `mix.exs` file then amend `config/config.ex` by adding:
     35 
     36       config :tds, :text_encoder, Tds.Encoding
     37 
     38   This should give you extended set of most encoding. For complete list check
     39   `Tds.Encoding` [documentation](https://hexdocs.pm/tds_encoding).
     40 
     41   ### After connect flags
     42 
     43   After connecting to MSSQL server, TDS will check if there are any flags set in
     44   connection options that should affect connection session behaviour. All flags are
     45   MSSQL standard *SET* options. The following flags are currently supported:
     46 
     47     * `:set_language` - sets session language (consult stored procedure output
     48        `exec sp_helplanguage` for valid values)
     49     * `:set_datefirst` - number in range 1..7
     50     * `:set_dateformat` - atom, one of `:mdy | :dmy | :ymd | :ydm | :myd | :dym`
     51     * `:set_deadlock_priority` - atom, one of `:low | :high | :normal | -10..10`
     52     * `:set_lock_timeout` - number in milliseconds > 0
     53     * `:set_remote_proc_transactions` - atom, one of `:on | :off`
     54     * `:set_implicit_transactions` - atom, one of `:on | :off`
     55     * `:set_allow_snapshot_isolation` - atom, one of `:on | :off`
     56        (required if `Repo.transaction(fn -> ... end, isolation_level: :snapshot)` is used)
     57     * `:set_read_committed_snapshot` - atom, one of `:on | :off`
     58 
     59   ## Limitations
     60 
     61   ### UUIDs
     62 
     63   MSSQL server has slightly different binary storage format for UUIDs (`uniqueidentifier`).
     64   If you use `:binary_id`, the proper choice is made. Otherwise you must use the `Tds.Ecto.UUID`
     65   type. Avoid using `Ecto.UUID` since it may cause unpredictable application behaviour.
     66 
     67   ### SQL `Char`, `VarChar` and `Text` types
     68 
     69   When working with binaries and strings,there are some limitations you should be aware of:
     70 
     71     - Strings that should be stored in mentioned sql types must be encoded to column
     72       codepage (defined in collation). If collation is different than database collation,
     73       it is not possible to store correct value into database since the connection
     74       respects the database collation. Ecto does not provide way to override parameter
     75       codepage.
     76 
     77     - If you need other than Latin1 or other than your database default collation, as
     78       mentioned in "Storage Options" section, then manually encode strings using
     79       `Tds.Encoding.encode/2` into desired codepage and then tag parameter as `:binary`.
     80       Please be aware that queries that use this approach in where clauses can be 10x slower
     81       due increased logical reads in database.
     82 
     83     - You can't store VarChar codepoints encoded in one collation/codepage to column that
     84       is encoded in different collation/codepage. You will always get wrong result. This is
     85       not adapter or driver limitation but rather how string encoding works for single byte
     86       encoded strings in MSSQL server. Don't be confused if you are always seeing latin1 chars,
     87       they are simply in each codepoint table.
     88 
     89   In particular, if a field has the type `:text`, only raw binaries will be allowed.
     90   To avoid above limitations always use `:string` (NVarChar) type for text if possible.
     91   If you really need to use VarChar's column type, you can use the `Tds.Ecto.VarChar`
     92   Ecto type.
     93 
     94   ### JSON support
     95 
     96   Even though the adapter will convert `:map` fields into JSON back and forth,
     97   actual value is stored in NVarChar column.
     98 
     99   ### Query hints and table hints
    100 
    101   MSSQL supports both query hints and table hints: https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query
    102 
    103   For Ecto compatibility, the query hints must be given via the `lock` option, and they
    104   will be translated to MSSQL's "OPTION". If you need to pass multiple options, you
    105   can separate them by comma:
    106 
    107       from query, lock: "HASH GROUP, FAST 10"
    108 
    109   Table hints are specified as a list alongside a `from` or `join`:
    110 
    111       from query, hints: ["INDEX (IX_Employee_ManagerID)"]
    112 
    113   The `:migration_lock` will be treated as a table hint and defaults to "UPDLOCK".
    114 
    115   ### Multi Repo calls in transactions
    116 
    117   To avoid deadlocks in your app, we exposed `:isolation_level`  repo transaction option.
    118   This will tell to SQL Server Transaction Manager how to begin transaction.
    119   By default, if this option is omitted, isolation level is set to `:read_committed`.
    120 
    121   Any attempt to manually set the transaction isolation via queries, such as
    122 
    123       Ecto.Adapter.SQL.query("SET TRANSACTION ISOLATION LEVEL XYZ")
    124 
    125   will fail once explicit transaction is started using `c:Ecto.Repo.transaction/2`
    126   and reset back to :read_committed.
    127 
    128   There is `Ecto.Query.lock/3` function can help by setting it to `WITH(NOLOCK)`.
    129   This should allow you to do eventually consistent reads and avoid locks on given
    130   table if you don't need to write to database.
    131 
    132   NOTE: after explicit transaction ends (commit or rollback) implicit transactions
    133   will run as READ_COMMITTED.
    134   """
    135 
    136   use Ecto.Adapters.SQL,
    137     driver: :tds
    138 
    139   require Logger
    140   require Ecto.Query
    141 
    142   @behaviour Ecto.Adapter.Storage
    143 
    144   @doc false
    145   def autogenerate(:binary_id), do: Tds.Ecto.UUID.bingenerate()
    146   def autogenerate(:embed_id), do: Tds.Ecto.UUID.generate()
    147   def autogenerate(type), do: super(type)
    148 
    149   @doc false
    150   @impl true
    151   def loaders({:map, _}, type), do: [&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
    152   def loaders(:map, type), do: [&json_decode/1, type]
    153   def loaders(:boolean, type), do: [&bool_decode/1, type]
    154   def loaders(:binary_id, type), do: [Tds.Ecto.UUID, type]
    155   def loaders(_, type), do: [type]
    156 
    157   @impl true
    158   def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
    159   def dumpers(:binary_id, type), do: [type, Tds.Ecto.UUID]
    160   def dumpers(_, type), do: [type]
    161 
    162   defp bool_decode(<<0>>), do: {:ok, false}
    163   defp bool_decode(<<1>>), do: {:ok, true}
    164   defp bool_decode(0), do: {:ok, false}
    165   defp bool_decode(1), do: {:ok, true}
    166   defp bool_decode(x) when is_boolean(x), do: {:ok, x}
    167 
    168   defp json_decode(x) when is_binary(x), do: {:ok, Tds.json_library().decode!(x)}
    169   defp json_decode(x), do: {:ok, x}
    170 
    171   # Storage API
    172   @doc false
    173   @impl true
    174   def storage_up(opts) do
    175     database =
    176       Keyword.fetch!(opts, :database) || raise ":database is nil in repository configuration"
    177 
    178     command =
    179       ~s(CREATE DATABASE [#{database}])
    180       |> concat_if(opts[:collation], &"COLLATE=#{&1}")
    181 
    182     case run_query(Keyword.put(opts, :database, "master"), command) do
    183       {:ok, _} ->
    184         :ok
    185 
    186       {:error, %{mssql: %{number: 1801}}} ->
    187         {:error, :already_up}
    188 
    189       {:error, error} ->
    190         {:error, Exception.message(error)}
    191     end
    192   end
    193 
    194   defp concat_if(content, nil, _fun), do: content
    195   defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
    196 
    197   @doc false
    198   @impl true
    199   def storage_down(opts) do
    200     database =
    201       Keyword.fetch!(opts, :database) || raise ":database is nil in repository configuration"
    202 
    203     case run_query(Keyword.put(opts, :database, "master"), "DROP DATABASE [#{database}]") do
    204       {:ok, _} ->
    205         :ok
    206 
    207       {:error, %{mssql: %{number: 3701}}} ->
    208         {:error, :already_down}
    209 
    210       {:error, error} ->
    211         {:error, Exception.message(error)}
    212     end
    213   end
    214 
    215   @impl Ecto.Adapter.Storage
    216   def storage_status(opts) do
    217     database =
    218       Keyword.fetch!(opts, :database) || raise ":database is nil in repository configuration"
    219 
    220     opts = Keyword.put(opts, :database, "master")
    221 
    222     check_database_query =
    223       "SELECT [name] FROM [master].[sys].[databases] WHERE [name] = '#{database}'"
    224 
    225     case run_query(opts, check_database_query) do
    226       {:ok, %{num_rows: 0}} -> :down
    227       {:ok, %{num_rows: _}} -> :up
    228       other -> {:error, other}
    229     end
    230   end
    231 
    232   defp run_query(opts, sql_command) do
    233     {:ok, _} = Application.ensure_all_started(:ecto_sql)
    234     {:ok, _} = Application.ensure_all_started(:tds)
    235 
    236     timeout = Keyword.get(opts, :timeout, 15_000)
    237 
    238     opts =
    239       opts
    240       |> Keyword.drop([:name, :log, :pool, :pool_size])
    241       |> Keyword.put(:backoff_type, :stop)
    242       |> Keyword.put(:max_restarts, 0)
    243 
    244     {:ok, pid} = Task.Supervisor.start_link()
    245 
    246     task =
    247       Task.Supervisor.async_nolink(pid, fn ->
    248         {:ok, conn} = Tds.start_link(opts)
    249         value = Ecto.Adapters.Tds.Connection.execute(conn, sql_command, [], opts)
    250         GenServer.stop(conn)
    251         value
    252       end)
    253 
    254     case Task.yield(task, timeout) || Task.shutdown(task) do
    255       {:ok, {:ok, result}} ->
    256         {:ok, result}
    257 
    258       {:ok, {:error, error}} ->
    259         {:error, error}
    260 
    261       {:exit, {%{__struct__: struct} = error, _}}
    262       when struct in [Tds.Error, DBConnection.Error] ->
    263         {:error, error}
    264 
    265       {:exit, reason} ->
    266         {:error, RuntimeError.exception(Exception.format_exit(reason))}
    267 
    268       nil ->
    269         {:error, RuntimeError.exception("command timed out")}
    270     end
    271   end
    272 
    273 
    274   @impl true
    275   def supports_ddl_transaction? do
    276     true
    277   end
    278 
    279   @impl true
    280   def lock_for_migrations(meta, opts, fun) do
    281     %{opts: adapter_opts, repo: repo} = meta
    282 
    283     if Keyword.fetch(adapter_opts, :pool_size) == {:ok, 1} do
    284       Ecto.Adapters.SQL.raise_migration_pool_size_error()
    285     end
    286 
    287     opts = Keyword.merge(opts, [timeout: :infinity, telemetry_options: [schema_migration: true]])
    288 
    289     {:ok, result} =
    290       transaction(meta, opts, fn ->
    291         lock_name = "'ecto_#{inspect(repo)}'"
    292         Ecto.Adapters.SQL.query!(meta, "sp_getapplock @Resource = #{lock_name}, @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = -1", [], opts)
    293         fun.()
    294       end)
    295 
    296     result
    297   end
    298 end