This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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