update
This commit is contained in:
72
phoenix/deps/ecto_sql/lib/ecto/adapter/migration.ex
Normal file
72
phoenix/deps/ecto_sql/lib/ecto/adapter/migration.ex
Normal file
@@ -0,0 +1,72 @@
|
||||
defmodule Ecto.Adapter.Migration do
|
||||
@moduledoc """
|
||||
Specifies the adapter migrations API.
|
||||
"""
|
||||
|
||||
alias Ecto.Migration.Constraint
|
||||
alias Ecto.Migration.Table
|
||||
alias Ecto.Migration.Index
|
||||
alias Ecto.Migration.Reference
|
||||
|
||||
@type adapter_meta :: Ecto.Adapter.adapter_meta()
|
||||
|
||||
@type drop_mode :: :restrict | :cascade
|
||||
|
||||
@typedoc "All migration commands"
|
||||
@type command ::
|
||||
raw ::
|
||||
String.t()
|
||||
| {:create, Table.t(), [table_subcommand]}
|
||||
| {:create_if_not_exists, Table.t(), [table_subcommand]}
|
||||
| {:alter, Table.t(), [table_subcommand]}
|
||||
| {:drop, Table.t(), drop_mode()}
|
||||
| {:drop_if_exists, Table.t(), drop_mode()}
|
||||
| {:create, Index.t()}
|
||||
| {:create_if_not_exists, Index.t()}
|
||||
| {:drop, Index.t(), drop_mode()}
|
||||
| {:drop_if_exists, Index.t(), drop_mode()}
|
||||
| {:create, Constraint.t()}
|
||||
| {:drop, Constraint.t(), drop_mode()}
|
||||
| {:drop_if_exists, Constraint.t(), drop_mode()}
|
||||
|
||||
@typedoc "All commands allowed within the block passed to `table/2`"
|
||||
@type table_subcommand ::
|
||||
{:add, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(), Keyword.t()}
|
||||
| {:add_if_not_exists, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(),
|
||||
Keyword.t()}
|
||||
| {:modify, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(),
|
||||
Keyword.t()}
|
||||
| {:remove, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary(),
|
||||
Keyword.t()}
|
||||
| {:remove, field :: atom}
|
||||
| {:remove_if_exists, field :: atom, type :: Ecto.Type.t() | Reference.t() | binary()}
|
||||
| {:remove_if_exists, field :: atom}
|
||||
|
||||
@typedoc """
|
||||
A struct that represents a table or index in a database schema.
|
||||
|
||||
These database objects can be modified through the use of a Data
|
||||
Definition Language, hence the name DDL object.
|
||||
"""
|
||||
@type ddl_object :: Table.t() | Index.t()
|
||||
|
||||
@doc """
|
||||
Checks if the adapter supports ddl transaction.
|
||||
"""
|
||||
@callback supports_ddl_transaction? :: boolean
|
||||
|
||||
@doc """
|
||||
Executes migration commands.
|
||||
"""
|
||||
@callback execute_ddl(adapter_meta, command, options :: Keyword.t()) ::
|
||||
{:ok, [{Logger.level(), Logger.message(), Logger.metadata()}]}
|
||||
|
||||
@doc """
|
||||
Locks the migrations table and emits the locked versions for callback execution.
|
||||
|
||||
It returns the result of calling the given function with a list of versions.
|
||||
"""
|
||||
@callback lock_for_migrations(adapter_meta, options :: Keyword.t(), fun) ::
|
||||
result
|
||||
when fun: (-> result), result: var
|
||||
end
|
||||
62
phoenix/deps/ecto_sql/lib/ecto/adapter/structure.ex
Normal file
62
phoenix/deps/ecto_sql/lib/ecto/adapter/structure.ex
Normal file
@@ -0,0 +1,62 @@
|
||||
defmodule Ecto.Adapter.Structure do
|
||||
@moduledoc """
|
||||
Specifies the adapter structure (dump/load) API.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Dumps the given structure.
|
||||
|
||||
The path will be looked in the `config` under :dump_path or
|
||||
default to the structure path inside `default`.
|
||||
|
||||
Returns an `:ok` tuple if it was dumped successfully, an error tuple otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
structure_dump("priv/repo", username: "postgres",
|
||||
database: "ecto_test",
|
||||
hostname: "localhost")
|
||||
|
||||
"""
|
||||
@callback structure_dump(default :: String.t(), config :: Keyword.t()) ::
|
||||
{:ok, String.t()} | {:error, term}
|
||||
|
||||
@doc """
|
||||
Loads the given structure.
|
||||
|
||||
The path will be looked in the `config` under :dump_path or
|
||||
default to the structure path inside `default`.
|
||||
|
||||
Returns an `:ok` tuple if it was loaded successfully, an error tuple otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
structure_load("priv/repo", username: "postgres",
|
||||
database: "ecto_test",
|
||||
hostname: "localhost")
|
||||
|
||||
"""
|
||||
@callback structure_load(default :: String.t(), config :: Keyword.t()) ::
|
||||
{:ok, String.t()} | {:error, term}
|
||||
|
||||
@doc """
|
||||
Runs the dump command for the given repo / config.
|
||||
|
||||
Calling this function will setup authentication and run the dump cli
|
||||
command with your provided `args`.
|
||||
|
||||
The options in `opts` are passed directly to `System.cmd/3`.
|
||||
|
||||
Returns `{output, exit_status}` where `output` is a string of the stdout
|
||||
(as long as no option `into` is provided, see `System.cmd/3`) and `exit_status`
|
||||
is the exit status of the invocation. (`0` for success)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dump_cmd(["--data-only", "--table", "table_name"], [stdout_to_stderr: true], Acme.Repo.config())
|
||||
{"--\n-- PostgreSQL database dump\n--\n" <> _rest, 0}
|
||||
|
||||
"""
|
||||
@callback dump_cmd(args :: [String.t()], opts :: Keyword.t(), config :: Keyword.t()) ::
|
||||
{output :: Collectable.t(), exit_status :: non_neg_integer()}
|
||||
end
|
||||
602
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql.ex
Normal file
602
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql.ex
Normal file
@@ -0,0 +1,602 @@
|
||||
defmodule Ecto.Adapters.MyXQL do
|
||||
@moduledoc """
|
||||
Adapter module for MySQL.
|
||||
|
||||
It uses `MyXQL` for communicating to the database.
|
||||
|
||||
## Options
|
||||
|
||||
MySQL options split in different categories described
|
||||
below. All options can be given via the repository
|
||||
configuration:
|
||||
|
||||
config :your_app, YourApp.Repo,
|
||||
...
|
||||
|
||||
The `:prepare` option may be specified per operation:
|
||||
|
||||
YourApp.Repo.all(Queryable, prepare: :unnamed)
|
||||
|
||||
### Connection options
|
||||
|
||||
* `:protocol` - Set to `:socket` for using UNIX domain socket, or `:tcp` for TCP
|
||||
(default: `:socket`)
|
||||
* `:socket` - Connect to MySQL via UNIX sockets in the given path.
|
||||
* `:hostname` - Server hostname
|
||||
* `:port` - Server port (default: 3306)
|
||||
* `: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` - Accepts a list of options to enable TLS for the client connection,
|
||||
or `false` to disable it. See the documentation for [Erlang's `ssl` module](`e:ssl:ssl`)
|
||||
for a list of options (default: false)
|
||||
* `:connect_timeout` - The timeout for establishing new connections (default: 5000)
|
||||
* `:cli_protocol` - The protocol used for the mysql client connection (default: `"tcp"`).
|
||||
This option is only used for `mix ecto.load` and `mix ecto.dump`,
|
||||
via the `mysql` command. For more information, please check
|
||||
[MySQL docs](https://dev.mysql.com/doc/en/connecting.html)
|
||||
* `:socket_options` - Specifies socket configuration
|
||||
* `:show_sensitive_data_on_connection_error` - show connection data and
|
||||
configuration whenever there is an error attempting to connect to the
|
||||
database
|
||||
|
||||
The `:socket_options` are particularly useful when configuring the size
|
||||
of both send and receive buffers. For example, when Ecto starts with a
|
||||
pool of 20 connections, the memory usage may quickly grow from 20MB to
|
||||
50MB based on the operating system default values for TCP buffers. It is
|
||||
advised to stick with the operating system defaults but they can be
|
||||
tweaked if desired:
|
||||
|
||||
socket_options: [recbuf: 8192, sndbuf: 8192]
|
||||
|
||||
We also recommend developers to consult the `MyXQL.start_link/1` documentation
|
||||
for a complete listing of all supported options.
|
||||
|
||||
### Storage options
|
||||
|
||||
* `:charset` - the database encoding (default: "utf8mb4")
|
||||
* `:collation` - the collation order
|
||||
* `:dump_path` - where to place dumped structures
|
||||
* `:dump_prefixes` - list of prefixes that will be included in the
|
||||
structure dump. For MySQL, this list must be of length 1. Multiple
|
||||
prefixes are not supported. When specified, the prefixes will have
|
||||
their definitions dumped along with the data in their migration table.
|
||||
When it is not specified, only the configured database and its migration
|
||||
table are dumped.
|
||||
|
||||
### After connect callback
|
||||
|
||||
If you want to execute a callback as soon as connection is established
|
||||
to the database, you can use the `:after_connect` configuration. For
|
||||
example, in your repository configuration you can add:
|
||||
|
||||
after_connect: {MyXQL, :query!, ["SET variable = value", []]}
|
||||
|
||||
You can also specify your own module that will receive the MyXQL
|
||||
connection as argument.
|
||||
|
||||
## Limitations
|
||||
|
||||
There are some limitations when using Ecto with MySQL that one
|
||||
needs to be aware of.
|
||||
|
||||
### Engine
|
||||
|
||||
Tables created by Ecto are guaranteed to use InnoDB, regardless
|
||||
of the MySQL version.
|
||||
|
||||
### UUIDs
|
||||
|
||||
MySQL does not support UUID types. Ecto emulates them by using
|
||||
`binary(16)`.
|
||||
|
||||
### Read after writes
|
||||
|
||||
Because MySQL does not support RETURNING clauses in INSERT and
|
||||
UPDATE, it does not support the `:read_after_writes` option of
|
||||
`Ecto.Schema.field/3`.
|
||||
|
||||
### DDL Transaction
|
||||
|
||||
MySQL does not support migrations inside transactions as it
|
||||
automatically commits after some commands like CREATE TABLE.
|
||||
Therefore MySQL migrations does not run inside transactions.
|
||||
|
||||
## Old MySQL versions
|
||||
|
||||
### JSON support
|
||||
|
||||
MySQL introduced a native JSON type in v5.7.8, if your server is
|
||||
using this version or higher, you may use `:map` type for your
|
||||
column in migration:
|
||||
|
||||
add :some_field, :map
|
||||
|
||||
If you're using older server versions, use a `TEXT` field instead:
|
||||
|
||||
add :some_field, :text
|
||||
|
||||
in either case, the adapter will automatically encode/decode the
|
||||
value from JSON.
|
||||
|
||||
### usec in datetime
|
||||
|
||||
Old MySQL versions did not support usec in datetime while
|
||||
more recent versions would round or truncate the usec value.
|
||||
|
||||
Therefore, in case the user decides to use microseconds in
|
||||
datetimes and timestamps with MySQL, be aware of such
|
||||
differences and consult the documentation for your MySQL
|
||||
version.
|
||||
|
||||
If your version of MySQL supports microsecond precision, you
|
||||
will be able to utilize Ecto's usec types.
|
||||
|
||||
## Multiple Result Support
|
||||
|
||||
MyXQL supports the execution of queries that return multiple
|
||||
results, such as text queries with multiple statements separated
|
||||
by semicolons or stored procedures. These can be executed with
|
||||
`Ecto.Adapters.SQL.query_many/4` or the `YourRepo.query_many/3`
|
||||
shortcut.
|
||||
|
||||
Be default, these queries will be executed with the `:query_type`
|
||||
option set to `:text`. To take advantage of prepared statements
|
||||
when executing a stored procedure, set the `:query_type` option
|
||||
to `:binary`.
|
||||
"""
|
||||
|
||||
# Inherit all behaviour from Ecto.Adapters.SQL
|
||||
use Ecto.Adapters.SQL, driver: :myxql
|
||||
|
||||
# And provide a custom storage implementation
|
||||
@behaviour Ecto.Adapter.Storage
|
||||
@behaviour Ecto.Adapter.Structure
|
||||
|
||||
@default_prepare_opt :named
|
||||
|
||||
## Custom MySQL types
|
||||
|
||||
@impl true
|
||||
def loaders({:array, _}, type), do: [&json_decode/1, type]
|
||||
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(:float, type), do: [&float_decode/1, type]
|
||||
def loaders(:boolean, type), do: [&bool_decode/1, type]
|
||||
def loaders(:binary_id, type), do: [Ecto.UUID, type]
|
||||
def loaders(_, type), do: [type]
|
||||
|
||||
defp bool_decode(<<0>>), do: {:ok, false}
|
||||
defp bool_decode(<<1>>), do: {:ok, true}
|
||||
defp bool_decode(<<0::size(1)>>), do: {:ok, false}
|
||||
defp bool_decode(<<1::size(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 float_decode(%Decimal{} = decimal), do: {:ok, Decimal.to_float(decimal)}
|
||||
defp float_decode(x), do: {:ok, x}
|
||||
|
||||
defp json_decode(x) when is_binary(x), do: {:ok, MyXQL.json_library().decode!(x)}
|
||||
defp json_decode(x), do: {:ok, x}
|
||||
|
||||
## Query API
|
||||
|
||||
@impl Ecto.Adapter.Queryable
|
||||
def execute(adapter_meta, query_meta, query, params, opts) do
|
||||
prepare = Keyword.get(opts, :prepare, @default_prepare_opt)
|
||||
|
||||
unless valid_prepare?(prepare) do
|
||||
raise ArgumentError,
|
||||
"expected option `:prepare` to be either `:named` or `:unnamed`, got: #{inspect(prepare)}"
|
||||
end
|
||||
|
||||
Ecto.Adapters.SQL.execute(prepare, adapter_meta, query_meta, query, params, opts)
|
||||
end
|
||||
|
||||
defp valid_prepare?(prepare) when prepare in [:named, :unnamed], do: true
|
||||
defp valid_prepare?(_), do: false
|
||||
|
||||
## Storage API
|
||||
|
||||
@impl true
|
||||
def storage_up(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.delete(opts, :database)
|
||||
charset = opts[:charset] || "utf8mb4"
|
||||
|
||||
check_existence_command =
|
||||
"SELECT TRUE FROM information_schema.schemata WHERE schema_name = '#{database}'"
|
||||
|
||||
case run_query(check_existence_command, opts) do
|
||||
{:ok, %{num_rows: 1}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
_ ->
|
||||
create_command =
|
||||
~s(CREATE DATABASE `#{database}` DEFAULT CHARACTER SET = #{charset})
|
||||
|> concat_if(opts[:collation], &"DEFAULT COLLATE = #{&1}")
|
||||
|
||||
case run_query(create_command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{mysql: %{name: :ER_DB_CREATE_EXISTS}}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
|
||||
{:exit, exit} ->
|
||||
{:error, exit_to_exception(exit)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp concat_if(content, nil, _fun), do: content
|
||||
defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
|
||||
|
||||
@impl true
|
||||
def storage_down(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.delete(opts, :database)
|
||||
command = "DROP DATABASE `#{database}`"
|
||||
|
||||
case run_query(command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{mysql: %{name: :ER_DB_DROP_EXISTS}}} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:error, %{mysql: %{name: :ER_BAD_DB_ERROR}}} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
|
||||
{:exit, :killed} ->
|
||||
{:error, :already_down}
|
||||
|
||||
{:exit, exit} ->
|
||||
{:error, exit_to_exception(exit)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ecto.Adapter.Storage
|
||||
def storage_status(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
opts = Keyword.delete(opts, :database)
|
||||
|
||||
check_database_query =
|
||||
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = '#{database}'"
|
||||
|
||||
case run_query(check_database_query, opts) do
|
||||
{:ok, %{num_rows: 0}} -> :down
|
||||
{:ok, %{num_rows: _num_rows}} -> :up
|
||||
other -> {:error, other}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def supports_ddl_transaction? do
|
||||
false
|
||||
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 ->
|
||||
lock_name = "\'ecto_#{inspect(repo)}\'"
|
||||
|
||||
try do
|
||||
{:ok, _} = Ecto.Adapters.SQL.query(meta, "SELECT GET_LOCK(#{lock_name}, -1)", [], opts)
|
||||
fun.()
|
||||
after
|
||||
{:ok, _} = Ecto.Adapters.SQL.query(meta, "SELECT RELEASE_LOCK(#{lock_name})", [], opts)
|
||||
end
|
||||
end)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
@impl true
|
||||
def insert(adapter_meta, schema_meta, params, on_conflict, returning, opts) do
|
||||
%{source: source, prefix: prefix} = schema_meta
|
||||
{_, query_params, _} = on_conflict
|
||||
|
||||
key = primary_key!(schema_meta, returning)
|
||||
{fields, values} = :lists.unzip(params)
|
||||
sql = @conn.insert(prefix, source, fields, [fields], on_conflict, [], [])
|
||||
|
||||
opts =
|
||||
if is_nil(Keyword.get(opts, :cache_statement)) do
|
||||
[{:cache_statement, "ecto_insert_#{source}_#{length(fields)}"} | opts]
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
case Ecto.Adapters.SQL.query(adapter_meta, sql, values ++ query_params, opts) do
|
||||
{:ok, %{num_rows: 0}} ->
|
||||
raise "insert operation failed to insert any row in the database. " <>
|
||||
"This may happen if you have trigger or other database conditions rejecting operations. " <>
|
||||
"The emitted SQL was: #{sql}"
|
||||
|
||||
# We were used to check if num_rows was 1 or 2 (in case of upserts)
|
||||
# but MariaDB supports tables with System Versioning, and in those
|
||||
# cases num_rows can be more than 2.
|
||||
{:ok, %{last_insert_id: last_insert_id}} ->
|
||||
{:ok, last_insert_id(key, last_insert_id)}
|
||||
|
||||
{:error, err} ->
|
||||
case @conn.to_constraints(err, source: source) do
|
||||
[] -> raise err
|
||||
constraints -> {:invalid, constraints}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp primary_key!(%{autogenerate_id: {_, key, _type}}, [key]), do: key
|
||||
defp primary_key!(_, []), do: nil
|
||||
|
||||
defp primary_key!(%{schema: schema}, returning) do
|
||||
raise ArgumentError,
|
||||
"MySQL does not support :read_after_writes in schemas for non-primary keys. " <>
|
||||
"The following fields in #{inspect(schema)} are tagged as such: #{inspect(returning)}"
|
||||
end
|
||||
|
||||
defp last_insert_id(nil, _last_insert_id), do: []
|
||||
defp last_insert_id(_key, 0), do: []
|
||||
defp last_insert_id(key, last_insert_id), do: [{key, last_insert_id}]
|
||||
|
||||
@impl true
|
||||
def structure_dump(default, config) do
|
||||
table = config[:migration_source] || "schema_migrations"
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
database = dump_database!(config[:dump_prefixes], config[:database])
|
||||
|
||||
with {:ok, versions} <- select_versions(database, table, config),
|
||||
{:ok, contents} <- mysql_dump(database, config),
|
||||
{:ok, contents} <- append_versions(table, versions, contents) do
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
File.write!(path, contents)
|
||||
{:ok, path}
|
||||
end
|
||||
end
|
||||
|
||||
defp dump_database!([prefix], _), do: prefix
|
||||
defp dump_database!(nil, config_database), do: config_database
|
||||
|
||||
defp dump_database!(_, _) do
|
||||
raise ArgumentError,
|
||||
"cannot dump multiple prefixes with MySQL. Please run the command separately for each prefix."
|
||||
end
|
||||
|
||||
defp select_versions(database, table, config) do
|
||||
case run_query(~s[SELECT version FROM `#{database}`.`#{table}` ORDER BY version], config) do
|
||||
{:ok, %{rows: rows}} -> {:ok, Enum.map(rows, &hd/1)}
|
||||
{:error, %{mysql: %{name: :ER_NO_SUCH_TABLE}}} -> {:ok, []}
|
||||
{:error, _} = error -> error
|
||||
{:exit, exit} -> {:error, exit_to_exception(exit)}
|
||||
end
|
||||
end
|
||||
|
||||
defp mysql_dump(database, config) do
|
||||
args = ["--no-data", "--routines", "--no-create-db", database]
|
||||
|
||||
case run_with_cmd("mysqldump", config, args) do
|
||||
{output, 0} -> {:ok, output}
|
||||
{output, _} -> {:error, output}
|
||||
end
|
||||
end
|
||||
|
||||
defp append_versions(_table, [], contents) do
|
||||
{:ok, contents}
|
||||
end
|
||||
|
||||
defp append_versions(table, versions, contents) do
|
||||
{:ok,
|
||||
contents <>
|
||||
Enum.map_join(versions, &~s[INSERT INTO `#{table}` (version) VALUES (#{&1});\n])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def structure_load(default, config) do
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
|
||||
case File.read(path) do
|
||||
{:ok, contents} ->
|
||||
args = [
|
||||
"--silent",
|
||||
"--batch",
|
||||
"--unbuffered",
|
||||
"--init-command=SET FOREIGN_KEY_CHECKS = 0;",
|
||||
"--database",
|
||||
config[:database]
|
||||
]
|
||||
|
||||
case run_with_port("mysql", config, args, contents) do
|
||||
{_output, 0} -> {:ok, path}
|
||||
{output, _} -> {:error, output}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "could not read #{inspect(path)}: #{:file.format_error(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def dump_cmd(args, opts \\ [], config) when is_list(config) and is_list(args) do
|
||||
args =
|
||||
if database = config[:database] do
|
||||
args ++ [database]
|
||||
else
|
||||
args
|
||||
end
|
||||
|
||||
run_with_cmd("mysqldump", config, args, opts)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp run_query(sql, opts) do
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
{:ok, _} = Application.ensure_all_started(:myxql)
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.drop([:name, :log, :pool, :pool_size])
|
||||
|> Keyword.put(:backoff_type, :stop)
|
||||
|> Keyword.put(:max_restarts, 0)
|
||||
|
||||
task =
|
||||
Task.Supervisor.async_nolink(Ecto.Adapters.SQL.StorageSupervisor, fn ->
|
||||
{:ok, conn} = MyXQL.start_link(opts)
|
||||
|
||||
value = MyXQL.query(conn, sql, [], opts)
|
||||
GenServer.stop(conn)
|
||||
value
|
||||
end)
|
||||
|
||||
timeout = Keyword.get(opts, :timeout, 15_000)
|
||||
|
||||
case Task.yield(task, timeout) || Task.shutdown(task) do
|
||||
{:ok, {:ok, result}} ->
|
||||
{:ok, result}
|
||||
|
||||
{:ok, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, exit} ->
|
||||
{:exit, exit}
|
||||
|
||||
nil ->
|
||||
{:error, RuntimeError.exception("command timed out")}
|
||||
end
|
||||
end
|
||||
|
||||
defp exit_to_exception({%{__struct__: struct} = error, _})
|
||||
when struct in [MyXQL.Error, DBConnection.Error],
|
||||
do: error
|
||||
|
||||
defp exit_to_exception(reason), do: RuntimeError.exception(Exception.format_exit(reason))
|
||||
|
||||
defp run_with_cmd(cmd, opts, opt_args, cmd_opts \\ []) do
|
||||
unless System.find_executable(cmd) do
|
||||
raise "could not find executable `#{cmd}` in path, " <>
|
||||
"please guarantee it is available before running ecto commands"
|
||||
end
|
||||
|
||||
{args, env} = args_env(opts, opt_args)
|
||||
|
||||
cmd_opts =
|
||||
cmd_opts
|
||||
|> Keyword.put_new(:stderr_to_stdout, true)
|
||||
|> Keyword.update(:env, env, &Enum.concat(env, &1))
|
||||
|
||||
System.cmd(cmd, args, cmd_opts)
|
||||
end
|
||||
|
||||
defp run_with_port(cmd, opts, opt_args, contents) do
|
||||
abs_cmd = System.find_executable(cmd)
|
||||
|
||||
unless abs_cmd do
|
||||
raise "could not find executable `#{cmd}` in path, " <>
|
||||
"please guarantee it is available before running ecto commands"
|
||||
end
|
||||
|
||||
abs_cmd = String.to_charlist(abs_cmd)
|
||||
{args, env} = args_env(opts, opt_args)
|
||||
|
||||
port_opts = [
|
||||
:use_stdio,
|
||||
:exit_status,
|
||||
:binary,
|
||||
:hide,
|
||||
:stderr_to_stdout,
|
||||
env: validate_env(env),
|
||||
args: args
|
||||
]
|
||||
|
||||
port = Port.open({:spawn_executable, abs_cmd}, port_opts)
|
||||
Port.command(port, contents)
|
||||
# Use this as a signal to close the port since we cannot
|
||||
# send an exit command to mysql in batch mode
|
||||
Port.command(port, ";SELECT '__ECTO_EOF__';\n")
|
||||
collect_output(port, "")
|
||||
end
|
||||
|
||||
defp args_env(opts, opt_args) do
|
||||
env =
|
||||
if password = opts[:password] do
|
||||
[{"MYSQL_PWD", password}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
host = opts[:hostname] || System.get_env("MYSQL_HOST") || "localhost"
|
||||
port = opts[:port] || System.get_env("MYSQL_TCP_PORT") || "3306"
|
||||
protocol = opts[:cli_protocol] || System.get_env("MYSQL_CLI_PROTOCOL") || "tcp"
|
||||
|
||||
user_args =
|
||||
if username = opts[:username] do
|
||||
["--user", username]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
args =
|
||||
[
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
to_string(port),
|
||||
"--protocol",
|
||||
protocol
|
||||
] ++ user_args ++ opt_args
|
||||
|
||||
{args, env}
|
||||
end
|
||||
|
||||
defp validate_env(enum) do
|
||||
Enum.map(enum, fn
|
||||
{k, nil} ->
|
||||
{String.to_charlist(k), false}
|
||||
|
||||
{k, v} ->
|
||||
{String.to_charlist(k), String.to_charlist(v)}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, "invalid environment key-value #{inspect(other)}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp collect_output(port, acc) do
|
||||
receive do
|
||||
{^port, {:data, data}} ->
|
||||
acc = acc <> data
|
||||
|
||||
if acc =~ "__ECTO_EOF__" do
|
||||
Port.close(port)
|
||||
{acc, 0}
|
||||
else
|
||||
collect_output(port, acc)
|
||||
end
|
||||
|
||||
{^port, {:exit_status, status}} ->
|
||||
{acc, status}
|
||||
end
|
||||
end
|
||||
end
|
||||
1630
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql/connection.ex
Normal file
1630
phoenix/deps/ecto_sql/lib/ecto/adapters/myxql/connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
548
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres.ex
Normal file
548
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres.ex
Normal file
@@ -0,0 +1,548 @@
|
||||
defmodule Ecto.Adapters.Postgres do
|
||||
@moduledoc """
|
||||
Adapter module for PostgreSQL.
|
||||
|
||||
It uses `Postgrex` for communicating to the database.
|
||||
|
||||
## 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
|
||||
|
||||
Postgres options split in different categories described
|
||||
below. All options can be given via the repository
|
||||
configuration:
|
||||
|
||||
config :your_app, YourApp.Repo,
|
||||
...
|
||||
|
||||
The `:prepare` option may be specified per operation:
|
||||
|
||||
YourApp.Repo.all(Queryable, prepare: :unnamed)
|
||||
|
||||
### Migration options
|
||||
|
||||
* `:migration_lock` - prevent multiple nodes from running migrations at the same
|
||||
time by obtaining a lock. The value `:table_lock` will lock migrations by wrapping
|
||||
the entire migration inside a database transaction, including inserting the
|
||||
migration version into the migration source (by default, "schema_migrations").
|
||||
You may alternatively select `:pg_advisory_lock` which has the benefit
|
||||
of allowing concurrent operations such as creating indexes. (default: `:table_lock`)
|
||||
|
||||
When using the `:pg_advisory_lock` migration lock strategy and Ecto cannot obtain
|
||||
the lock due to another instance occupying the lock, Ecto will wait for 5 seconds
|
||||
and then retry infinity times. This is configurable on the repo with keys
|
||||
`:migration_advisory_lock_retry_interval_ms` and `:migration_advisory_lock_max_tries`.
|
||||
If the retries are exhausted, the migration will fail.
|
||||
|
||||
Some downsides to using advisory locks is that some Postgres-compatible systems or plugins
|
||||
may not support session level locks well and therefore result in inconsistent behavior.
|
||||
For example, PgBouncer when using pool_modes other than session won't work well with
|
||||
advisory locks. CockroachDB is another system that is designed in a way that advisory
|
||||
locks don't make sense for their distributed database.
|
||||
|
||||
### Connection options
|
||||
|
||||
* `:hostname` - Server hostname
|
||||
* `:socket_dir` - Connect to Postgres via UNIX sockets in the given directory
|
||||
The socket name is derived based on the port. This is the preferred method
|
||||
for configuring sockets and it takes precedence over the hostname. If you are
|
||||
connecting to a socket outside of the Postgres convention, use `:socket` instead;
|
||||
* `:socket` - Connect to Postgres via UNIX sockets in the given path.
|
||||
This option takes precedence over the `:hostname` and `:socket_dir`
|
||||
* `:username` - Username
|
||||
* `:password` - User password
|
||||
* `:port` - Server port (default: 5432)
|
||||
* `:database` - the database to connect to
|
||||
* `:maintenance_database` - Specifies the name of the database to connect to when
|
||||
creating or dropping the database. Defaults to `"postgres"`
|
||||
* `:pool` - The connection pool module, may be set to `Ecto.Adapters.SQL.Sandbox`
|
||||
* `:ssl` - Accepts a list of options to enable TLS for the client connection,
|
||||
or `false` to disable it. See the documentation for [Erlang's `ssl` module](`e:ssl:ssl`)
|
||||
for a list of options (default: false)
|
||||
* `:parameters` - Keyword list of connection parameters
|
||||
* `:connect_timeout` - The timeout for establishing new connections (default: 5000)
|
||||
* `:prepare` - How to prepare queries, either `:named` to use named queries
|
||||
or `:unnamed` to force unnamed queries (default: `:named`)
|
||||
* `:socket_options` - Specifies socket configuration
|
||||
* `:show_sensitive_data_on_connection_error` - show connection data and
|
||||
configuration whenever there is an error attempting to connect to the
|
||||
database
|
||||
|
||||
The `:socket_options` are particularly useful when configuring the size
|
||||
of both send and receive buffers. For example, when Ecto starts with a
|
||||
pool of 20 connections, the memory usage may quickly grow from 20MB to
|
||||
50MB based on the operating system default values for TCP buffers. It is
|
||||
advised to stick with the operating system defaults but they can be
|
||||
tweaked if desired:
|
||||
|
||||
socket_options: [recbuf: 8192, sndbuf: 8192]
|
||||
|
||||
We also recommend developers to consult the `Postgrex.start_link/1`
|
||||
documentation for a complete listing of all supported options.
|
||||
|
||||
### Storage options
|
||||
|
||||
* `:encoding` - the database encoding (default: "UTF8")
|
||||
or `:unspecified` to remove encoding parameter (alternative engine compatibility)
|
||||
* `:template` - the template to create the database from
|
||||
* `:lc_collate` - the collation order
|
||||
* `:lc_ctype` - the character classification
|
||||
* `:dump_path` - where to place dumped structures
|
||||
* `:dump_prefixes` - list of prefixes that will be included in the structure dump.
|
||||
When specified, the prefixes will have their definitions dumped along with the
|
||||
data in their migration table. When it is not specified, the configured
|
||||
database has the definitions dumped from all of its schemas but only
|
||||
the data from the migration table from the `public` schema is included.
|
||||
* `:force_drop` - force the database to be dropped even
|
||||
if it has connections to it (requires PostgreSQL 13+)
|
||||
|
||||
### After connect callback
|
||||
|
||||
If you want to execute a callback as soon as connection is established
|
||||
to the database, you can use the `:after_connect` configuration. For
|
||||
example, in your repository configuration you can add:
|
||||
|
||||
after_connect: {Postgrex, :query!, ["SET search_path TO global_prefix", []]}
|
||||
|
||||
You can also specify your own module that will receive the Postgrex
|
||||
connection as argument.
|
||||
|
||||
## Extensions
|
||||
|
||||
Both PostgreSQL and its adapter for Elixir, Postgrex, support an
|
||||
extension system. If you want to use custom extensions for Postgrex
|
||||
alongside Ecto, you must define a type module with your extensions.
|
||||
Create a new file anywhere in your application with the following:
|
||||
|
||||
Postgrex.Types.define(MyApp.PostgresTypes, [MyExtension.Foo, MyExtensionBar])
|
||||
|
||||
Once your type module is defined, you can configure the repository to use it:
|
||||
|
||||
config :my_app, MyApp.Repo, types: MyApp.PostgresTypes
|
||||
|
||||
## Unix socket connection
|
||||
|
||||
You may desire to communicate with Postgres via Unix sockets.
|
||||
If your PG server is started on the same machine as your code, you could check that:
|
||||
|
||||
```bash
|
||||
% sudo grep unix_socket_directories /var/lib/postgres/data/postgresql.conf
|
||||
unix_socket_directories = '/run/postgresql'
|
||||
```
|
||||
|
||||
```bash
|
||||
% ls -lah /run/postgresql
|
||||
итого 4,0K
|
||||
drwxr-xr-x 2 postgres postgres 80 июн 4 10:58 .
|
||||
drwxr-xr-x 35 root root 840 июн 4 21:02 ..
|
||||
srwxrwxrwx 1 postgres postgres 0 июн 5 07:41 .s.PGSQL.5432
|
||||
-rw------- 1 postgres postgres 61 июн 5 07:41 .s.PGSQL.5432.lock
|
||||
```
|
||||
|
||||
So you have postgresql started and listening on the socket.
|
||||
Then you may use it as follows:
|
||||
|
||||
config :your_app, YourApp.Repo,
|
||||
socket_dir: "/run/postgresql"
|
||||
"""
|
||||
|
||||
# Inherit all behaviour from Ecto.Adapters.SQL
|
||||
use Ecto.Adapters.SQL, driver: :postgrex
|
||||
|
||||
require Logger
|
||||
|
||||
# And provide a custom storage implementation
|
||||
@behaviour Ecto.Adapter.Storage
|
||||
@behaviour Ecto.Adapter.Structure
|
||||
|
||||
@default_maintenance_database "postgres"
|
||||
@default_prepare_opt :named
|
||||
|
||||
@doc """
|
||||
All Ecto extensions for Postgrex.
|
||||
|
||||
Currently Ecto does not define any of its own extensions for Postgrex.
|
||||
If this changes in a future release, you will need to call this function
|
||||
when defining your own custom extensions:
|
||||
|
||||
Postgrex.Types.define(MyApp.PostgresTypes,
|
||||
[MyExtension.Foo, MyExtensionBar] ++ Ecto.Adapters.Postgres.extensions())
|
||||
"""
|
||||
def extensions do
|
||||
[]
|
||||
end
|
||||
|
||||
# Support arrays in place of IN
|
||||
@impl true
|
||||
def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
|
||||
def dumpers({:in, sub}, {:in, sub}), do: [{:array, sub}]
|
||||
def dumpers(:binary_id, type), do: [type, Ecto.UUID]
|
||||
def dumpers(_, type), do: [type]
|
||||
|
||||
## Query API
|
||||
|
||||
@impl Ecto.Adapter.Queryable
|
||||
def execute(adapter_meta, query_meta, query, params, opts) do
|
||||
prepare = Keyword.get(opts, :prepare, @default_prepare_opt)
|
||||
|
||||
unless valid_prepare?(prepare) do
|
||||
raise ArgumentError,
|
||||
"expected option `:prepare` to be either `:named` or `:unnamed`, got: #{inspect(prepare)}"
|
||||
end
|
||||
|
||||
Ecto.Adapters.SQL.execute(prepare, adapter_meta, query_meta, query, params, opts)
|
||||
end
|
||||
|
||||
defp valid_prepare?(prepare) when prepare in [:named, :unnamed], do: true
|
||||
defp valid_prepare?(_), do: false
|
||||
|
||||
## Storage API
|
||||
|
||||
@impl true
|
||||
def storage_up(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
encoding = if opts[:encoding] == :unspecified, do: nil, else: opts[:encoding] || "UTF8"
|
||||
maintenance_database = Keyword.get(opts, :maintenance_database, @default_maintenance_database)
|
||||
opts = Keyword.put(opts, :database, maintenance_database)
|
||||
|
||||
check_existence_command = "SELECT FROM pg_database WHERE datname = '#{database}'"
|
||||
|
||||
case run_query(check_existence_command, opts) do
|
||||
{:ok, %{num_rows: 1}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
_ ->
|
||||
create_command =
|
||||
~s(CREATE DATABASE "#{database}")
|
||||
|> concat_if(encoding, &"ENCODING '#{&1}'")
|
||||
|> concat_if(opts[:template], &"TEMPLATE=#{&1}")
|
||||
|> concat_if(opts[:lc_ctype], &"LC_CTYPE='#{&1}'")
|
||||
|> concat_if(opts[:lc_collate], &"LC_COLLATE='#{&1}'")
|
||||
|
||||
case run_query(create_command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{postgres: %{code: :duplicate_database}}} ->
|
||||
{:error, :already_up}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, Exception.message(error)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp concat_if(content, nil, _), do: content
|
||||
defp concat_if(content, false, _), do: content
|
||||
defp concat_if(content, value, fun), do: content <> " " <> fun.(value)
|
||||
|
||||
@impl true
|
||||
def storage_down(opts) do
|
||||
database = Keyword.fetch!(opts, :database)
|
||||
|
||||
command =
|
||||
"DROP DATABASE \"#{database}\""
|
||||
|> concat_if(opts[:force_drop], fn _ -> "WITH (FORCE)" end)
|
||||
|
||||
maintenance_database = Keyword.get(opts, :maintenance_database, @default_maintenance_database)
|
||||
opts = Keyword.put(opts, :database, maintenance_database)
|
||||
|
||||
case run_query(command, opts) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, %{postgres: %{code: :invalid_catalog_name}}} ->
|
||||
{: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)
|
||||
|
||||
maintenance_database = Keyword.get(opts, :maintenance_database, @default_maintenance_database)
|
||||
opts = Keyword.put(opts, :database, maintenance_database)
|
||||
|
||||
check_database_query =
|
||||
"SELECT datname FROM pg_catalog.pg_database WHERE datname = '#{database}'"
|
||||
|
||||
case run_query(check_database_query, opts) do
|
||||
{:ok, %{num_rows: 0}} -> :down
|
||||
{:ok, %{num_rows: _num_rows}} -> :up
|
||||
other -> {:error, other}
|
||||
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])
|
||||
config = repo.config()
|
||||
lock_strategy = Keyword.get(config, :migration_lock, :table_lock)
|
||||
do_lock_for_migrations(lock_strategy, meta, opts, config, fun)
|
||||
end
|
||||
|
||||
defp do_lock_for_migrations(:pg_advisory_lock, meta, opts, config, fun) do
|
||||
lock = :erlang.phash2({:ecto, opts[:prefix], meta.repo})
|
||||
|
||||
retry_state = %{
|
||||
retry_interval_ms: config[:migration_advisory_lock_retry_interval_ms] || 5000,
|
||||
max_tries: config[:migration_advisory_lock_max_tries] || :infinity,
|
||||
tries: 0
|
||||
}
|
||||
|
||||
advisory_lock(meta, opts, lock, retry_state, fun)
|
||||
end
|
||||
|
||||
defp do_lock_for_migrations(:table_lock, meta, opts, _config, fun) do
|
||||
{:ok, res} =
|
||||
transaction(meta, opts, fn ->
|
||||
# SHARE UPDATE EXCLUSIVE MODE is the first lock that locks
|
||||
# itself but still allows updates to happen, see
|
||||
# # https://www.postgresql.org/docs/9.4/explicit-locking.html
|
||||
source = Keyword.get(opts, :migration_source, "schema_migrations")
|
||||
table = if prefix = opts[:prefix], do: ~s|"#{prefix}"."#{source}"|, else: ~s|"#{source}"|
|
||||
lock_statement = "LOCK TABLE #{table} IN SHARE UPDATE EXCLUSIVE MODE"
|
||||
{:ok, _} = Ecto.Adapters.SQL.query(meta, lock_statement, [], opts)
|
||||
|
||||
fun.()
|
||||
end)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
defp advisory_lock(meta, opts, lock, retry_state, fun) do
|
||||
result =
|
||||
checkout(meta, opts, fn ->
|
||||
case Ecto.Adapters.SQL.query(meta, "SELECT pg_try_advisory_lock(#{lock})", [], opts) do
|
||||
{:ok, %{rows: [[true]]}} ->
|
||||
try do
|
||||
{:ok, fun.()}
|
||||
after
|
||||
release_advisory_lock(meta, opts, lock)
|
||||
end
|
||||
|
||||
_ ->
|
||||
:no_advisory_lock
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, fun_result} ->
|
||||
fun_result
|
||||
|
||||
:no_advisory_lock ->
|
||||
maybe_retry_advisory_lock(meta, opts, lock, retry_state, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp release_advisory_lock(meta, opts, lock) do
|
||||
case Ecto.Adapters.SQL.query(meta, "SELECT pg_advisory_unlock(#{lock})", [], opts) do
|
||||
{:ok, %{rows: [[true]]}} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
raise "failed to release advisory lock"
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_retry_advisory_lock(meta, opts, lock, retry_state, fun) do
|
||||
%{retry_interval_ms: interval, max_tries: max_tries, tries: tries} = retry_state
|
||||
|
||||
if max_tries != :infinity && max_tries <= tries do
|
||||
raise "failed to obtain advisory lock. Tried #{max_tries} times waiting #{interval}ms between tries"
|
||||
else
|
||||
if Keyword.get(opts, :log_migrator_sql, false) do
|
||||
Logger.info(
|
||||
"Migration lock occupied for #{inspect(meta.repo)}. Retry #{tries + 1}/#{max_tries} at #{interval}ms intervals."
|
||||
)
|
||||
end
|
||||
|
||||
Process.sleep(interval)
|
||||
retry_state = %{retry_state | tries: tries + 1}
|
||||
advisory_lock(meta, opts, lock, retry_state, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def structure_dump(default, config) do
|
||||
table = config[:migration_source] || "schema_migrations"
|
||||
|
||||
with {:ok, versions} <- select_versions(table, config),
|
||||
{:ok, path} <- pg_dump(default, config),
|
||||
do: append_versions(table, versions, path)
|
||||
end
|
||||
|
||||
defp select_versions(table, config) do
|
||||
prefixes = config[:dump_prefixes] || ["public"]
|
||||
|
||||
result =
|
||||
Enum.reduce_while(prefixes, [], fn prefix, versions ->
|
||||
case run_query(~s[SELECT version FROM #{prefix}."#{table}" ORDER BY version], config) do
|
||||
{:ok, %{rows: rows}} -> {:cont, Enum.map(rows, &{prefix, hd(&1)}) ++ versions}
|
||||
{:error, %{postgres: %{code: :undefined_table}}} -> {:cont, versions}
|
||||
{:error, _} = error -> {:halt, error}
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:error, _} = error -> error
|
||||
versions -> {:ok, versions}
|
||||
end
|
||||
end
|
||||
|
||||
defp pg_dump(default, config) do
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
prefixes = config[:dump_prefixes] || []
|
||||
non_prefix_args = ["--file", path, "--schema-only", "--no-acl", "--no-owner"]
|
||||
|
||||
args =
|
||||
Enum.reduce(prefixes, non_prefix_args, fn prefix, acc ->
|
||||
["-n", prefix | acc]
|
||||
end)
|
||||
|
||||
File.mkdir_p!(Path.dirname(path))
|
||||
|
||||
case run_with_cmd("pg_dump", config, args) do
|
||||
{_output, 0} ->
|
||||
{:ok, path}
|
||||
|
||||
{output, _} ->
|
||||
{:error, output}
|
||||
end
|
||||
end
|
||||
|
||||
defp append_versions(_table, [], path) do
|
||||
{:ok, path}
|
||||
end
|
||||
|
||||
defp append_versions(table, versions, path) do
|
||||
sql =
|
||||
Enum.map_join(versions, fn {prefix, version} ->
|
||||
~s[INSERT INTO #{prefix}."#{table}" (version) VALUES (#{version});\n]
|
||||
end)
|
||||
|
||||
File.open!(path, [:append], fn file ->
|
||||
IO.write(file, sql)
|
||||
end)
|
||||
|
||||
{:ok, path}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def structure_load(default, config) do
|
||||
path = config[:dump_path] || Path.join(default, "structure.sql")
|
||||
args = ["--quiet", "--file", path, "-vON_ERROR_STOP=1", "--single-transaction"]
|
||||
|
||||
case run_with_cmd("psql", config, args) do
|
||||
{_output, 0} -> {:ok, path}
|
||||
{output, _} -> {:error, output}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def dump_cmd(args, opts \\ [], config) when is_list(config) and is_list(args),
|
||||
do: run_with_cmd("pg_dump", config, args, opts)
|
||||
|
||||
## Helpers
|
||||
|
||||
defp run_query(sql, opts) do
|
||||
{:ok, _} = Application.ensure_all_started(:ecto_sql)
|
||||
{:ok, _} = Application.ensure_all_started(:postgrex)
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.drop([:name, :log, :pool, :pool_size])
|
||||
|> Keyword.put(:backoff_type, :stop)
|
||||
|> Keyword.put(:max_restarts, 0)
|
||||
|
||||
task =
|
||||
Task.Supervisor.async_nolink(Ecto.Adapters.SQL.StorageSupervisor, fn ->
|
||||
{:ok, conn} = Postgrex.start_link(opts)
|
||||
|
||||
value = Postgrex.query(conn, sql, [], opts)
|
||||
GenServer.stop(conn)
|
||||
value
|
||||
end)
|
||||
|
||||
timeout = Keyword.get(opts, :timeout, 15_000)
|
||||
|
||||
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 [Postgrex.Error, DBConnection.Error] ->
|
||||
{:error, error}
|
||||
|
||||
{:exit, reason} ->
|
||||
{:error, RuntimeError.exception(Exception.format_exit(reason))}
|
||||
|
||||
nil ->
|
||||
{:error, RuntimeError.exception("command timed out")}
|
||||
end
|
||||
end
|
||||
|
||||
defp run_with_cmd(cmd, opts, opt_args, cmd_opts \\ []) do
|
||||
unless System.find_executable(cmd) do
|
||||
raise "could not find executable `#{cmd}` in path, " <>
|
||||
"please guarantee it is available before running ecto commands"
|
||||
end
|
||||
|
||||
env = [{"PGCONNECT_TIMEOUT", "10"}]
|
||||
|
||||
env =
|
||||
if password = opts[:password] do
|
||||
[{"PGPASSWORD", password} | env]
|
||||
else
|
||||
env
|
||||
end
|
||||
|
||||
args = []
|
||||
args = if username = opts[:username], do: ["--username", username | args], else: args
|
||||
args = if port = opts[:port], do: ["--port", to_string(port) | args], else: args
|
||||
args = if database = opts[:database], do: ["--dbname", database | args], else: args
|
||||
|
||||
host = opts[:socket_dir] || opts[:hostname] || System.get_env("PGHOST") || "localhost"
|
||||
|
||||
if opts[:socket] do
|
||||
IO.warn(
|
||||
":socket option is ignored when connecting in structure_load/2 and structure_dump/2," <>
|
||||
" use :socket_dir or :hostname instead"
|
||||
)
|
||||
end
|
||||
|
||||
args = ["--host", host | args]
|
||||
args = args ++ opt_args
|
||||
|
||||
cmd_opts =
|
||||
cmd_opts
|
||||
|> Keyword.put_new(:stderr_to_stdout, true)
|
||||
|> Keyword.update(:env, env, &Enum.concat(env, &1))
|
||||
|
||||
System.cmd(cmd, args, cmd_opts)
|
||||
end
|
||||
end
|
||||
2080
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres/connection.ex
Normal file
2080
phoenix/deps/ecto_sql/lib/ecto/adapters/postgres/connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
1522
phoenix/deps/ecto_sql/lib/ecto/adapters/sql.ex
Normal file
1522
phoenix/deps/ecto_sql/lib/ecto/adapters/sql.ex
Normal file
File diff suppressed because it is too large
Load Diff
14
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/application.ex
Normal file
14
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/application.ex
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule Ecto.Adapters.SQL.Application do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{DynamicSupervisor, strategy: :one_for_one, name: Ecto.MigratorSupervisor},
|
||||
{Task.Supervisor, name: Ecto.Adapters.SQL.StorageSupervisor}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Ecto.Adapters.SQL.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
154
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/connection.ex
Normal file
154
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/connection.ex
Normal file
@@ -0,0 +1,154 @@
|
||||
defmodule Ecto.Adapters.SQL.Connection do
|
||||
@moduledoc """
|
||||
Specifies the behaviour to be implemented by all SQL connections.
|
||||
"""
|
||||
|
||||
@typedoc "The query name"
|
||||
@type name :: String.t()
|
||||
|
||||
@typedoc "The SQL statement"
|
||||
@type statement :: String.t()
|
||||
|
||||
@typedoc "The cached query which is a DBConnection Query"
|
||||
@type cached :: map
|
||||
|
||||
@type connection :: DBConnection.conn()
|
||||
@type params :: [term]
|
||||
|
||||
@doc """
|
||||
Receives options and returns `DBConnection` supervisor child
|
||||
specification.
|
||||
"""
|
||||
@callback child_spec(options :: Keyword.t()) :: :supervisor.child_spec() | {module, Keyword.t()}
|
||||
|
||||
@doc """
|
||||
Prepares and executes the given query with `DBConnection`.
|
||||
"""
|
||||
@callback prepare_execute(connection, name, statement, params, options :: Keyword.t()) ::
|
||||
{:ok, cached, term} | {:error, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Executes a cached query.
|
||||
"""
|
||||
@callback execute(connection, cached, params, options :: Keyword.t()) ::
|
||||
{:ok, cached, term} | {:ok, term} | {:error | :reset, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Runs the given statement as a query.
|
||||
"""
|
||||
@callback query(connection, statement, params, options :: Keyword.t()) ::
|
||||
{:ok, term} | {:error, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Runs the given statement as a multi-result query.
|
||||
"""
|
||||
@callback query_many(connection, statement, params, options :: Keyword.t()) ::
|
||||
{:ok, term} | {:error, Exception.t()}
|
||||
|
||||
@doc """
|
||||
Returns a stream that prepares and executes the given query with
|
||||
`DBConnection`.
|
||||
"""
|
||||
@callback stream(connection, statement, params, options :: Keyword.t()) ::
|
||||
Enum.t()
|
||||
|
||||
@doc """
|
||||
Receives the exception returned by `c:query/4`.
|
||||
|
||||
The constraints are in the keyword list and must return the
|
||||
constraint type, like `:unique`, and the constraint name as
|
||||
a string, for example:
|
||||
|
||||
[unique: "posts_title_index"]
|
||||
|
||||
Must return an empty list if the error does not come
|
||||
from any constraint.
|
||||
"""
|
||||
@callback to_constraints(exception :: Exception.t(), options :: Keyword.t()) :: Keyword.t()
|
||||
|
||||
## Queries
|
||||
|
||||
@doc """
|
||||
Receives a query and must return a SELECT query.
|
||||
"""
|
||||
@callback all(query :: Ecto.Query.t()) :: iodata
|
||||
|
||||
@doc """
|
||||
Receives a query and values to update and must return an UPDATE query.
|
||||
"""
|
||||
@callback update_all(query :: Ecto.Query.t()) :: iodata
|
||||
|
||||
@doc """
|
||||
Receives a query and must return a DELETE query.
|
||||
"""
|
||||
@callback delete_all(query :: Ecto.Query.t()) :: iodata
|
||||
|
||||
@doc """
|
||||
Returns an INSERT for the given `rows` in `table` returning
|
||||
the given `returning`.
|
||||
"""
|
||||
@callback insert(
|
||||
prefix :: String.t(),
|
||||
table :: String.t(),
|
||||
header :: [atom],
|
||||
rows :: [[atom | nil]],
|
||||
on_conflict :: Ecto.Adapter.Schema.on_conflict(),
|
||||
returning :: [atom],
|
||||
placeholders :: [term]
|
||||
) :: iodata
|
||||
|
||||
@doc """
|
||||
Returns an UPDATE for the given `fields` in `table` filtered by
|
||||
`filters` returning the given `returning`.
|
||||
"""
|
||||
@callback update(
|
||||
prefix :: String.t(),
|
||||
table :: String.t(),
|
||||
fields :: [atom],
|
||||
filters :: [atom],
|
||||
returning :: [atom]
|
||||
) :: iodata
|
||||
|
||||
@doc """
|
||||
Returns a DELETE for the `filters` returning the given `returning`.
|
||||
"""
|
||||
@callback delete(
|
||||
prefix :: String.t(),
|
||||
table :: String.t(),
|
||||
filters :: [atom],
|
||||
returning :: [atom]
|
||||
) :: iodata
|
||||
|
||||
@doc """
|
||||
Executes an EXPLAIN query or similar depending on the adapter to obtains statistics of the given query.
|
||||
|
||||
Receives the `connection`, `query`, `params` for the query,
|
||||
and all `opts` including those related to the EXPLAIN statement and shared opts.
|
||||
|
||||
Must execute the explain query and return the result.
|
||||
"""
|
||||
@callback explain_query(
|
||||
connection,
|
||||
query :: String.t(),
|
||||
params :: Keyword.t(),
|
||||
opts :: Keyword.t()
|
||||
) ::
|
||||
{:ok, term} | {:error, Exception.t()}
|
||||
|
||||
## DDL
|
||||
|
||||
@doc """
|
||||
Receives a DDL command and returns a query that executes it.
|
||||
"""
|
||||
@callback execute_ddl(command :: Ecto.Adapter.Migration.command()) :: String.t() | [iodata]
|
||||
|
||||
@doc """
|
||||
Receives a query result and returns a list of logs.
|
||||
"""
|
||||
@callback ddl_logs(result :: term) :: [{Logger.level(), Logger.message(), Logger.metadata()}]
|
||||
|
||||
@doc """
|
||||
Returns a queryable to check if the given `table` exists.
|
||||
"""
|
||||
@callback table_exists_query(table :: String.t()) :: {iodata, [term]}
|
||||
end
|
||||
705
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/sandbox.ex
Normal file
705
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/sandbox.ex
Normal file
@@ -0,0 +1,705 @@
|
||||
defmodule Ecto.Adapters.SQL.Sandbox do
|
||||
@moduledoc ~S"""
|
||||
A pool for concurrent transactional tests.
|
||||
|
||||
The sandbox pool is implemented on top of an ownership mechanism.
|
||||
When started, the pool is in automatic mode, which means the
|
||||
repository will automatically check connections out as with any
|
||||
other pool.
|
||||
|
||||
The `mode/2` function can be used to change the pool mode from
|
||||
automatic to either manual or shared. In the latter two modes,
|
||||
the connection must be explicitly checked out before use.
|
||||
When explicit checkouts are made, the sandbox will wrap the
|
||||
connection in a transaction by default and control who has
|
||||
access to it. This means developers have a safe mechanism for
|
||||
running concurrent tests against the database.
|
||||
|
||||
## Database support
|
||||
|
||||
While both PostgreSQL and MySQL support SQL Sandbox, only PostgreSQL
|
||||
supports concurrent tests while running the SQL Sandbox. Therefore, do
|
||||
not run concurrent tests with MySQL as you may run into deadlocks due to
|
||||
its transaction implementation.
|
||||
|
||||
## Example
|
||||
|
||||
The first step is to configure your database to use the
|
||||
`Ecto.Adapters.SQL.Sandbox` pool. You set those options in your
|
||||
`config/config.exs` (or preferably `config/test.exs`) if you
|
||||
haven't yet:
|
||||
|
||||
config :my_app, Repo,
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
Now with the test database properly configured, you can write
|
||||
transactional tests:
|
||||
|
||||
# At the end of your test_helper.exs
|
||||
# Set the pool mode to manual for explicit checkouts
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Repo, :manual)
|
||||
|
||||
defmodule PostTest do
|
||||
# Once the mode is manual, tests can also be async
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
setup do
|
||||
# Explicitly get a connection before each test
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo)
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "create post" do
|
||||
# Use the repository as usual
|
||||
assert %Post{} = Repo.insert!(%Post{})
|
||||
end
|
||||
end
|
||||
|
||||
## Collaborating processes
|
||||
|
||||
The example above is straight-forward because we have only
|
||||
a single process using the database connection. However,
|
||||
sometimes a test may need to interact with multiple processes,
|
||||
all using the same connection so they all belong to the same
|
||||
transaction.
|
||||
|
||||
Before we discuss solutions, let's see what happens if we try
|
||||
to use a connection from a new process without explicitly
|
||||
checking it out first:
|
||||
|
||||
setup do
|
||||
# Explicitly get a connection before each test
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||
end
|
||||
|
||||
test "calls worker that runs a query" do
|
||||
GenServer.call(MyApp.Worker, :run_query)
|
||||
end
|
||||
|
||||
The test above will fail with an error similar to:
|
||||
|
||||
** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.35.0>
|
||||
|
||||
That's because the `setup` block is checking out the connection only
|
||||
for the test process. Once the worker attempts to perform a query,
|
||||
there is no connection assigned to it and it will fail.
|
||||
|
||||
The sandbox module provides two ways of doing so, via allowances or
|
||||
by running in shared mode.
|
||||
|
||||
### Allowances
|
||||
|
||||
The idea behind allowances is that you can explicitly tell a process
|
||||
which checked out connection it should use, allowing multiple processes
|
||||
to collaborate over the same connection. Let's give it a try:
|
||||
|
||||
test "calls worker that runs a query" do
|
||||
allow = Process.whereis(MyApp.Worker)
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), allow)
|
||||
GenServer.call(MyApp.Worker, :run_query)
|
||||
end
|
||||
|
||||
And that's it, by calling `allow/3`, we are explicitly assigning
|
||||
the parent's connection (i.e. the test process' connection) to
|
||||
the task.
|
||||
|
||||
Besides calling `allow/3` allowance can also be provided to processes
|
||||
via [Caller Tracking](`m:Task#module-ancestor-and-caller-tracking`).
|
||||
|
||||
Because allowances use an explicit mechanism, their advantage
|
||||
is that you can still run your tests in async mode. The downside
|
||||
is that you need to explicitly control and allow every single
|
||||
process. This is not always possible. In such cases, you will
|
||||
want to use shared mode.
|
||||
|
||||
### Shared mode
|
||||
|
||||
Shared mode allows a process to share its connection with any other
|
||||
process automatically, without relying on explicit allowances.
|
||||
Let's change the example above to use shared mode:
|
||||
|
||||
setup do
|
||||
# Explicitly get a connection before each test
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||
# Setting the shared mode must be done only after checkout
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||
end
|
||||
|
||||
test "calls worker that runs a query" do
|
||||
GenServer.call(MyApp.Worker, :run_query)
|
||||
end
|
||||
|
||||
By calling `mode({:shared, self()})`, any process that needs
|
||||
to talk to the database will now use the same connection as the
|
||||
one checked out by the test process during the `setup` block.
|
||||
|
||||
Make sure to always check a connection out before setting the mode
|
||||
to `{:shared, self()}`.
|
||||
|
||||
The advantage of shared mode is that by calling a single function,
|
||||
you will ensure all upcoming processes and operations will use that
|
||||
shared connection, without a need to explicitly allow them. The
|
||||
downside is that tests can no longer run concurrently in shared mode.
|
||||
|
||||
Also, beware that if the test process terminates while the worker is
|
||||
using the connection, the connection will be taken away from the worker,
|
||||
which will error. Therefore it is important to guarantee the work is done
|
||||
before the test concludes. In the example above, we are using a `call`,
|
||||
which is synchronous, avoiding the problem, but you may need to explicitly
|
||||
flush the worker or terminate it under such scenarios in your tests.
|
||||
|
||||
### Summing up
|
||||
|
||||
There are two mechanisms for explicit ownerships:
|
||||
|
||||
* Using allowances - requires explicit allowances.
|
||||
Tests may run concurrently.
|
||||
|
||||
* Using shared mode - does not require explicit allowances.
|
||||
Tests cannot run concurrently.
|
||||
|
||||
## FAQ
|
||||
|
||||
When running the sandbox mode concurrently, developers may run into
|
||||
issues we explore in the upcoming sections.
|
||||
|
||||
### "owner exited"
|
||||
|
||||
In some situations, you may see error reports similar to the one below:
|
||||
|
||||
23:59:59.999 [error] Postgrex.Protocol (#PID<>) disconnected:
|
||||
** (DBConnection.Error) owner #PID<> exited
|
||||
Client #PID<> is still using a connection from owner
|
||||
|
||||
Such errors are usually followed by another error report from another
|
||||
process that failed while executing a database query.
|
||||
|
||||
To understand the failure, we need to answer the question: who are the
|
||||
owner and client processes? The owner process is the one that checks
|
||||
out the connection, which, in the majority of cases, is the test process,
|
||||
the one running your tests. In other words, the error happens because
|
||||
the test process has finished, either because the test succeeded or
|
||||
because it failed, while the client process was trying to get information
|
||||
from the database. Since the owner process, the one that owns the
|
||||
connection, no longer exists, Ecto will check the connection back in
|
||||
and notify the client process using the connection that the connection
|
||||
owner is no longer available.
|
||||
|
||||
This can happen in different situations. For example, imagine you query
|
||||
a GenServer in your test that is using a database connection:
|
||||
|
||||
test "gets results from GenServer" do
|
||||
{:ok, pid} = MyAppServer.start_link()
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
|
||||
assert MyAppServer.get_my_data_fast(timeout: 1000) == [...]
|
||||
end
|
||||
|
||||
In the test above, we spawn the server and allow it to perform database
|
||||
queries using the connection owned by the test process. Since we gave
|
||||
a timeout of 1 second, in case the database takes longer than one second
|
||||
to reply, the test process will fail, due to the timeout, making the
|
||||
"owner down" message to be printed because the server process is still
|
||||
waiting on a connection reply.
|
||||
|
||||
In some situations, such failures may be intermittent. Imagine that you
|
||||
allow a process that queries the database every half second:
|
||||
|
||||
test "queries periodically" do
|
||||
{:ok, pid} = PeriodicServer.start_link()
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
|
||||
# assertions
|
||||
end
|
||||
|
||||
Because the server is querying the database from time to time, there is
|
||||
a chance that, when the test exits, the periodic process may be querying
|
||||
the database, regardless of test success or failure.
|
||||
|
||||
To address this, you can tell ExUnit to manage your processes:
|
||||
|
||||
test "queries periodically" do
|
||||
pid = start_supervised!(PeriodicServer)
|
||||
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
|
||||
# assertions
|
||||
end
|
||||
|
||||
By using `start_supervised!/1`, ExUnit guarantees the process finishes
|
||||
before your test (the connection owner).
|
||||
|
||||
In some situations, however, the dynamic processes are directly started
|
||||
inside a `DynamicSupervisor` or a `Task.Supervisor`. You can guarantee
|
||||
proper termination in such scenarios by adding an `on_exit` callback
|
||||
that waits until all supervised children terminate:
|
||||
|
||||
on_exit(fn ->
|
||||
for {_, pid, _, _} <- DynamicSupervisor.which_children(MyApp.DynamicSupervisor) do
|
||||
ref = Process.monitor(pid)
|
||||
assert_receive {:DOWN, ^ref, _, _, _}, :infinity
|
||||
end
|
||||
end)
|
||||
|
||||
### "owner timed out because it owned the connection for longer than Nms"
|
||||
|
||||
In some situations, you may see error reports similar to the one below:
|
||||
|
||||
09:56:43.081 [error] Postgrex.Protocol (#PID<>) disconnected:
|
||||
** (DBConnection.ConnectionError) owner #PID<> timed out
|
||||
because it owned the connection for longer than 120000ms
|
||||
|
||||
If you have a long running test (or you're debugging with IEx.pry),
|
||||
the timeout for the connection ownership may be too short. You can
|
||||
increase the timeout by setting the `:ownership_timeout` options for
|
||||
your repo config in `config/config.exs` (or preferably in `config/test.exs`):
|
||||
|
||||
config :my_app, MyApp.Repo,
|
||||
ownership_timeout: NEW_TIMEOUT_IN_MILLISECONDS
|
||||
|
||||
The `:ownership_timeout` option is part of `DBConnection.Ownership`
|
||||
and defaults to 120000ms. Timeouts are given as integers in milliseconds.
|
||||
|
||||
Alternately, if this is an issue for only a handful of long-running tests,
|
||||
you can pass an `:ownership_timeout` option when calling
|
||||
`Ecto.Adapters.SQL.Sandbox.checkout/2` instead of setting a longer timeout
|
||||
globally in your config.
|
||||
|
||||
### Deferred constraints
|
||||
|
||||
Some databases allow to defer constraint validation to the transaction
|
||||
commit time, instead of the particular statement execution time. This
|
||||
feature, for instance, allows for a cyclic foreign key referencing.
|
||||
Since the SQL Sandbox mode rolls back transactions, tests might report
|
||||
false positives because deferred constraints are never checked by the
|
||||
database. To manually force deferred constraints validation when using
|
||||
PostgreSQL use the following line right at the end of your test case:
|
||||
|
||||
Repo.query!("SET CONSTRAINTS ALL IMMEDIATE")
|
||||
|
||||
### Database locks and deadlocks
|
||||
|
||||
Since the sandbox relies on concurrent transactional tests, there is
|
||||
a chance your tests may trigger deadlocks in your database. This is
|
||||
specially true with MySQL, where the solutions presented here are not
|
||||
enough to avoid deadlocks and therefore making the use of concurrent tests
|
||||
with MySQL prohibited.
|
||||
|
||||
However, even on databases like PostgreSQL, performance degradations or
|
||||
deadlocks may still occur. For example, imagine a "users" table with a
|
||||
unique index on the "email" column. Now consider multiple tests are
|
||||
trying to insert the same user email to the database. They will attempt
|
||||
to retrieve the same database lock, causing only one test to succeed and
|
||||
run while all other tests wait for the lock.
|
||||
|
||||
In other situations, two different tests may proceed in a way that
|
||||
each test retrieves locks desired by the other, leading to a situation
|
||||
that cannot be resolved, a deadlock. For instance:
|
||||
|
||||
```text
|
||||
Transaction 1: Transaction 2:
|
||||
begin
|
||||
begin
|
||||
update posts where id = 1
|
||||
update posts where id = 2
|
||||
update posts where id = 1
|
||||
update posts where id = 2
|
||||
**deadlock**
|
||||
```
|
||||
|
||||
There are different ways to avoid such problems. One of them is
|
||||
to make sure your tests work on distinct data. Regardless of
|
||||
your choice between using fixtures or factories for test data,
|
||||
make sure you get a new set of data per test. This is specially
|
||||
important for data that is meant to be unique like user emails.
|
||||
|
||||
For example, instead of:
|
||||
|
||||
def insert_user do
|
||||
Repo.insert!(%User{email: "sample@example.com"})
|
||||
end
|
||||
|
||||
prefer:
|
||||
|
||||
def insert_user do
|
||||
Repo.insert!(%User{email: "sample-#{counter()}@example.com"})
|
||||
end
|
||||
|
||||
defp counter do
|
||||
System.unique_integer([:positive])
|
||||
end
|
||||
|
||||
In fact, avoiding unique emails like above can also have a positive
|
||||
impact on the test suite performance, as it reduces contention and
|
||||
wait between concurrent tests. We have heard reports where using
|
||||
dynamic values for uniquely indexed columns, as we did for email
|
||||
above, made a test suite run between 2x to 3x faster.
|
||||
|
||||
Deadlocks may happen in other circumstances. If you believe you
|
||||
are hitting a scenario that has not been described here, please
|
||||
report an issue so we can improve our examples. As a last resort,
|
||||
you can always disable the test triggering the deadlock from
|
||||
running asynchronously by setting "async: false".
|
||||
"""
|
||||
|
||||
defmodule Connection do
|
||||
@moduledoc false
|
||||
if Code.ensure_loaded?(DBConnection) do
|
||||
@behaviour DBConnection
|
||||
end
|
||||
|
||||
def connect(_opts) do
|
||||
raise "should never be invoked"
|
||||
end
|
||||
|
||||
def disconnect(err, {conn_mod, state, _in_transaction?}) do
|
||||
conn_mod.disconnect(err, state)
|
||||
end
|
||||
|
||||
def checkout(state), do: proxy(:checkout, state, [])
|
||||
def checkin(state), do: proxy(:checkin, state, [])
|
||||
def ping(state), do: proxy(:ping, state, [])
|
||||
|
||||
def handle_begin(opts, {conn_mod, state, false}) do
|
||||
opts = [mode: :savepoint] ++ opts
|
||||
|
||||
case conn_mod.handle_begin(opts, state) do
|
||||
{:ok, value, state} ->
|
||||
{:ok, value, {conn_mod, state, true}}
|
||||
|
||||
{kind, err, state} ->
|
||||
{kind, err, {conn_mod, state, false}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_commit(opts, {conn_mod, state, true}) do
|
||||
opts = [mode: :savepoint] ++ opts
|
||||
proxy(:handle_commit, {conn_mod, state, false}, [opts])
|
||||
end
|
||||
|
||||
def handle_rollback(opts, {conn_mod, state, _}) do
|
||||
opts = [mode: :savepoint] ++ opts
|
||||
proxy(:handle_rollback, {conn_mod, state, false}, [opts])
|
||||
end
|
||||
|
||||
def handle_status(opts, state),
|
||||
do: proxy(:handle_status, state, [maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_prepare(query, opts, state),
|
||||
do: proxy(:handle_prepare, state, [query, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_execute(query, params, opts, state),
|
||||
do: proxy(:handle_execute, state, [query, params, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_close(query, opts, state),
|
||||
do: proxy(:handle_close, state, [query, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_declare(query, params, opts, state),
|
||||
do: proxy(:handle_declare, state, [query, params, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_fetch(query, cursor, opts, state),
|
||||
do: proxy(:handle_fetch, state, [query, cursor, maybe_savepoint(opts, state)])
|
||||
|
||||
def handle_deallocate(query, cursor, opts, state),
|
||||
do: proxy(:handle_deallocate, state, [query, cursor, maybe_savepoint(opts, state)])
|
||||
|
||||
defp maybe_savepoint(opts, {_, _, in_transaction?}) do
|
||||
if not in_transaction? and Keyword.get(opts, :sandbox_subtransaction, true) do
|
||||
[mode: :savepoint] ++ opts
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy(fun, {conn_mod, state, in_transaction?}, args) do
|
||||
result = apply(conn_mod, fun, args ++ [state])
|
||||
pos = :erlang.tuple_size(result)
|
||||
:erlang.setelement(pos, result, {conn_mod, :erlang.element(pos, result), in_transaction?})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a process that will check out and own a connection, then returns that process's pid.
|
||||
|
||||
The process is not linked to the caller, so it is your responsibility to ensure that it will be
|
||||
stopped with `stop_owner/1`. In tests, this is done in an `ExUnit.Callbacks.on_exit/2` callback:
|
||||
|
||||
setup tags do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
## `start_owner!/2` vs `checkout/2`
|
||||
|
||||
`start_owner!/2` should be used in place of `checkout/2`.
|
||||
|
||||
`start_owner!/2` solves the problem of unlinked processes started in a test outliving the test process and causing ownership errors.
|
||||
For example, `LiveView`'s `live(...)` test helper starts a process linked to the LiveView supervisor, not the test process.
|
||||
These errors can be eliminated by having the owner of the connection be a separate process from the test process.
|
||||
|
||||
Outside of that scenario, `checkout/2` involves less overhead than this function and so can be preferable.
|
||||
|
||||
## Options
|
||||
|
||||
* `:shared` - if `true`, the pool runs in the shared mode. Defaults to `false`
|
||||
|
||||
The remaining options are passed to `checkout/2`.
|
||||
"""
|
||||
@doc since: "3.4.4"
|
||||
@spec start_owner!(Ecto.Repo.t() | pid(), keyword()) :: pid()
|
||||
def start_owner!(repo, opts \\ []) do
|
||||
parent = self()
|
||||
|
||||
{:ok, pid} =
|
||||
Agent.start(fn ->
|
||||
{shared, opts} = Keyword.pop(opts, :shared, false)
|
||||
:ok = checkout(repo, opts)
|
||||
|
||||
if shared do
|
||||
:ok = mode(repo, {:shared, self()})
|
||||
else
|
||||
:ok = allow(repo, self(), parent)
|
||||
end
|
||||
end)
|
||||
|
||||
pid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops an owner process started by `start_owner!/2`.
|
||||
"""
|
||||
@doc since: "3.4.4"
|
||||
@spec stop_owner(pid()) :: :ok
|
||||
def stop_owner(pid) do
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the mode for the `repo` pool.
|
||||
|
||||
The modes can be:
|
||||
|
||||
* `:auto` - this is the default mode. When trying to use the repository,
|
||||
processes can automatically checkout a connection without calling
|
||||
`checkout/2` or `start_owner/2` before. This is the mode you will run
|
||||
on before your test suite starts
|
||||
|
||||
* `:manual` - in this mode, the connection always has to be explicitly
|
||||
checked before used. Other processes are allowed to use the same
|
||||
connection if they are explicitly allowed via `allow/4`. You usually
|
||||
set the mode to manual at the end of your `test/test_helper.exs` file.
|
||||
This is also the mode you will run your async tests in
|
||||
|
||||
* `{:shared, pid}` - after checking out a connection in manual mode,
|
||||
you can change the mode to `{:shared, pid}`, where pid is the process
|
||||
that owns the connection, most often `{:shared, self()}`. This makes it
|
||||
so all processes can use the same connection as the one owned by the
|
||||
current process. This is the mode you will run your sync tests in
|
||||
|
||||
Whenever you change the mode to `:manual` or `:auto`, all existing
|
||||
connections are checked in. Therefore, it is recommend to set those
|
||||
modes before your test suite starts, as otherwise you will check in
|
||||
connections being used in any other test running concurrently.
|
||||
|
||||
If successful, returns `:ok` (this is always successful for `:auto`
|
||||
and `:manual` modes). It may return `:not_owner` or `:not_found`
|
||||
when setting `{:shared, pid}` and the given `pid` does not own any
|
||||
connection for the repo. May return `:already_shared` if another
|
||||
process set the ownership mode to `{:shared, _}` and is still alive.
|
||||
"""
|
||||
@spec mode(Ecto.Repo.t() | pid(), :auto | :manual | {:shared, pid()}) ::
|
||||
:ok | :already_shared | :not_owner | :not_found
|
||||
def mode(repo, mode)
|
||||
when (is_atom(repo) or is_pid(repo)) and mode in [:auto, :manual]
|
||||
when (is_atom(repo) or is_pid(repo)) and elem(mode, 0) == :shared and is_pid(elem(mode, 1)) do
|
||||
%{pid: pool, opts: opts} = lookup_meta!(repo)
|
||||
DBConnection.Ownership.ownership_mode(pool, mode, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks a connection out for the given `repo`.
|
||||
|
||||
The process calling `checkout/2` will own the connection
|
||||
until it calls `checkin/2` or until it crashes in which case
|
||||
the connection will be automatically reclaimed by the pool.
|
||||
|
||||
If successful, returns `:ok`. If the caller already has a
|
||||
connection, it returns `{:already, :owner | :allowed}`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:sandbox` - when true the connection is wrapped in
|
||||
a transaction. Defaults to true.
|
||||
|
||||
* `:isolation` - set the query to the given isolation level.
|
||||
|
||||
* `:ownership_timeout` - limits how long the connection can be
|
||||
owned. Defaults to the value in your repo config in
|
||||
`config/config.exs` (or preferably in `config/test.exs`), or
|
||||
120000 ms if not set. The timeout exists for sanity checking
|
||||
purposes, to ensure there is no connection leakage, and can
|
||||
be bumped whenever necessary.
|
||||
|
||||
"""
|
||||
@spec checkout(Ecto.Repo.t() | pid(), keyword()) :: :ok | {:already, :owner | :allowed}
|
||||
def checkout(repo, opts \\ []) when is_atom(repo) or is_pid(repo) do
|
||||
%{pid: pool, opts: pool_opts} = lookup_meta!(repo)
|
||||
|
||||
pool_opts =
|
||||
if Keyword.get(opts, :sandbox, true) do
|
||||
[
|
||||
post_checkout: &post_checkout(&1, &2, opts),
|
||||
pre_checkin: &pre_checkin(&1, &2, &3, opts)
|
||||
] ++ pool_opts
|
||||
else
|
||||
pool_opts
|
||||
end
|
||||
|
||||
pool_opts_overrides = Keyword.take(opts, [:ownership_timeout, :isolation_level])
|
||||
pool_opts = Keyword.merge(pool_opts, pool_opts_overrides)
|
||||
|
||||
case DBConnection.Ownership.ownership_checkout(pool, pool_opts) do
|
||||
:ok ->
|
||||
if isolation = opts[:isolation] do
|
||||
set_transaction_isolation_level(repo, isolation)
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
defp set_transaction_isolation_level(repo, isolation) do
|
||||
query = "SET TRANSACTION ISOLATION LEVEL #{isolation}"
|
||||
|
||||
case Ecto.Adapters.SQL.query(repo, query, [], sandbox_subtransaction: false) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
checkin(repo, [])
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks in the connection back into the sandbox pool.
|
||||
"""
|
||||
@spec checkin(Ecto.Repo.t() | pid()) :: :ok | :not_owner | :not_found
|
||||
def checkin(repo, _opts \\ []) when is_atom(repo) or is_pid(repo) do
|
||||
%{pid: pool, opts: opts} = lookup_meta!(repo)
|
||||
DBConnection.Ownership.ownership_checkin(pool, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Allows the `allow` process to use the same connection as `parent`.
|
||||
|
||||
`allow` may be a PID or a locally registered name.
|
||||
|
||||
If the allowance is successful, this function returns `:ok`. If `allow` is already an
|
||||
owner or already allowed, it returns `{:already, :owner | :allowed}`. If `parent` has not
|
||||
checked out a connection from the repo, it returns `:not_found`.
|
||||
"""
|
||||
@spec allow(Ecto.Repo.t() | pid(), pid(), term()) ::
|
||||
:ok | {:already, :owner | :allowed} | :not_found
|
||||
def allow(repo, parent, allow, opts \\ []) when is_atom(repo) or is_pid(repo) do
|
||||
case GenServer.whereis(allow) do
|
||||
pid when is_pid(pid) ->
|
||||
%{pid: pool, opts: meta_opts} = lookup_meta!(repo)
|
||||
opts = Keyword.merge(meta_opts, opts)
|
||||
DBConnection.Ownership.ownership_allow(pool, parent, pid, opts)
|
||||
|
||||
other ->
|
||||
raise """
|
||||
only PID or a locally registered process can be allowed to \
|
||||
use the same connection as parent but the lookup returned #{inspect(other)}
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs a function outside of the sandbox.
|
||||
"""
|
||||
@spec unboxed_run(Ecto.Repo.t() | pid(), (-> result)) :: result when result: var
|
||||
def unboxed_run(repo, fun) when is_atom(repo) or is_pid(repo) do
|
||||
checkin(repo)
|
||||
checkout(repo, sandbox: false)
|
||||
|
||||
try do
|
||||
fun.()
|
||||
after
|
||||
checkin(repo)
|
||||
end
|
||||
end
|
||||
|
||||
defp lookup_meta!(repo) do
|
||||
%{opts: opts} =
|
||||
meta =
|
||||
repo
|
||||
|> find_repo()
|
||||
|> Ecto.Adapter.lookup_meta()
|
||||
|
||||
if opts[:pool] != DBConnection.Ownership do
|
||||
raise """
|
||||
cannot invoke sandbox operation with pool #{inspect(opts[:pool])}.
|
||||
To use the SQL Sandbox, configure your repository pool as:
|
||||
|
||||
pool: #{inspect(__MODULE__)}
|
||||
"""
|
||||
end
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
defp find_repo(repo) when is_atom(repo), do: repo.get_dynamic_repo()
|
||||
defp find_repo(repo), do: repo
|
||||
|
||||
defp post_checkout(conn_mod, conn_state, opts) do
|
||||
case conn_mod.handle_begin([mode: :transaction] ++ opts, conn_state) do
|
||||
{:ok, _, conn_state} ->
|
||||
{:ok, Connection, {conn_mod, conn_state, false}}
|
||||
|
||||
{:transaction, _conn_state} ->
|
||||
raise """
|
||||
Ecto SQL sandbox transaction cannot be started because there is already\
|
||||
a transaction running.
|
||||
|
||||
This either means some code is starting a transaction before the sandbox\
|
||||
or a connection was not appropriately rolled back after use.
|
||||
"""
|
||||
|
||||
{_error_or_disconnect, err, conn_state} ->
|
||||
{:disconnect, err, conn_mod, conn_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp pre_checkin(:checkin, Connection, {conn_mod, conn_state, _in_transaction?}, opts) do
|
||||
case conn_mod.handle_rollback([mode: :transaction] ++ opts, conn_state) do
|
||||
{:ok, _, conn_state} ->
|
||||
{:ok, conn_mod, conn_state}
|
||||
|
||||
{:idle, _conn_state} ->
|
||||
raise """
|
||||
Ecto SQL sandbox transaction was already committed/rolled back.
|
||||
|
||||
The sandbox works by running each test in a transaction and closing the\
|
||||
transaction afterwards. However, the transaction has already terminated.\
|
||||
Your test code is likely committing or rolling back transactions manually,\
|
||||
either by invoking procedures or running custom SQL commands.
|
||||
|
||||
One option is to manually checkout a connection without a sandbox:
|
||||
|
||||
Ecto.Adapters.SQL.Sandbox.checkout(repo, sandbox: false)
|
||||
|
||||
But remember you will have to undo any database changes performed by such tests.
|
||||
"""
|
||||
|
||||
{_error_or_disconnect, err, conn_state} ->
|
||||
{:disconnect, err, conn_mod, conn_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp pre_checkin(_, Connection, {conn_mod, conn_state, _in_transaction?}, _opts) do
|
||||
{:ok, conn_mod, conn_state}
|
||||
end
|
||||
end
|
||||
43
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/stream.ex
Normal file
43
phoenix/deps/ecto_sql/lib/ecto/adapters/sql/stream.ex
Normal file
@@ -0,0 +1,43 @@
|
||||
defmodule Ecto.Adapters.SQL.Stream do
|
||||
@moduledoc false
|
||||
|
||||
defstruct [:meta, :statement, :params, :opts]
|
||||
|
||||
def build(meta, statement, params, opts) do
|
||||
%__MODULE__{meta: meta, statement: statement, params: params, opts: opts}
|
||||
end
|
||||
end
|
||||
|
||||
alias Ecto.Adapters.SQL.Stream
|
||||
|
||||
defimpl Enumerable, for: Stream do
|
||||
def count(_), do: {:error, __MODULE__}
|
||||
|
||||
def member?(_, _), do: {:error, __MODULE__}
|
||||
|
||||
def slice(_), do: {:error, __MODULE__}
|
||||
|
||||
def reduce(stream, acc, fun) do
|
||||
%Stream{meta: meta, statement: statement, params: params, opts: opts} = stream
|
||||
Ecto.Adapters.SQL.reduce(meta, statement, params, opts, acc, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Stream do
|
||||
def into(stream) do
|
||||
%Stream{meta: meta, statement: statement, params: params, opts: opts} = stream
|
||||
{state, fun} = Ecto.Adapters.SQL.into(meta, statement, params, opts)
|
||||
{state, make_into(fun, stream)}
|
||||
end
|
||||
|
||||
defp make_into(fun, stream) do
|
||||
fn
|
||||
state, :done ->
|
||||
fun.(state, :done)
|
||||
stream
|
||||
|
||||
state, acc ->
|
||||
fun.(state, acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
296
phoenix/deps/ecto_sql/lib/ecto/adapters/tds.ex
Normal file
296
phoenix/deps/ecto_sql/lib/ecto/adapters/tds.ex
Normal file
@@ -0,0 +1,296 @@
|
||||
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
|
||||
1987
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/connection.ex
Normal file
1987
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
293
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/types.ex
Normal file
293
phoenix/deps/ecto_sql/lib/ecto/adapters/tds/types.ex
Normal file
@@ -0,0 +1,293 @@
|
||||
if Code.ensure_loaded?(Tds) do
|
||||
defmodule Tds.Ecto.UUID do
|
||||
@moduledoc """
|
||||
A TDS adapter type for UUIDs strings.
|
||||
|
||||
If you are using Tds adapter and UUIDs in your project, instead of `Ecto.UUID`
|
||||
you should use Tds.Ecto.UUID to generate correct bytes that should be stored
|
||||
in database.
|
||||
"""
|
||||
|
||||
use Ecto.Type
|
||||
|
||||
@typedoc """
|
||||
A hex-encoded UUID string.
|
||||
"""
|
||||
@type t :: <<_::288>>
|
||||
|
||||
@typedoc """
|
||||
A raw binary representation of a UUID.
|
||||
"""
|
||||
@type raw :: <<_::128>>
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def type(), do: :uuid
|
||||
|
||||
@doc """
|
||||
Casts to UUID.
|
||||
"""
|
||||
@impl true
|
||||
@spec cast(t | raw | any) :: {:ok, t} | :error
|
||||
def cast(
|
||||
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2,
|
||||
d3, d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
|
||||
) do
|
||||
<<c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-, c(b1), c(b2), c(b3), c(b4),
|
||||
?-, c(c1), c(c2), c(c3), c(c4), ?-, c(d1), c(d2), c(d3), c(d4), ?-, c(e1), c(e2), c(e3),
|
||||
c(e4), c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12)>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
casted -> {:ok, casted}
|
||||
end
|
||||
|
||||
def cast(<<bin::binary-size(16)>>), do: encode(bin)
|
||||
def cast(_), do: :error
|
||||
|
||||
@doc """
|
||||
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
|
||||
"""
|
||||
def cast!(value) do
|
||||
case cast(value) do
|
||||
{:ok, uuid} -> uuid
|
||||
:error -> raise Ecto.CastError, type: __MODULE__, value: value
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, c: 1}
|
||||
|
||||
defp c(?0), do: ?0
|
||||
defp c(?1), do: ?1
|
||||
defp c(?2), do: ?2
|
||||
defp c(?3), do: ?3
|
||||
defp c(?4), do: ?4
|
||||
defp c(?5), do: ?5
|
||||
defp c(?6), do: ?6
|
||||
defp c(?7), do: ?7
|
||||
defp c(?8), do: ?8
|
||||
defp c(?9), do: ?9
|
||||
defp c(?A), do: ?a
|
||||
defp c(?B), do: ?b
|
||||
defp c(?C), do: ?c
|
||||
defp c(?D), do: ?d
|
||||
defp c(?E), do: ?e
|
||||
defp c(?F), do: ?f
|
||||
defp c(?a), do: ?a
|
||||
defp c(?b), do: ?b
|
||||
defp c(?c), do: ?c
|
||||
defp c(?d), do: ?d
|
||||
defp c(?e), do: ?e
|
||||
defp c(?f), do: ?f
|
||||
defp c(_), do: throw(:error)
|
||||
|
||||
@doc """
|
||||
Converts a string representing a UUID into a binary.
|
||||
"""
|
||||
@impl true
|
||||
@spec dump(t | any) :: {:ok, raw} | :error
|
||||
def dump(
|
||||
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2,
|
||||
d3, d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
|
||||
) do
|
||||
try do
|
||||
<<d(a7)::4, d(a8)::4, d(a5)::4, d(a6)::4, d(a3)::4, d(a4)::4, d(a1)::4, d(a2)::4,
|
||||
d(b3)::4, d(b4)::4, d(b1)::4, d(b2)::4, d(c3)::4, d(c4)::4, d(c1)::4, d(c2)::4,
|
||||
d(d1)::4, d(d2)::4, d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4,
|
||||
d(e5)::4, d(e6)::4, d(e7)::4, d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
binary ->
|
||||
{:ok, binary}
|
||||
end
|
||||
end
|
||||
|
||||
def dump(_), do: :error
|
||||
|
||||
def dump!(value) do
|
||||
case dump(value) do
|
||||
{:ok, binary} -> binary
|
||||
:error -> raise ArgumentError, "Invalid uuid value #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, d: 1}
|
||||
|
||||
defp d(?0), do: 0
|
||||
defp d(?1), do: 1
|
||||
defp d(?2), do: 2
|
||||
defp d(?3), do: 3
|
||||
defp d(?4), do: 4
|
||||
defp d(?5), do: 5
|
||||
defp d(?6), do: 6
|
||||
defp d(?7), do: 7
|
||||
defp d(?8), do: 8
|
||||
defp d(?9), do: 9
|
||||
defp d(?A), do: 10
|
||||
defp d(?B), do: 11
|
||||
defp d(?C), do: 12
|
||||
defp d(?D), do: 13
|
||||
defp d(?E), do: 14
|
||||
defp d(?F), do: 15
|
||||
defp d(?a), do: 10
|
||||
defp d(?b), do: 11
|
||||
defp d(?c), do: 12
|
||||
defp d(?d), do: 13
|
||||
defp d(?e), do: 14
|
||||
defp d(?f), do: 15
|
||||
defp d(_), do: throw(:error)
|
||||
|
||||
@doc """
|
||||
Converts a binary UUID into a string.
|
||||
"""
|
||||
@impl true
|
||||
@spec load(raw | any) :: {:ok, t} | :error
|
||||
def load(<<_::128>> = uuid) do
|
||||
encode(uuid)
|
||||
end
|
||||
|
||||
def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
|
||||
raise ArgumentError,
|
||||
"trying to load string UUID as Tds.Ecto.UUID: #{inspect(string)}. " <>
|
||||
"Maybe you wanted to declare :uuid as your database field?"
|
||||
end
|
||||
|
||||
def load(_), do: :error
|
||||
|
||||
@doc """
|
||||
Generates a version 4 (random) UUID.
|
||||
"""
|
||||
@spec generate() :: t
|
||||
def generate do
|
||||
{:ok, uuid} = encode(bingenerate())
|
||||
uuid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a version 4 (random) UUID in the binary format.
|
||||
"""
|
||||
@spec bingenerate() :: raw
|
||||
def bingenerate do
|
||||
<<u0::56, u1::36, u2::28>> = :crypto.strong_rand_bytes(15)
|
||||
<<u0::56, 4::4, u1::36, 2::4, u2::28>>
|
||||
end
|
||||
|
||||
# Callback invoked by autogenerate fields.
|
||||
@impl true
|
||||
def autogenerate, do: generate()
|
||||
|
||||
defp encode(
|
||||
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
|
||||
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
|
||||
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
|
||||
) do
|
||||
<<e(a7), e(a8), e(a5), e(a6), e(a3), e(a4), e(a1), e(a2), ?-, e(b3), e(b4), e(b1), e(b2),
|
||||
?-, e(c3), e(c4), e(c1), e(c2), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3),
|
||||
e(e4), e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
|
||||
catch
|
||||
:error -> :error
|
||||
else
|
||||
encoded -> {:ok, encoded}
|
||||
end
|
||||
|
||||
@compile {:inline, e: 1}
|
||||
|
||||
defp e(0), do: ?0
|
||||
defp e(1), do: ?1
|
||||
defp e(2), do: ?2
|
||||
defp e(3), do: ?3
|
||||
defp e(4), do: ?4
|
||||
defp e(5), do: ?5
|
||||
defp e(6), do: ?6
|
||||
defp e(7), do: ?7
|
||||
defp e(8), do: ?8
|
||||
defp e(9), do: ?9
|
||||
defp e(10), do: ?a
|
||||
defp e(11), do: ?b
|
||||
defp e(12), do: ?c
|
||||
defp e(13), do: ?d
|
||||
defp e(14), do: ?e
|
||||
defp e(15), do: ?f
|
||||
end
|
||||
|
||||
defmodule Tds.Ecto.VarChar do
|
||||
@moduledoc """
|
||||
A Tds adapter Ecto Type that wraps erlang string into tuple so TDS driver
|
||||
can understand if erlang string should be encoded as NVarChar or Varchar.
|
||||
|
||||
Due to some limitations in Ecto and Tds driver, it is not possible to
|
||||
support collations other than the one set on connection during login.
|
||||
Please be aware of this limitation if you plan to store varchar values in
|
||||
your database using Ecto since you will probably lose some codepoints in
|
||||
the value during encoding. Instead use `tds_encoding` library and first
|
||||
encode value and then annotate it as `:binary` by calling `Ecto.Query.API.type/2`
|
||||
in your query. This way all codepoints will be properly preserved during
|
||||
insert to database.
|
||||
"""
|
||||
use Ecto.Type
|
||||
|
||||
@typedoc """
|
||||
An erlang string
|
||||
"""
|
||||
@type t :: String.t()
|
||||
|
||||
@typedoc """
|
||||
A value annotated as varchar.
|
||||
"""
|
||||
@type varchar :: {String.t(), :varchar}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def type(), do: :varchar
|
||||
|
||||
@doc """
|
||||
Casts to string.
|
||||
"""
|
||||
@spec cast(t | varchar | any) :: {:ok, t} | :error
|
||||
@impl true
|
||||
def cast({value, :varchar}) do
|
||||
# In case we get already dumped value
|
||||
{:ok, value}
|
||||
end
|
||||
|
||||
def cast(value) when is_binary(value) do
|
||||
{:ok, value}
|
||||
end
|
||||
|
||||
def cast(_), do: :error
|
||||
|
||||
@doc """
|
||||
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
|
||||
"""
|
||||
@spec cast!(t | varchar | any) :: t
|
||||
def cast!(value) do
|
||||
case cast(value) do
|
||||
{:ok, uuid} -> uuid
|
||||
:error -> raise Ecto.CastError, type: __MODULE__, value: value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads the DB type as is.
|
||||
"""
|
||||
@impl true
|
||||
@spec load(t | any) :: {:ok, t} | :error
|
||||
def load(value) do
|
||||
{:ok, value}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a string representing a VarChar into a tuple `{value, :varchar}`.
|
||||
|
||||
Returns `:error` if value is not binary.
|
||||
"""
|
||||
@impl true
|
||||
@spec dump(t | any) :: {:ok, varchar} | :error
|
||||
def dump(value) when is_binary(value) do
|
||||
{:ok, {value, :varchar}}
|
||||
end
|
||||
|
||||
def dump(_), do: :error
|
||||
end
|
||||
end
|
||||
1778
phoenix/deps/ecto_sql/lib/ecto/migration.ex
Normal file
1778
phoenix/deps/ecto_sql/lib/ecto/migration.ex
Normal file
File diff suppressed because it is too large
Load Diff
503
phoenix/deps/ecto_sql/lib/ecto/migration/runner.ex
Normal file
503
phoenix/deps/ecto_sql/lib/ecto/migration/runner.ex
Normal file
@@ -0,0 +1,503 @@
|
||||
defmodule Ecto.Migration.Runner do
|
||||
@moduledoc false
|
||||
use Agent, restart: :temporary
|
||||
|
||||
require Logger
|
||||
|
||||
alias Ecto.Migration.Table
|
||||
alias Ecto.Migration.Index
|
||||
alias Ecto.Migration.Constraint
|
||||
alias Ecto.Migration.Command
|
||||
|
||||
@doc """
|
||||
Runs the given migration.
|
||||
"""
|
||||
def run(repo, config, version, module, direction, operation, migrator_direction, opts) do
|
||||
level = Keyword.get(opts, :log, :info)
|
||||
sql = Keyword.get(opts, :log_migrations_sql, false)
|
||||
log = %{level: level, sql: sql}
|
||||
args = {self(), repo, config, module, direction, migrator_direction, log}
|
||||
|
||||
{:ok, runner} = DynamicSupervisor.start_child(Ecto.MigratorSupervisor, {__MODULE__, args})
|
||||
metadata(runner, opts)
|
||||
|
||||
log(level, "== Running #{version} #{inspect(module)}.#{operation}/0 #{direction}")
|
||||
{time, _} = :timer.tc(fn -> perform_operation(repo, module, operation) end)
|
||||
log(level, "== Migrated #{version} in #{inspect(div(time, 100_000) / 10)}s")
|
||||
after
|
||||
stop()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the runner metadata.
|
||||
"""
|
||||
def metadata(runner, opts) do
|
||||
prefix = opts[:prefix]
|
||||
Process.put(:ecto_migration, %{runner: runner, prefix: prefix && to_string(prefix)})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts the runner for the specified repo.
|
||||
"""
|
||||
def start_link({parent, repo, config, module, direction, migrator_direction, log}) do
|
||||
Agent.start_link(fn ->
|
||||
Process.link(parent)
|
||||
|
||||
%{
|
||||
direction: direction,
|
||||
repo: repo,
|
||||
migration: module,
|
||||
migrator_direction: migrator_direction,
|
||||
command: nil,
|
||||
subcommands: [],
|
||||
log: log,
|
||||
commands: [],
|
||||
config: config
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the runner.
|
||||
"""
|
||||
def stop() do
|
||||
Agent.stop(runner())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Accesses the given repository configuration.
|
||||
"""
|
||||
def repo_config(key, default) do
|
||||
Agent.get(runner(), &Keyword.get(&1.config, key, default))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the migrator command (up or down).
|
||||
|
||||
* forward + up: up
|
||||
* forward + down: down
|
||||
* forward + change: up
|
||||
* backward + change: down
|
||||
|
||||
"""
|
||||
def migrator_direction do
|
||||
Agent.get(runner(), & &1.migrator_direction)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the repo for this migration
|
||||
"""
|
||||
def repo do
|
||||
Agent.get(runner(), & &1.repo)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the prefix for this migration
|
||||
"""
|
||||
def prefix do
|
||||
case Process.get(:ecto_migration) do
|
||||
%{prefix: prefix} -> prefix
|
||||
_ -> raise "could not find migration runner process for #{inspect(self())}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes queue migration commands.
|
||||
|
||||
Reverses the order commands are executed when doing a rollback
|
||||
on a change/0 function and resets commands queue.
|
||||
"""
|
||||
def flush do
|
||||
%{commands: commands, direction: direction, repo: repo, log: log, migration: migration} =
|
||||
Agent.get_and_update(runner(), fn state -> {state, %{state | commands: []}} end)
|
||||
|
||||
commands = if direction == :backward, do: commands, else: Enum.reverse(commands)
|
||||
|
||||
for command <- commands do
|
||||
execute_in_direction(repo, migration, direction, log, command)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Queues command tuples or strings for execution.
|
||||
|
||||
Ecto.MigrationError will be raised when the server
|
||||
is in `:backward` direction and `command` is irreversible.
|
||||
"""
|
||||
def execute(command) do
|
||||
reply =
|
||||
Agent.get_and_update(runner(), fn
|
||||
%{command: nil} = state ->
|
||||
{:ok, %{state | subcommands: [], commands: [command | state.commands]}}
|
||||
|
||||
%{command: _} = state ->
|
||||
{:error, %{state | command: nil}}
|
||||
end)
|
||||
|
||||
case reply do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
raise Ecto.MigrationError, "cannot execute nested commands"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a command.
|
||||
"""
|
||||
def start_command(command) do
|
||||
reply =
|
||||
Agent.get_and_update(runner(), fn
|
||||
%{command: nil} = state ->
|
||||
{:ok, %{state | command: command}}
|
||||
|
||||
%{command: _} = state ->
|
||||
{:error, %{state | command: command}}
|
||||
end)
|
||||
|
||||
case reply do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
raise Ecto.MigrationError, "cannot execute nested commands"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Queues and clears current command. Must call `start_command/1` first.
|
||||
"""
|
||||
def end_command do
|
||||
Agent.update(runner(), fn state ->
|
||||
{operation, object} = state.command
|
||||
command = {operation, object, Enum.reverse(state.subcommands)}
|
||||
%{state | command: nil, subcommands: [], commands: [command | state.commands]}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a subcommand to the current command. Must call `start_command/1` first.
|
||||
"""
|
||||
def subcommand(subcommand) do
|
||||
reply =
|
||||
Agent.get_and_update(runner(), fn
|
||||
%{command: nil} = state ->
|
||||
{:error, state}
|
||||
|
||||
state ->
|
||||
{:ok, update_in(state.subcommands, &[subcommand | &1])}
|
||||
end)
|
||||
|
||||
case reply do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
raise Ecto.MigrationError, message: "cannot execute command outside of block"
|
||||
end
|
||||
end
|
||||
|
||||
## Execute
|
||||
|
||||
defp execute_in_direction(repo, migration, :forward, log, %Command{up: up}) do
|
||||
log_and_execute_ddl(repo, migration, log, up)
|
||||
end
|
||||
|
||||
defp execute_in_direction(repo, migration, :forward, log, command) do
|
||||
log_and_execute_ddl(repo, migration, log, command)
|
||||
end
|
||||
|
||||
defp execute_in_direction(repo, migration, :backward, log, %Command{down: down}) do
|
||||
log_and_execute_ddl(repo, migration, log, down)
|
||||
end
|
||||
|
||||
defp execute_in_direction(repo, migration, :backward, log, command) do
|
||||
if reversed = reverse(command) do
|
||||
log_and_execute_ddl(repo, migration, log, reversed)
|
||||
else
|
||||
raise Ecto.MigrationError,
|
||||
message:
|
||||
"cannot reverse migration command: #{command(command)}. " <>
|
||||
"You will need to explicitly define up/0 and down/0 in your migration"
|
||||
end
|
||||
end
|
||||
|
||||
defp reverse({:create, %Index{} = index}),
|
||||
do: {:drop, index, :restrict}
|
||||
|
||||
defp reverse({:create_if_not_exists, %Index{} = index}),
|
||||
do: {:drop_if_exists, index, :restrict}
|
||||
|
||||
defp reverse({:drop, %Index{} = index, _}),
|
||||
do: {:create, index}
|
||||
|
||||
defp reverse({:drop_if_exists, %Index{} = index, _}),
|
||||
do: {:create_if_not_exists, index}
|
||||
|
||||
defp reverse({:rename, %Index{} = index, new_name}),
|
||||
do: {:rename, %{index | name: new_name}, index.name}
|
||||
|
||||
defp reverse({:create, %Table{} = table, _columns}),
|
||||
do: {:drop, table, :restrict}
|
||||
|
||||
defp reverse({:create_if_not_exists, %Table{} = table, _columns}),
|
||||
do: {:drop_if_exists, table, :restrict}
|
||||
|
||||
defp reverse({:rename, %Table{} = table_current, %Table{} = table_new}),
|
||||
do: {:rename, table_new, table_current}
|
||||
|
||||
defp reverse({:rename, %Table{} = table, current_column, new_column}),
|
||||
do: {:rename, table, new_column, current_column}
|
||||
|
||||
defp reverse({:alter, %Table{} = table, changes}) do
|
||||
if reversed = table_reverse(changes, []) do
|
||||
{:alter, table, reversed}
|
||||
end
|
||||
end
|
||||
|
||||
# It is not a good idea to reverse constraints because
|
||||
# we can't guarantee data integrity when applying them back.
|
||||
defp reverse({:create_if_not_exists, %Constraint{} = constraint}),
|
||||
do: {:drop_if_exists, constraint, :restrict}
|
||||
|
||||
defp reverse({:create, %Constraint{} = constraint}),
|
||||
do: {:drop, constraint, :restrict}
|
||||
|
||||
defp reverse(_command), do: false
|
||||
|
||||
defp table_reverse([{:remove, name, type, opts} | t], acc) do
|
||||
table_reverse(t, [{:add, name, type, opts} | acc])
|
||||
end
|
||||
|
||||
defp table_reverse([{:modify, name, type, opts} | t], acc) do
|
||||
case opts[:from] do
|
||||
nil ->
|
||||
false
|
||||
|
||||
{reverse_type, from_opts} when is_list(from_opts) ->
|
||||
reverse_from = {type, Keyword.delete(opts, :from)}
|
||||
reverse_opts = Keyword.put(from_opts, :from, reverse_from)
|
||||
table_reverse(t, [{:modify, name, reverse_type, reverse_opts} | acc])
|
||||
|
||||
reverse_type ->
|
||||
reverse_opts = Keyword.put(opts, :from, type)
|
||||
table_reverse(t, [{:modify, name, reverse_type, reverse_opts} | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp table_reverse([{:add, name, type, _opts} | t], acc) do
|
||||
table_reverse(t, [{:remove, name, type, []} | acc])
|
||||
end
|
||||
|
||||
defp table_reverse([_ | _], _acc) do
|
||||
false
|
||||
end
|
||||
|
||||
defp table_reverse([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp perform_operation(repo, module, operation) do
|
||||
if function_exported?(repo, :in_transaction?, 0) and repo.in_transaction?() do
|
||||
if function_exported?(module, :after_begin, 0) do
|
||||
module.after_begin()
|
||||
flush()
|
||||
end
|
||||
|
||||
apply(module, operation, [])
|
||||
flush()
|
||||
|
||||
if function_exported?(module, :before_commit, 0) do
|
||||
module.before_commit()
|
||||
flush()
|
||||
end
|
||||
else
|
||||
apply(module, operation, [])
|
||||
flush()
|
||||
end
|
||||
end
|
||||
|
||||
defp runner do
|
||||
case Process.get(:ecto_migration) do
|
||||
%{runner: runner} -> runner
|
||||
_ -> raise "could not find migration runner process for #{inspect(self())}"
|
||||
end
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(repo, migration, log, {instruction, %Index{} = index}) do
|
||||
maybe_warn_index_ddl_transaction(index, migration)
|
||||
maybe_warn_index_migration_lock(index, repo, migration)
|
||||
log_and_execute_ddl(repo, log, {instruction, index})
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(repo, _migration, log, command) do
|
||||
log_and_execute_ddl(repo, log, command)
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(_repo, _log, func) when is_function(func, 0) do
|
||||
func.()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp log_and_execute_ddl(repo, %{level: level, sql: sql}, command) do
|
||||
log(level, command(command))
|
||||
meta = Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
|
||||
{:ok, logs} = repo.__adapter__().execute_ddl(meta, command, timeout: :infinity, log: sql)
|
||||
|
||||
Enum.each(logs, fn {ddl_log_level, message, metadata} ->
|
||||
ddl_log(ddl_log_level, level, message, metadata)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ddl_log(_level, false, _msg, _metadata), do: :ok
|
||||
defp ddl_log(level, _, msg, metadata), do: log(level, msg, metadata)
|
||||
|
||||
defp log(level, msg, metadata \\ [])
|
||||
defp log(false, _msg, _metadata), do: :ok
|
||||
defp log(true, msg, metadata), do: Logger.log(:info, msg, metadata)
|
||||
defp log(level, msg, metadata), do: Logger.log(level, msg, metadata)
|
||||
|
||||
defp maybe_warn_index_ddl_transaction(%{concurrently: true} = index, migration) do
|
||||
migration_config = migration.__migration__()
|
||||
|
||||
if not migration_config[:disable_ddl_transaction] do
|
||||
IO.warn(
|
||||
"""
|
||||
Migration #{inspect(migration)} has set index `#{index.name}` on table \
|
||||
`#{index.table}` to concurrently but did not disable ddl transaction. \
|
||||
Please set:
|
||||
|
||||
use Ecto.Migration
|
||||
@disable_ddl_transaction true
|
||||
""",
|
||||
[]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_warn_index_ddl_transaction(_index, _migration), do: :ok
|
||||
|
||||
defp maybe_warn_index_migration_lock(%{concurrently: true} = index, repo, migration) do
|
||||
migration_lock_disabled = migration.__migration__()[:disable_migration_lock]
|
||||
lock_strategy = repo.config()[:migration_lock]
|
||||
adapter = repo.__adapter__()
|
||||
|
||||
case {migration_lock_disabled, adapter, lock_strategy} do
|
||||
{false, Ecto.Adapters.Postgres, :pg_advisory_lock} ->
|
||||
:ok
|
||||
|
||||
{false, Ecto.Adapters.Postgres, _} ->
|
||||
IO.warn(
|
||||
"""
|
||||
Migration #{inspect(migration)} has set index `#{index.name}` on table \
|
||||
`#{index.table}` to concurrently but did not disable migration lock. \
|
||||
Please set:
|
||||
|
||||
use Ecto.Migration
|
||||
@disable_migration_lock true
|
||||
|
||||
Alternatively, consider using advisory locks during migrations in the \
|
||||
repo configuration:
|
||||
|
||||
config #{inspect(repo)}, migration_lock: :pg_advisory_lock
|
||||
""",
|
||||
[]
|
||||
)
|
||||
|
||||
{false, _adapter, _migration_lock} ->
|
||||
IO.warn(
|
||||
"""
|
||||
Migration #{inspect(migration)} has set index `#{index.name}` on table \
|
||||
`#{index.table}` to concurrently but did not disable migration lock. \
|
||||
Please set:
|
||||
|
||||
use Ecto.Migration
|
||||
@disable_migration_lock true
|
||||
""",
|
||||
[]
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_warn_index_migration_lock(_index, _repo, _migration), do: :ok
|
||||
|
||||
defp command(ddl) when is_binary(ddl) or is_list(ddl),
|
||||
do: "execute #{inspect(ddl)}"
|
||||
|
||||
defp command({:create, %Table{} = table, _}),
|
||||
do: "create table #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:create_if_not_exists, %Table{} = table, _}),
|
||||
do: "create table if not exists #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:alter, %Table{} = table, _}),
|
||||
do: "alter table #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:drop, %Table{} = table, mode}),
|
||||
do: "drop table #{quote_name(table.prefix, table.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:drop_if_exists, %Table{} = table, mode}),
|
||||
do: "drop table if exists #{quote_name(table.prefix, table.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:create, %Index{} = index}),
|
||||
do: "create index #{quote_name(index.prefix, index.name)}"
|
||||
|
||||
defp command({:create_if_not_exists, %Index{} = index}),
|
||||
do: "create index if not exists #{quote_name(index.prefix, index.name)}"
|
||||
|
||||
defp command({:drop, %Index{} = index, mode}),
|
||||
do: "drop index #{quote_name(index.prefix, index.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:drop_if_exists, %Index{} = index, mode}),
|
||||
do: "drop index if exists #{quote_name(index.prefix, index.name)}#{drop_mode(mode)}"
|
||||
|
||||
defp command({:rename, %Index{} = index_current, new_name}),
|
||||
do: "rename index #{quote_name(index_current.prefix, index_current.name)} to #{new_name}"
|
||||
|
||||
defp command({:rename, %Table{} = current_table, %Table{} = new_table}),
|
||||
do:
|
||||
"rename table #{quote_name(current_table.prefix, current_table.name)} to #{quote_name(new_table.prefix, new_table.name)}"
|
||||
|
||||
defp command({:rename, %Table{} = table, current_column, new_column}),
|
||||
do:
|
||||
"rename column #{current_column} to #{new_column} on table #{quote_name(table.prefix, table.name)}"
|
||||
|
||||
defp command({:create, %Constraint{check: nil, exclude: nil}}),
|
||||
do: raise(ArgumentError, "a constraint must have either a check or exclude option")
|
||||
|
||||
defp command({:create, %Constraint{check: check, exclude: exclude}})
|
||||
when is_binary(check) and is_binary(exclude),
|
||||
do: raise(ArgumentError, "a constraint must not have both check and exclude options")
|
||||
|
||||
defp command({:create, %Constraint{check: check} = constraint}) when is_binary(check),
|
||||
do:
|
||||
"create check constraint #{constraint.name} on table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp command({:create, %Constraint{exclude: exclude} = constraint}) when is_binary(exclude),
|
||||
do:
|
||||
"create exclude constraint #{constraint.name} on table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp command({:drop, %Constraint{} = constraint, _}),
|
||||
do:
|
||||
"drop constraint #{constraint.name} from table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp command({:drop_if_exists, %Constraint{} = constraint, _}),
|
||||
do:
|
||||
"drop constraint if exists #{constraint.name} from table #{quote_name(constraint.prefix, constraint.table)}"
|
||||
|
||||
defp drop_mode(:restrict), do: ""
|
||||
defp drop_mode(:cascade), do: " cascade"
|
||||
|
||||
defp quote_name(nil, name), do: quote_name(name)
|
||||
defp quote_name(prefix, name), do: quote_name(prefix) <> "." <> quote_name(name)
|
||||
defp quote_name(name) when is_atom(name), do: quote_name(Atom.to_string(name))
|
||||
defp quote_name(name), do: name
|
||||
end
|
||||
81
phoenix/deps/ecto_sql/lib/ecto/migration/schema_migration.ex
Normal file
81
phoenix/deps/ecto_sql/lib/ecto/migration/schema_migration.ex
Normal file
@@ -0,0 +1,81 @@
|
||||
defmodule Ecto.Migration.SchemaMigration do
|
||||
# Defines a schema that works with a table that tracks schema migrations.
|
||||
# The table name defaults to `schema_migrations`.
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
@primary_key false
|
||||
schema "schema_migrations" do
|
||||
field :version, :integer, primary_key: true
|
||||
timestamps updated_at: false
|
||||
end
|
||||
|
||||
# The migration flag is used to signal to the repository
|
||||
# we are in a migration operation.
|
||||
@default_opts [
|
||||
timeout: :infinity,
|
||||
log: false,
|
||||
# Keep schema_migration for backwards compatibility
|
||||
schema_migration: true,
|
||||
ecto_query: :schema_migration,
|
||||
telemetry_options: [schema_migration: true]
|
||||
]
|
||||
|
||||
def ensure_schema_migrations_table!(repo, config, opts) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
table_name = String.to_atom(source)
|
||||
table = %Ecto.Migration.Table{name: table_name, prefix: opts[:prefix]}
|
||||
meta = Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
|
||||
|
||||
commands = [
|
||||
{:add, :version, :bigint, primary_key: true},
|
||||
{:add, :inserted_at, :naive_datetime, []}
|
||||
]
|
||||
|
||||
repo.__adapter__().execute_ddl(meta, {:create_if_not_exists, table, commands}, @default_opts)
|
||||
end
|
||||
|
||||
def versions(repo, config, prefix) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
from_opts = [prefix: prefix] ++ @default_opts
|
||||
|
||||
query =
|
||||
if Keyword.get(config, :migration_cast_version_column, false) do
|
||||
from(m in source, select: type(m.version, :integer))
|
||||
else
|
||||
from(m in source, select: m.version)
|
||||
end
|
||||
|
||||
{repo, query, from_opts}
|
||||
end
|
||||
|
||||
def up(repo, config, version, opts) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
|
||||
%__MODULE__{version: version}
|
||||
|> Ecto.put_meta(source: source)
|
||||
|> repo.insert(default_opts(opts))
|
||||
end
|
||||
|
||||
def down(repo, config, version, opts) do
|
||||
{repo, source} = get_repo_and_source(repo, config)
|
||||
|
||||
from(m in source, where: m.version == type(^version, :integer))
|
||||
|> repo.delete_all(default_opts(opts))
|
||||
end
|
||||
|
||||
def get_repo_and_source(repo, config) do
|
||||
{Keyword.get(config, :migration_repo, repo),
|
||||
Keyword.get(config, :migration_source, "schema_migrations")}
|
||||
end
|
||||
|
||||
defp default_opts(opts) do
|
||||
Keyword.merge(
|
||||
@default_opts,
|
||||
prefix: opts[:prefix],
|
||||
log: Keyword.get(opts, :log_migrator_sql, false)
|
||||
)
|
||||
end
|
||||
end
|
||||
861
phoenix/deps/ecto_sql/lib/ecto/migrator.ex
Normal file
861
phoenix/deps/ecto_sql/lib/ecto/migrator.ex
Normal file
@@ -0,0 +1,861 @@
|
||||
defmodule Ecto.Migrator do
|
||||
@moduledoc """
|
||||
Lower level API for managing migrations.
|
||||
|
||||
EctoSQL provides three mix tasks for running and managing migrations:
|
||||
|
||||
* `mix ecto.migrate` - migrates a repository
|
||||
* `mix ecto.rollback` - rolls back a particular migration
|
||||
* `mix ecto.migrations` - shows all migrations and their status
|
||||
|
||||
Those tasks are built on top of the functions in this module.
|
||||
While the tasks above cover most use cases, it may be necessary
|
||||
from time to time to jump into the lower level API. For example,
|
||||
if you are assembling an Elixir release, Mix is not available,
|
||||
so this module provides a nice complement to still migrate your
|
||||
system.
|
||||
|
||||
To learn more about migrations in general, see `Ecto.Migration`.
|
||||
|
||||
## Example: Running an individual migration
|
||||
|
||||
Imagine you have this migration:
|
||||
|
||||
defmodule MyApp.MigrationExample do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute "CREATE TABLE users(id serial PRIMARY_KEY, username text)"
|
||||
end
|
||||
|
||||
def down do
|
||||
execute "DROP TABLE users"
|
||||
end
|
||||
end
|
||||
|
||||
You can execute it manually with:
|
||||
|
||||
Ecto.Migrator.up(Repo, 20080906120000, MyApp.MigrationExample)
|
||||
|
||||
## Example: Running migrations in a release
|
||||
|
||||
Elixir v1.9 introduces `mix release`, which generates a self-contained
|
||||
directory that consists of your application code, all of its dependencies,
|
||||
plus the whole Erlang Virtual Machine (VM) and runtime.
|
||||
|
||||
When a release is assembled, Mix is no longer available inside a release
|
||||
and therefore none of the Mix tasks. Users may still need a mechanism to
|
||||
migrate their databases. This can be achieved with using the `Ecto.Migrator`
|
||||
module:
|
||||
|
||||
defmodule MyApp.Release do
|
||||
@app :my_app
|
||||
|
||||
def migrate do
|
||||
for repo <- repos() do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
end
|
||||
end
|
||||
|
||||
def rollback(repo, version) do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||
end
|
||||
|
||||
defp repos do
|
||||
Application.load(@app)
|
||||
Application.fetch_env!(@app, :ecto_repos)
|
||||
end
|
||||
end
|
||||
|
||||
The example above uses `with_repo/3` to make sure the repository is
|
||||
started and then runs all migrations up or a given migration down.
|
||||
Note you will have to replace `MyApp` and `:my_app` on the first two
|
||||
lines by your actual application name. Once the file above is added
|
||||
to your application, you can assemble a new release and invoke the
|
||||
commands above in the release root like this:
|
||||
|
||||
$ bin/my_app eval "MyApp.Release.migrate"
|
||||
$ bin/my_app eval "MyApp.Release.rollback(MyApp.Repo, 20190417140000)"
|
||||
|
||||
## Example: Running migrations on application startup
|
||||
|
||||
Add the following to the top of your application children spec:
|
||||
|
||||
{Ecto.Migrator,
|
||||
repos: Application.fetch_env!(:my_app, :ecto_repos),
|
||||
skip: System.get_env("SKIP_MIGRATIONS") == "true"}
|
||||
|
||||
To skip migrations you can also pass `skip: true` or as in the example
|
||||
set the environment variable `SKIP_MIGRATIONS` to a truthy value.
|
||||
|
||||
And all other options described in `up/4` are allowed,
|
||||
for example if you want to log the SQL commands,
|
||||
and run migrations in a prefix:
|
||||
|
||||
{Ecto.Migrator,
|
||||
repos: Application.fetch_env!(:my_app, :ecto_repos),
|
||||
log_migrator_sql: true,
|
||||
prefix: "my_app"}
|
||||
|
||||
To roll back you'd do it normally:
|
||||
|
||||
$ mix ecto.rollback
|
||||
|
||||
"""
|
||||
|
||||
require Logger
|
||||
require Ecto.Query
|
||||
|
||||
alias Ecto.Migration.Runner
|
||||
alias Ecto.Migration.SchemaMigration
|
||||
|
||||
@doc """
|
||||
Ensures the repo is started to perform migration operations.
|
||||
|
||||
All of the application required to run the repo will be started
|
||||
before hand with chosen mode. If the repo has not yet been started,
|
||||
it is manually started, with a `:pool_size` of 2, before the given
|
||||
function is executed, and the repo is then terminated. If the repo
|
||||
was already started, then the function is directly executed, without
|
||||
terminating the repo afterwards.
|
||||
|
||||
Although this function was designed to start repositories for running
|
||||
migrations, it can be used by any code, Mix task, or release tooling
|
||||
that needs to briefly start a repository to perform a certain operation
|
||||
and then terminate.
|
||||
|
||||
The repo may also configure a `:start_apps_before_migration` option
|
||||
which is a list of applications to be started before the migration
|
||||
runs.
|
||||
|
||||
It returns `{:ok, fun_return, apps}`, with all apps that have been
|
||||
started, or `{:error, term}`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:pool_size` - The pool size to start the repo for migrations.
|
||||
Defaults to 2.
|
||||
* `:mode` - The mode to start all applications.
|
||||
Defaults to `:permanent`.
|
||||
|
||||
## Examples
|
||||
|
||||
{:ok, _, _} =
|
||||
Ecto.Migrator.with_repo(repo, fn repo ->
|
||||
Ecto.Migrator.run(repo, :up, all: true)
|
||||
end)
|
||||
|
||||
"""
|
||||
def with_repo(repo, fun, opts \\ []) do
|
||||
config = repo.config()
|
||||
mode = Keyword.get(opts, :mode, :permanent)
|
||||
apps = [:ecto_sql | config[:start_apps_before_migration] || []]
|
||||
|
||||
extra_started =
|
||||
Enum.flat_map(apps, fn app ->
|
||||
{:ok, started} = Application.ensure_all_started(app, mode)
|
||||
started
|
||||
end)
|
||||
|
||||
{:ok, repo_started} = repo.__adapter__().ensure_all_started(config, mode)
|
||||
started = extra_started ++ repo_started
|
||||
pool_size = Keyword.get(opts, :pool_size, 2)
|
||||
migration_repo = config[:migration_repo] || repo
|
||||
|
||||
case ensure_repo_started(repo, pool_size) do
|
||||
{:ok, repo_after} ->
|
||||
case ensure_migration_repo_started(migration_repo, repo) do
|
||||
{:ok, migration_repo_after} ->
|
||||
try do
|
||||
{:ok, fun.(repo), started}
|
||||
after
|
||||
after_action(repo, repo_after)
|
||||
after_action(migration_repo, migration_repo_after)
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
after_action(repo, repo_after)
|
||||
error
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the migrations path from a repository.
|
||||
|
||||
This function accepts an optional second parameter to customize the
|
||||
migrations directory. This can be used to specify a custom migrations
|
||||
path.
|
||||
"""
|
||||
@spec migrations_path(Ecto.Repo.t(), String.t()) :: String.t()
|
||||
def migrations_path(repo, directory \\ "migrations") do
|
||||
config = repo.config()
|
||||
priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
|
||||
app = Keyword.fetch!(config, :otp_app)
|
||||
Application.app_dir(app, Path.join(priv, directory))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all migrated versions.
|
||||
|
||||
This function ensures the migration table exists
|
||||
if no table has been defined yet.
|
||||
|
||||
## Options
|
||||
|
||||
* `:prefix` - the prefix to run the migrations on
|
||||
* `:dynamic_repo` - the name of the Repo supervisor process.
|
||||
See `c:Ecto.Repo.put_dynamic_repo/1`.
|
||||
* `:skip_table_creation` - skips any attempt to create the migration table
|
||||
Useful for situations where user needs to check migrations but has
|
||||
insufficient permissions to create the table. Note that migrations
|
||||
commands may fail if this is set to true. Defaults to `false`. Accepts a
|
||||
boolean.
|
||||
"""
|
||||
@spec migrated_versions(Ecto.Repo.t(), Keyword.t()) :: [integer]
|
||||
def migrated_versions(repo, opts \\ []) do
|
||||
lock_for_migrations(true, repo, opts, fn _config, versions -> versions end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs an up migration on the given repository.
|
||||
|
||||
## Options
|
||||
|
||||
* `:log` - the level to use for logging of migration instructions.
|
||||
Defaults to `:info`. Can be any of `Logger.level/0` values or a boolean.
|
||||
If `false`, it also avoids logging messages from the database.
|
||||
* `:log_migrations_sql` - the level to use for logging of SQL commands
|
||||
generated by migrations. Can be any of the `Logger.level/0` values
|
||||
or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:log_migrator_sql` - the level to use for logging of SQL commands emitted
|
||||
by the migrator, such as transactions, locks, etc. Can be any of the `Logger.level/0`
|
||||
values or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:prefix` - the prefix to run the migrations on
|
||||
* `:dynamic_repo` - the name of the Repo supervisor process.
|
||||
See `c:Ecto.Repo.put_dynamic_repo/1`.
|
||||
* `:strict_version_order` - abort when applying a migration with old timestamp
|
||||
(otherwise it emits a warning)
|
||||
"""
|
||||
@spec up(Ecto.Repo.t(), integer, module, Keyword.t()) :: :ok | :already_up
|
||||
def up(repo, version, module, opts \\ []) do
|
||||
conditional_lock_for_migrations(module, version, repo, opts, fn config, versions ->
|
||||
if version in versions do
|
||||
:already_up
|
||||
else
|
||||
result = do_up(repo, config, version, module, opts)
|
||||
|
||||
if version != Enum.max([version | versions]) do
|
||||
latest = Enum.max(versions)
|
||||
|
||||
message = """
|
||||
You are running migration #{version} but an older \
|
||||
migration with version #{latest} has already run.
|
||||
|
||||
This can be an issue if you have already ran #{latest} in production \
|
||||
because a new deployment may migrate #{version} but a rollback command \
|
||||
would revert #{latest} instead of #{version}.
|
||||
|
||||
If this can be an issue, we recommend to rollback #{version} and change \
|
||||
it to a version later than #{latest}.
|
||||
"""
|
||||
|
||||
if opts[:strict_version_order] do
|
||||
raise Ecto.MigrationError, message
|
||||
else
|
||||
Logger.warning(message)
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_up(repo, config, version, module, opts) do
|
||||
async_migrate_maybe_in_transaction(repo, config, version, module, :up, opts, fn ->
|
||||
attempt(repo, config, version, module, :forward, :up, :up, opts) ||
|
||||
attempt(repo, config, version, module, :forward, :change, :up, opts) ||
|
||||
{:error,
|
||||
Ecto.MigrationError.exception(
|
||||
"#{inspect(module)} does not implement a `up/0` or `change/0` function"
|
||||
)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs a down migration on the given repository.
|
||||
|
||||
## Options
|
||||
|
||||
* `:log` - the level to use for logging of migration commands. Defaults to `:info`.
|
||||
Can be any of `Logger.level/0` values or a boolean.
|
||||
* `:log_migrations_sql` - the level to use for logging of SQL commands
|
||||
generated by migrations. Can be any of the `Logger.level/0` values
|
||||
or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:log_migrator_sql` - the level to use for logging of SQL commands emitted
|
||||
by the migrator, such as transactions, locks, etc. Can be any of the `Logger.level/0`
|
||||
values or a boolean. If `false`, logging is disabled. If `true`, uses the configured
|
||||
Repo logger level. Defaults to `false`
|
||||
* `:prefix` - the prefix to run the migrations on
|
||||
* `:dynamic_repo` - the name of the Repo supervisor process.
|
||||
See `c:Ecto.Repo.put_dynamic_repo/1`.
|
||||
|
||||
"""
|
||||
@spec down(Ecto.Repo.t(), integer, module) :: :ok | :already_down
|
||||
def down(repo, version, module, opts \\ []) do
|
||||
conditional_lock_for_migrations(module, version, repo, opts, fn config, versions ->
|
||||
if version in versions do
|
||||
do_down(repo, config, version, module, opts)
|
||||
else
|
||||
:already_down
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_down(repo, config, version, module, opts) do
|
||||
async_migrate_maybe_in_transaction(repo, config, version, module, :down, opts, fn ->
|
||||
attempt(repo, config, version, module, :forward, :down, :down, opts) ||
|
||||
attempt(repo, config, version, module, :backward, :change, :down, opts) ||
|
||||
{:error,
|
||||
Ecto.MigrationError.exception(
|
||||
"#{inspect(module)} does not implement a `down/0` or `change/0` function"
|
||||
)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp async_migrate_maybe_in_transaction(repo, config, version, module, direction, opts, fun) do
|
||||
dynamic_repo = repo.get_dynamic_repo()
|
||||
|
||||
fun_with_status = fn ->
|
||||
result = fun.()
|
||||
apply(SchemaMigration, direction, [repo, config, version, opts])
|
||||
result
|
||||
end
|
||||
|
||||
fn -> run_maybe_in_transaction(repo, dynamic_repo, module, fun_with_status, opts) end
|
||||
|> Task.async()
|
||||
|> Task.await(:infinity)
|
||||
end
|
||||
|
||||
defp run_maybe_in_transaction(repo, dynamic_repo, module, fun, opts) do
|
||||
repo.put_dynamic_repo(dynamic_repo)
|
||||
|
||||
if module.__migration__()[:disable_ddl_transaction] ||
|
||||
not repo.__adapter__().supports_ddl_transaction?() do
|
||||
fun.()
|
||||
else
|
||||
{:ok, result} = repo.transaction(fun, log: migrator_log(opts), timeout: :infinity)
|
||||
|
||||
result
|
||||
end
|
||||
catch
|
||||
kind, reason ->
|
||||
{kind, reason, __STACKTRACE__}
|
||||
end
|
||||
|
||||
defp attempt(repo, config, version, module, direction, operation, reference, opts) do
|
||||
if Code.ensure_loaded?(module) and function_exported?(module, operation, 0) do
|
||||
Runner.run(repo, config, version, module, direction, operation, reference, opts)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs migrations for the given repository.
|
||||
|
||||
Equivalent to:
|
||||
|
||||
Ecto.Migrator.run(repo, [Ecto.Migrator.migrations_path(repo)], direction, opts)
|
||||
|
||||
See `run/4` for more information.
|
||||
"""
|
||||
@spec run(Ecto.Repo.t(), atom, Keyword.t()) :: [integer]
|
||||
def run(repo, direction, opts) do
|
||||
run(repo, [migrations_path(repo)], direction, opts)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Apply migrations to a repository with a given strategy.
|
||||
|
||||
The second argument identifies where the migrations are sourced from.
|
||||
A binary representing directory (or a list of binaries representing
|
||||
directories) may be passed, in which case we will load all files
|
||||
following the "#{VERSION}_#{NAME}.exs" schema. The `migration_source`
|
||||
may also be a list of tuples that identify the version number and
|
||||
migration modules to be run, for example:
|
||||
|
||||
Ecto.Migrator.run(Repo, [{0, MyApp.Migration1}, {1, MyApp.Migration2}, ...], :up, opts)
|
||||
|
||||
A strategy (which is one of `:all`, `:step`, `:to`, or `:to_exclusive`) must be given as
|
||||
an option.
|
||||
|
||||
## Execution model
|
||||
|
||||
In order to run migrations, at least two database connections are
|
||||
necessary. One is used to lock the "schema_migrations" table and
|
||||
the other one to effectively run the migrations. This allows multiple
|
||||
nodes to run migrations at the same time, but guarantee that only one
|
||||
of them will effectively migrate the database.
|
||||
|
||||
A downside of this approach is that migrations cannot run dynamically
|
||||
during test under the `Ecto.Adapters.SQL.Sandbox`, as the sandbox has
|
||||
to share a single connection across processes to guarantee the changes
|
||||
can be reverted.
|
||||
|
||||
## Options
|
||||
|
||||
* `:all` - runs all available if `true`
|
||||
|
||||
* `:step` - runs the specific number of migrations
|
||||
|
||||
* `:to` - runs all until the supplied version is reached
|
||||
(including the version given in `:to`)
|
||||
|
||||
* `:to_exclusive` - runs all until the supplied version is reached
|
||||
(excluding the version given in `:to_exclusive`)
|
||||
|
||||
Plus all other options described in `up/4`.
|
||||
"""
|
||||
@spec run(Ecto.Repo.t(), String.t() | [String.t()] | [{integer, module}], atom, Keyword.t()) ::
|
||||
[integer]
|
||||
def run(repo, migration_source, direction, opts) do
|
||||
migration_source = List.wrap(migration_source)
|
||||
|
||||
pending =
|
||||
lock_for_migrations(true, repo, opts, fn _config, versions ->
|
||||
cond do
|
||||
opts[:all] ->
|
||||
pending_all(versions, migration_source, direction)
|
||||
|
||||
to = opts[:to] ->
|
||||
pending_to(versions, migration_source, direction, to)
|
||||
|
||||
to_exclusive = opts[:to_exclusive] ->
|
||||
pending_to_exclusive(versions, migration_source, direction, to_exclusive)
|
||||
|
||||
step = opts[:step] ->
|
||||
pending_step(versions, migration_source, direction, step)
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
ArgumentError.exception(
|
||||
"expected one of :all, :to, :to_exclusive, or :step strategies"
|
||||
)}
|
||||
end
|
||||
end)
|
||||
|
||||
# The lock above already created the table, so we can now skip it.
|
||||
opts = Keyword.put(opts, :skip_table_creation, true)
|
||||
|
||||
ensure_no_duplication!(pending)
|
||||
migrate(Enum.map(pending, &load_migration!/1), direction, repo, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an array of tuples as the migration status of the given repo,
|
||||
without actually running any migrations.
|
||||
|
||||
Equivalent to:
|
||||
|
||||
Ecto.Migrator.migrations(repo, [Ecto.Migrator.migrations_path(repo)])
|
||||
|
||||
"""
|
||||
@spec migrations(Ecto.Repo.t()) :: [{:up | :down, id :: integer(), name :: String.t()}]
|
||||
def migrations(repo) do
|
||||
migrations(repo, [migrations_path(repo)])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an array of tuples as the migration status of the given repo,
|
||||
without actually running any migrations.
|
||||
"""
|
||||
@spec migrations(Ecto.Repo.t(), String.t() | [String.t()], Keyword.t()) ::
|
||||
[{:up | :down, id :: integer(), name :: String.t()}]
|
||||
def migrations(repo, directories, opts \\ []) do
|
||||
directories = List.wrap(directories)
|
||||
|
||||
repo
|
||||
|> migrated_versions(opts)
|
||||
|> collect_migrations(directories)
|
||||
|> Enum.sort_by(fn {_, version, _} -> version end)
|
||||
end
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc """
|
||||
Runs migrations as part of your supervision tree.
|
||||
|
||||
## Options
|
||||
|
||||
* `:repos` - Required option to tell the migrator which Repo's to
|
||||
migrate. Example: `repos: [MyApp.Repo]`
|
||||
|
||||
* `:skip` - Option to skip migrations. Defaults to `false`.
|
||||
|
||||
Plus all other options described in `up/4`.
|
||||
|
||||
See "Example: Running migrations on application startup" for more info.
|
||||
"""
|
||||
def start_link(opts) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
{repos, opts} = Keyword.pop!(opts, :repos)
|
||||
{skip?, opts} = Keyword.pop(opts, :skip, false)
|
||||
{migrator, opts} = Keyword.pop(opts, :migrator, &Ecto.Migrator.run/3)
|
||||
opts = Keyword.put(opts, :all, true)
|
||||
|
||||
unless skip? do
|
||||
for repo <- repos do
|
||||
{:ok, _, _} = with_repo(repo, &migrator.(&1, :up, opts))
|
||||
end
|
||||
end
|
||||
|
||||
:ignore
|
||||
end
|
||||
|
||||
defp collect_migrations(versions, migration_source) do
|
||||
ups_with_file =
|
||||
versions
|
||||
|> pending_in_direction(migration_source, :down)
|
||||
|> Enum.map(fn {version, name, _} -> {:up, version, name} end)
|
||||
|
||||
ups_without_file =
|
||||
versions
|
||||
|> versions_without_file(migration_source)
|
||||
|> Enum.map(fn version -> {:up, version, "** FILE NOT FOUND **"} end)
|
||||
|
||||
downs =
|
||||
versions
|
||||
|> pending_in_direction(migration_source, :up)
|
||||
|> Enum.map(fn {version, name, _} -> {:down, version, name} end)
|
||||
|
||||
ups_with_file ++ ups_without_file ++ downs
|
||||
end
|
||||
|
||||
defp versions_without_file(versions, migration_source) do
|
||||
versions_with_file =
|
||||
migration_source
|
||||
|> migrations_for()
|
||||
|> Enum.map(fn {version, _, _} -> version end)
|
||||
|
||||
versions -- versions_with_file
|
||||
end
|
||||
|
||||
defp lock_for_migrations(lock_or_migration_number, repo, opts, fun) do
|
||||
dynamic_repo = Keyword.get(opts, :dynamic_repo, repo.get_dynamic_repo())
|
||||
skip_table_creation = Keyword.get(opts, :skip_table_creation, false)
|
||||
previous_dynamic_repo = repo.put_dynamic_repo(dynamic_repo)
|
||||
|
||||
try do
|
||||
config = repo.config()
|
||||
|
||||
unless skip_table_creation do
|
||||
verbose_schema_migration(repo, "create schema migrations table", fn ->
|
||||
SchemaMigration.ensure_schema_migrations_table!(repo, config, opts)
|
||||
end)
|
||||
end
|
||||
|
||||
{migration_repo, query, all_opts} = SchemaMigration.versions(repo, config, opts[:prefix])
|
||||
|
||||
migration_lock? =
|
||||
Keyword.get(opts, :migration_lock, Keyword.get(config, :migration_lock, true))
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.put(:migration_source, config[:migration_source] || "schema_migrations")
|
||||
|> Keyword.put(:log, migrator_log(opts))
|
||||
|
||||
result =
|
||||
if lock_or_migration_number && migration_lock? do
|
||||
# If there is a migration_repo, it wins over dynamic_repo,
|
||||
# otherwise the dynamic_repo is the one locked in migrations.
|
||||
meta_repo = if migration_repo != repo, do: migration_repo, else: dynamic_repo
|
||||
meta = Ecto.Adapter.lookup_meta(meta_repo)
|
||||
|
||||
migration_repo.__adapter__().lock_for_migrations(meta, opts, fn ->
|
||||
fun.(config, migration_repo.all(query, all_opts))
|
||||
end)
|
||||
else
|
||||
fun.(config, migration_repo.all(query, all_opts))
|
||||
end
|
||||
|
||||
case result do
|
||||
{kind, reason, stacktrace} ->
|
||||
:erlang.raise(kind, reason, stacktrace)
|
||||
|
||||
{:error, error} ->
|
||||
raise error
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
after
|
||||
repo.put_dynamic_repo(previous_dynamic_repo)
|
||||
end
|
||||
end
|
||||
|
||||
defp conditional_lock_for_migrations(module, version, repo, opts, fun) do
|
||||
lock = if module.__migration__()[:disable_migration_lock], do: false, else: version
|
||||
lock_for_migrations(lock, repo, opts, fun)
|
||||
end
|
||||
|
||||
defp pending_to(versions, migration_source, direction, target) when is_integer(target) do
|
||||
within_target_version? = fn
|
||||
{version, _, _}, target, :up ->
|
||||
version <= target
|
||||
|
||||
{version, _, _}, target, :down ->
|
||||
version >= target
|
||||
end
|
||||
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
|> Enum.take_while(&within_target_version?.(&1, target, direction))
|
||||
end
|
||||
|
||||
defp pending_to_exclusive(versions, migration_source, direction, target)
|
||||
when is_integer(target) do
|
||||
within_target_version? = fn
|
||||
{version, _, _}, target, :up ->
|
||||
version < target
|
||||
|
||||
{version, _, _}, target, :down ->
|
||||
version > target
|
||||
end
|
||||
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
|> Enum.take_while(&within_target_version?.(&1, target, direction))
|
||||
end
|
||||
|
||||
defp pending_step(versions, migration_source, direction, count) do
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
|> Enum.take(count)
|
||||
end
|
||||
|
||||
defp pending_all(versions, migration_source, direction) do
|
||||
pending_in_direction(versions, migration_source, direction)
|
||||
end
|
||||
|
||||
defp pending_in_direction(versions, migration_source, :up) do
|
||||
migration_source
|
||||
|> migrations_for()
|
||||
|> Enum.filter(fn {version, _name, _file} -> version not in versions end)
|
||||
end
|
||||
|
||||
defp pending_in_direction(versions, migration_source, :down) do
|
||||
migration_source
|
||||
|> migrations_for()
|
||||
|> Enum.filter(fn {version, _name, _file} -> version in versions end)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp migrations_for(migration_source) when is_list(migration_source) do
|
||||
migration_source
|
||||
|> Enum.flat_map(fn
|
||||
directory when is_binary(directory) ->
|
||||
Path.join([directory, "**", "*.{ex,exs}"])
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(&extract_migration_info/1)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
{version, module} ->
|
||||
[{version, module, module}]
|
||||
end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp extract_migration_info(file) do
|
||||
base = Path.basename(file)
|
||||
|
||||
case Integer.parse(Path.rootname(base)) do
|
||||
{integer, "_" <> name} ->
|
||||
if Path.extname(base) == ".ex" do
|
||||
# See: https://github.com/elixir-ecto/ecto_sql/issues/599
|
||||
IO.warn(
|
||||
"""
|
||||
file looks like a migration but ends in .ex. \
|
||||
Migration files should end in .exs. Use "mix ecto.gen.migration" to generate \
|
||||
migration files with the correct extension.\
|
||||
""",
|
||||
stacktrace_info(file: file)
|
||||
)
|
||||
|
||||
nil
|
||||
else
|
||||
{integer, name, file}
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove when we require Elixir 1.14
|
||||
if Version.match?(System.version(), ">= 1.14.0") do
|
||||
defp stacktrace_info(info), do: info
|
||||
else
|
||||
defp stacktrace_info(_info), do: []
|
||||
end
|
||||
|
||||
defp ensure_no_duplication!([{version, name, _} | t]) do
|
||||
cond do
|
||||
List.keyfind(t, version, 0) ->
|
||||
raise Ecto.MigrationError,
|
||||
"migrations can't be executed, migration version #{version} is duplicated"
|
||||
|
||||
List.keyfind(t, name, 1) ->
|
||||
raise Ecto.MigrationError,
|
||||
"migrations can't be executed, migration name #{name} is duplicated"
|
||||
|
||||
true ->
|
||||
ensure_no_duplication!(t)
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_no_duplication!([]), do: :ok
|
||||
|
||||
defp load_migration!({version, _, mod}) when is_atom(mod) do
|
||||
if migration?(mod) do
|
||||
{version, mod}
|
||||
else
|
||||
raise Ecto.MigrationError, "module #{inspect(mod)} is not an Ecto.Migration"
|
||||
end
|
||||
end
|
||||
|
||||
defp load_migration!({version, _, file}) when is_binary(file) do
|
||||
loaded_modules = file |> Code.compile_file() |> Enum.map(&elem(&1, 0))
|
||||
|
||||
if mod = Enum.find(loaded_modules, &migration?/1) do
|
||||
{version, mod}
|
||||
else
|
||||
raise Ecto.MigrationError,
|
||||
"file #{Path.relative_to_cwd(file)} does not define an Ecto.Migration"
|
||||
end
|
||||
end
|
||||
|
||||
defp migration?(mod) do
|
||||
Code.ensure_loaded?(mod) and function_exported?(mod, :__migration__, 0)
|
||||
end
|
||||
|
||||
defp migrate([], direction, _repo, opts) do
|
||||
level = Keyword.get(opts, :log, :info)
|
||||
log(level, "Migrations already #{direction}")
|
||||
[]
|
||||
end
|
||||
|
||||
defp migrate(migrations, direction, repo, opts) do
|
||||
for {version, mod} <- migrations,
|
||||
do_direction(direction, repo, version, mod, opts),
|
||||
do: version
|
||||
end
|
||||
|
||||
defp do_direction(:up, repo, version, mod, opts) do
|
||||
conditional_lock_for_migrations(mod, version, repo, opts, fn config, versions ->
|
||||
unless version in versions do
|
||||
do_up(repo, config, version, mod, opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_direction(:down, repo, version, mod, opts) do
|
||||
conditional_lock_for_migrations(mod, version, repo, opts, fn config, versions ->
|
||||
if version in versions do
|
||||
do_down(repo, config, version, mod, opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp verbose_schema_migration(repo, reason, fun) do
|
||||
try do
|
||||
fun.()
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("""
|
||||
Could not #{reason}. This error usually happens due to the following:
|
||||
|
||||
* The database does not exist
|
||||
* The "schema_migrations" table, which Ecto uses for managing
|
||||
migrations, was defined by another library
|
||||
* There is a deadlock while migrating (such as using concurrent
|
||||
indexes with a migration_lock)
|
||||
|
||||
To fix the first issue, run "mix ecto.create" for the desired MIX_ENV.
|
||||
|
||||
To address the second, you can run "mix ecto.drop" followed by
|
||||
"mix ecto.create", both for the desired MIX_ENV. Alternatively you may
|
||||
configure Ecto to use another table and/or repository for managing
|
||||
migrations:
|
||||
|
||||
config #{inspect(repo.config()[:otp_app])}, #{inspect(repo)},
|
||||
migration_source: "some_other_table_for_schema_migrations",
|
||||
migration_repo: AnotherRepoForSchemaMigrations
|
||||
|
||||
The full error report is shown below.
|
||||
""")
|
||||
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
defp log(false, _msg), do: :ok
|
||||
defp log(true, msg), do: Logger.info(msg)
|
||||
defp log(level, msg), do: Logger.log(level, msg)
|
||||
|
||||
defp migrator_log(opts) do
|
||||
Keyword.get(opts, :log_migrator_sql, false)
|
||||
end
|
||||
|
||||
defp ensure_repo_started(repo, pool_size) do
|
||||
case repo.start_link(pool_size: pool_size) do
|
||||
{:ok, _} ->
|
||||
{:ok, :stop}
|
||||
|
||||
{:error, {:already_started, _pid}} ->
|
||||
{:ok, :restart}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_migration_repo_started(repo, repo) do
|
||||
{:ok, :noop}
|
||||
end
|
||||
|
||||
defp ensure_migration_repo_started(migration_repo, _repo) do
|
||||
case migration_repo.start_link() do
|
||||
{:ok, _} ->
|
||||
{:ok, :stop}
|
||||
|
||||
{:error, {:already_started, _pid}} ->
|
||||
{:ok, :noop}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp after_action(repo, :restart) do
|
||||
if Process.whereis(repo) do
|
||||
%{pid: pid} = Ecto.Adapter.lookup_meta(repo)
|
||||
Supervisor.restart_child(repo, pid)
|
||||
end
|
||||
end
|
||||
|
||||
defp after_action(repo, :stop) do
|
||||
repo.stop()
|
||||
end
|
||||
|
||||
defp after_action(_repo, :noop) do
|
||||
:noop
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user