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