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,115 @@
defmodule Mix.Tasks.Ecto.Dump do
use Mix.Task
import Mix.Ecto
import Mix.EctoSQL
@shortdoc "Dumps the repository database structure"
@default_opts [quiet: false]
@aliases [
d: :dump_path,
q: :quiet,
r: :repo
]
@switches [
dump_path: :string,
quiet: :boolean,
repo: [:string, :keep],
no_compile: :boolean,
no_deps_check: :boolean,
prefix: [:string, :keep]
]
@moduledoc """
Dumps the current environment's database structure for the
given repository into a structure file.
The repository must be set under `:ecto_repos` in the
current app configuration or given via the `-r` option.
This task needs some shell utility to be present on the machine
running the task.
Database | Utility needed
:--------- | :-------------
PostgreSQL | pg_dump
MySQL | mysqldump
## Example
$ mix ecto.dump
## Command line options
* `-r`, `--repo` - the repo to load the structure info from
* `-d`, `--dump-path` - the path of the dump file to create
* `-q`, `--quiet` - run the command quietly
* `--no-compile` - does not compile applications before dumping
* `--no-deps-check` - does not check dependencies before dumping
* `--prefix` - prefix that will be included in the structure dump.
Can include multiple prefixes (ex. `--prefix foo --prefix bar`) with
PostgreSQL but not MySQL. When specified, the prefixes will have
their definitions dumped along with the data in their migration table.
The default behavior is dependent on the adapter for backwards compatibility
reasons. For PostgreSQL, 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. For MySQL, only the configured
database and its migration table are dumped.
"""
@impl true
def run(args) do
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
dump_prefixes =
case Keyword.get_values(opts, :prefix) do
[_ | _] = prefixes -> prefixes
[] -> nil
end
opts =
@default_opts
|> Keyword.merge(opts)
|> Keyword.put(:dump_prefixes, dump_prefixes)
Enum.each(parse_repo(args), fn repo ->
ensure_repo(repo, args)
ensure_implements(
repo.__adapter__(),
Ecto.Adapter.Structure,
"dump structure for #{inspect(repo)}"
)
migration_repo = repo.config()[:migration_repo] || repo
for repo <- Enum.uniq([repo, migration_repo]) do
config = Keyword.merge(repo.config(), opts)
start_time = System.system_time()
case repo.__adapter__().structure_dump(source_repo_priv(repo), config) do
{:ok, location} ->
unless opts[:quiet] do
elapsed =
System.convert_time_unit(System.system_time() - start_time, :native, :microsecond)
Mix.shell().info(
"The structure for #{inspect(repo)} has been dumped to #{location} in #{format_time(elapsed)}"
)
end
{:error, term} when is_binary(term) ->
Mix.raise("The structure for #{inspect(repo)} couldn't be dumped: #{term}")
{:error, term} ->
Mix.raise("The structure for #{inspect(repo)} couldn't be dumped: #{inspect(term)}")
end
end
end)
end
defp format_time(microsec) when microsec < 1_000, do: "#{microsec} μs"
defp format_time(microsec) when microsec < 1_000_000, do: "#{div(microsec, 1_000)} ms"
defp format_time(microsec), do: "#{Float.round(microsec / 1_000_000.0)} s"
end

View File

@@ -0,0 +1,130 @@
defmodule Mix.Tasks.Ecto.Gen.Migration do
use Mix.Task
import Macro, only: [camelize: 1, underscore: 1]
import Mix.Generator
import Mix.Ecto
import Mix.EctoSQL
@shortdoc "Generates a new migration for the repo"
@aliases [
r: :repo
]
@switches [
change: :string,
repo: [:string, :keep],
no_compile: :boolean,
no_deps_check: :boolean,
migrations_path: :string
]
@moduledoc """
Generates a migration.
The repository must be set under `:ecto_repos` in the
current app configuration or given via the `-r` option.
## Examples
$ mix ecto.gen.migration add_posts_table
$ mix ecto.gen.migration add_posts_table -r Custom.Repo
The generated migration filename will be prefixed with the current
timestamp in UTC which is used for versioning and ordering.
By default, the migration will be generated to the
"priv/YOUR_REPO/migrations" directory of the current application
but it can be configured to be any subdirectory of `priv` by
specifying the `:priv` key under the repository configuration.
This generator will automatically open the generated file if
you have `ECTO_EDITOR` set in your environment variable.
## Command line options
* `-r`, `--repo` - the repo to generate migration for
* `--no-compile` - does not compile applications before running
* `--no-deps-check` - does not check dependencies before running
* `--migrations-path` - the path to run the migrations from, defaults to `priv/repo/migrations`
## Configuration
If the current app configuration specifies a custom migration module
the generated migration code will use that rather than the default
`Ecto.Migration`:
config :ecto_sql, migration_module: MyApplication.CustomMigrationModule
"""
@impl true
def run(args) do
repos = parse_repo(args)
Enum.map(repos, fn repo ->
case OptionParser.parse!(args, strict: @switches, aliases: @aliases) do
{opts, [name]} ->
ensure_repo(repo, args)
path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")
base_name = "#{underscore(name)}.exs"
file = Path.join(path, "#{timestamp()}_#{base_name}")
unless File.dir?(path), do: create_directory(path)
fuzzy_path = Path.join(path, "*_#{base_name}")
if Path.wildcard(fuzzy_path) != [] do
Mix.raise(
"migration can't be created, there is already a migration file with name #{name}."
)
end
# The :change option may be used by other tasks but not the CLI
assigns = [
mod: Module.concat([repo, Migrations, camelize(name)]),
change: opts[:change]
]
create_file(file, migration_template(assigns))
if open?(file) and Mix.shell().yes?("Do you want to run this migration?") do
Mix.Task.run("ecto.migrate", ["-r", inspect(repo), "--migrations-path", path])
end
file
{_, _} ->
Mix.raise(
"expected ecto.gen.migration to receive the migration file name, " <>
"got: #{inspect(Enum.join(args, " "))}"
)
end
end)
end
defp timestamp do
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end
defp pad(i) when i < 10, do: <<?0, ?0 + i>>
defp pad(i), do: to_string(i)
defp migration_module do
case Application.get_env(:ecto_sql, :migration_module, Ecto.Migration) do
migration_module when is_atom(migration_module) -> migration_module
other -> Mix.raise("Expected :migration_module to be a module, got: #{inspect(other)}")
end
end
embed_template(:migration, """
defmodule <%= inspect @mod %> do
use <%= inspect migration_module() %>
def change do
<%= @change %>
end
end
""")
end

View File

@@ -0,0 +1,146 @@
defmodule Mix.Tasks.Ecto.Load do
use Mix.Task
import Mix.Ecto
import Mix.EctoSQL
@shortdoc "Loads previously dumped database structure"
@default_opts [force: false, quiet: false]
@aliases [
d: :dump_path,
f: :force,
q: :quiet,
r: :repo
]
@switches [
dump_path: :string,
force: :boolean,
quiet: :boolean,
repo: [:string, :keep],
no_compile: :boolean,
no_deps_check: :boolean,
skip_if_loaded: :boolean
]
@moduledoc """
Loads the current environment's database structure for the
given repository from a previously dumped structure file.
The repository must be set under `:ecto_repos` in the
current app configuration or given via the `-r` option.
This task needs some shell utility to be present on the machine
running the task.
Database | Utility needed
:--------- | :-------------
PostgreSQL | psql
MySQL | mysql
## Example
$ mix ecto.load
## Command line options
* `-r`, `--repo` - the repo to load the structure info into
* `-d`, `--dump-path` - the path of the dump file to load from
* `-q`, `--quiet` - run the command quietly
* `-f`, `--force` - do not ask for confirmation when loading data.
Configuration is asked only when `:start_permanent` is set to true
(typically in production)
* `--no-compile` - does not compile applications before loading
* `--no-deps-check` - does not check dependencies before loading
* `--skip-if-loaded` - does not load the dump file if the repo has the migrations table up
"""
@impl true
def run(args, table_exists? \\ &Ecto.Adapters.SQL.table_exists?/3) do
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts = Keyword.merge(@default_opts, opts)
opts = if opts[:quiet], do: Keyword.put(opts, :log, false), else: opts
Enum.each(parse_repo(args), fn repo ->
ensure_repo(repo, args)
ensure_implements(
repo.__adapter__(),
Ecto.Adapter.Structure,
"load structure for #{inspect(repo)}"
)
{migration_repo, source} =
Ecto.Migration.SchemaMigration.get_repo_and_source(repo, repo.config())
{:ok, loaded?, _} =
Ecto.Migrator.with_repo(migration_repo, table_exists_closure(table_exists?, source, opts))
for repo <- Enum.uniq([repo, migration_repo]) do
cond do
loaded? and opts[:skip_if_loaded] ->
:ok
(skip_safety_warnings?() and not loaded?) or opts[:force] or confirm_load(repo, loaded?) ->
load_structure(repo, opts)
true ->
:ok
end
end
end)
end
defp table_exists_closure(fun, source, opts) when is_function(fun, 3) do
&fun.(&1, source, opts)
end
defp table_exists_closure(fun, source, _opts) when is_function(fun, 2) do
&fun.(&1, source)
end
defp skip_safety_warnings? do
Mix.Project.config()[:start_permanent] != true
end
defp confirm_load(repo, false) do
Mix.shell().yes?(
"Are you sure you want to load a new structure for #{inspect(repo)}? Any existing data in this repo may be lost."
)
end
defp confirm_load(repo, true) do
Mix.shell().yes?("""
It looks like a structure was already loaded for #{inspect(repo)}. Any attempt to load it again might fail.
Are you sure you want to proceed?
""")
end
defp load_structure(repo, opts) do
config = Keyword.merge(repo.config(), opts)
start_time = System.system_time()
case repo.__adapter__().structure_load(source_repo_priv(repo), config) do
{:ok, location} ->
unless opts[:quiet] do
elapsed =
System.convert_time_unit(System.system_time() - start_time, :native, :microsecond)
Mix.shell().info(
"The structure for #{inspect(repo)} has been loaded from #{location} in #{format_time(elapsed)}"
)
end
{:error, term} when is_binary(term) ->
Mix.raise("The structure for #{inspect(repo)} couldn't be loaded: #{term}")
{:error, term} ->
Mix.raise("The structure for #{inspect(repo)} couldn't be loaded: #{inspect(term)}")
end
end
defp format_time(microsec) when microsec < 1_000, do: "#{microsec} μs"
defp format_time(microsec) when microsec < 1_000_000, do: "#{div(microsec, 1_000)} ms"
defp format_time(microsec), do: "#{Float.round(microsec / 1_000_000.0)} s"
end

View File

@@ -0,0 +1,162 @@
defmodule Mix.Tasks.Ecto.Migrate do
use Mix.Task
import Mix.Ecto
import Mix.EctoSQL
@shortdoc "Runs the repository migrations"
@aliases [
n: :step,
r: :repo
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
to_exclusive: :integer,
quiet: :boolean,
prefix: :string,
pool_size: :integer,
log_level: :string,
log_migrations_sql: :boolean,
log_migrator_sql: :boolean,
strict_version_order: :boolean,
repo: [:keep, :string],
no_compile: :boolean,
no_deps_check: :boolean,
migrations_path: :keep
]
@moduledoc """
Runs the pending migrations for the given repository.
Migrations are expected at "priv/YOUR_REPO/migrations" directory
of the current application, where "YOUR_REPO" is the last segment
in your repository name. For example, the repository `MyApp.Repo`
will use "priv/repo/migrations". The repository `Whatever.MyRepo`
will use "priv/my_repo/migrations".
You can configure a repository to use another directory by specifying
the `:priv` key under the repository configuration. The "migrations"
part will be automatically appended to it. For instance, to use
"priv/custom_repo/migrations":
config :my_app, MyApp.Repo, priv: "priv/custom_repo"
This task runs all pending migrations by default. To migrate up to a
specific version number, supply `--to version_number`. To migrate a
specific number of times, use `--step n`.
The repositories to migrate are the ones specified under the
`:ecto_repos` option in the current app configuration. However,
if the `-r` option is given, it replaces the `:ecto_repos` config.
Since Ecto tasks can only be executed once, if you need to migrate
multiple repositories, set `:ecto_repos` accordingly or pass the `-r`
flag multiple times.
If a repository has not yet been started, one will be started outside
your application supervision tree and shutdown afterwards.
## Examples
$ mix ecto.migrate
$ mix ecto.migrate -r Custom.Repo
$ mix ecto.migrate -n 3
$ mix ecto.migrate --step 3
$ mix ecto.migrate --to 20080906120000
## Command line options
* `--all` - run all pending migrations
* `--log-migrations-sql` - log SQL generated by migration commands
* `--log-migrator-sql` - log SQL generated by the migrator, such as
transactions, table locks, etc
* `--log-level` (since v3.11.0) - the level to set for `Logger`. This task
does not start your application, so whatever level you have configured in
your config files will not be used. If this is not provided, no level
will be set, so that if you set it yourself before calling this task
then this won't interfere. Can be any of the `t:Logger.level/0` levels
* `--migrations-path` - the path to load the migrations from, defaults to
`"priv/repo/migrations"`. This option may be given multiple times in which
case the migrations are loaded from all the given directories and sorted
as if they were in the same one
* `--no-compile` - does not compile applications before migrating
* `--no-deps-check` - does not check dependencies before migrating
* `--pool-size` - the pool size if the repository is started
only for the task (defaults to 2)
* `--prefix` - the prefix to run migrations on
* `--quiet` - do not log migration commands
* `-r`, `--repo` - the repo to migrate
* `--step`, `-n` - run n number of pending migrations
* `--strict-version-order` - abort when applying a migration with old
timestamp (otherwise it emits a warning)
* `--to` - run all migrations up to and including version
* `--to-exclusive` - run all migrations up to and excluding version
"""
@impl true
def run(args, migrator \\ &Ecto.Migrator.run/4) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts =
if opts[:to] || opts[:to_exclusive] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :all, true)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_migrations_sql: false, log_migrator_sql: false),
else: opts
if log_level = opts[:log_level] do
Logger.configure(level: String.to_existing_atom(log_level))
end
# Start ecto_sql explicitly before as we don't need
# to restart those apps if migrated.
{:ok, _} = Application.ensure_all_started(:ecto_sql)
for repo <- repos do
ensure_repo(repo, args)
paths = ensure_migrations_paths(repo, opts)
pool = repo.config()[:pool]
fun =
if Code.ensure_loaded?(pool) and function_exported?(pool, :unboxed_run, 2) do
&pool.unboxed_run(&1, fn -> migrator.(&1, paths, :up, opts) end)
else
&migrator.(&1, paths, :up, opts)
end
case Ecto.Migrator.with_repo(repo, fun, [mode: :temporary] ++ opts) do
{:ok, _migrated, _apps} ->
:ok
{:error, error} ->
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
end
end
:ok
end
end

View File

@@ -0,0 +1,97 @@
defmodule Mix.Tasks.Ecto.Migrations do
use Mix.Task
import Mix.Ecto
import Mix.EctoSQL
@shortdoc "Displays the repository migration status"
@aliases [
r: :repo
]
@switches [
repo: [:keep, :string],
no_compile: :boolean,
no_deps_check: :boolean,
migrations_path: :keep,
prefix: :string
]
@moduledoc """
Displays the up / down migration status for the given repository.
The repository must be set under `:ecto_repos` in the
current app configuration or given via the `-r` option.
By default, migrations are expected at "priv/YOUR_REPO/migrations"
directory of the current application but it can be configured
by specifying the `:priv` key under the repository configuration.
If the repository has not been started yet, one will be
started outside our application supervision tree and shutdown
afterwards.
## Examples
$ mix ecto.migrations
$ mix ecto.migrations -r Custom.Repo
## Command line options
* `--migrations-path` - the path to load the migrations from, defaults to
`"priv/repo/migrations"`. This option may be given multiple times in which
case the migrations are loaded from all the given directories and sorted as
if they were in the same one.
Note, if you have previously run migrations from paths `a/` and `b/`, and now
run `mix ecto.migrations --migrations-path a/` (omitting path `b/`), the
migrations from the path `b/` will be shown in the output as `** FILE NOT FOUND **`.
* `--no-compile` - does not compile applications before running
* `--no-deps-check` - does not check dependencies before running
* `--prefix` - the prefix to check migrations on
* `-r`, `--repo` - the repo to obtain the status for
"""
@impl true
def run(args, migrations \\ &Ecto.Migrator.migrations/3, puts \\ &IO.puts/1) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
for repo <- repos do
ensure_repo(repo, args)
paths = ensure_migrations_paths(repo, opts)
case Ecto.Migrator.with_repo(repo, &migrations.(&1, paths, opts), mode: :temporary) do
{:ok, repo_status, _} ->
puts.(
"""
Repo: #{inspect(repo)}
Status Migration ID Migration Name
--------------------------------------------------
""" <>
Enum.map_join(repo_status, "\n", fn {status, number, description} ->
" #{format(status, 10)}#{format(number, 16)}#{description}"
end) <> "\n"
)
{:error, error} ->
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
end
end
:ok
end
defp format(content, pad) do
content
|> to_string
|> String.pad_trailing(pad)
end
end

View File

@@ -0,0 +1,158 @@
defmodule Mix.Tasks.Ecto.Rollback do
use Mix.Task
import Mix.Ecto
import Mix.EctoSQL
@shortdoc "Rolls back the repository migrations"
@aliases [
r: :repo,
n: :step
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
to_exclusive: :integer,
quiet: :boolean,
prefix: :string,
pool_size: :integer,
log_level: :string,
log_migrations_sql: :boolean,
log_migrator_sql: :boolean,
repo: [:keep, :string],
no_compile: :boolean,
no_deps_check: :boolean,
migrations_path: :keep
]
@moduledoc """
Reverts applied migrations in the given repository.
Migrations are expected at "priv/YOUR_REPO/migrations" directory
of the current application, where "YOUR_REPO" is the last segment
in your repository name. For example, the repository `MyApp.Repo`
will use "priv/repo/migrations". The repository `Whatever.MyRepo`
will use "priv/my_repo/migrations".
You can configure a repository to use another directory by specifying
the `:priv` key under the repository configuration. The "migrations"
part will be automatically appended to it. For instance, to use
"priv/custom_repo/migrations":
config :my_app, MyApp.Repo, priv: "priv/custom_repo"
This task rolls back the last applied migration by default. To roll
back to a version number, supply `--to version_number`. To roll
back a specific number of times, use `--step n`. To undo all applied
migrations, provide `--all`.
The repositories to rollback are the ones specified under the
`:ecto_repos` option in the current app configuration. However,
if the `-r` option is given, it replaces the `:ecto_repos` config.
If a repository has not yet been started, one will be started outside
your application supervision tree and shutdown afterwards.
## Examples
$ mix ecto.rollback
$ mix ecto.rollback -r Custom.Repo
$ mix ecto.rollback -n 3
$ mix ecto.rollback --step 3
$ mix ecto.rollback --to 20080906120000
## Command line options
* `--all` - run all pending migrations
* `--log-migrations-sql` - log SQL generated by migration commands
* `--log-migrator-sql` - log SQL generated by the migrator, such as
transactions, table locks, etc
* `--log-level` (since v3.11.0) - the level to set for `Logger`. This task
does not start your application, so whatever level you have configured in
your config files will not be used. If this is not provided, no level
will be set, so that if you set it yourself before calling this task
then this won't interfere. Can be any of the `t:Logger.level/0` levels
* `--migrations-path` - the path to load the migrations from, defaults to
`"priv/repo/migrations"`. This option may be given multiple times in which
case the migrations are loaded from all the given directories and sorted
as if they were in the same one
* `--no-compile` - does not compile applications before migrating
* `--no-deps-check` - does not check dependencies before migrating
* `--pool-size` - the pool size if the repository is started
only for the task (defaults to 2)
* `--prefix` - the prefix to run migrations on
* `--quiet` - do not log migration commands
* `-r`, `--repo` - the repo to migrate
* `--step`, `-n` - revert n migrations
* `--strict-version-order` - abort when applying a migration with old
timestamp (otherwise it emits a warning)
* `--to` - revert all migrations down to and including version
* `--to-exclusive` - revert all migrations down to and excluding version
"""
@impl true
def run(args, migrator \\ &Ecto.Migrator.run/4) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts =
if opts[:to] || opts[:to_exclusive] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :step, 1)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_migrations_sql: false, log_migrator_sql: false),
else: opts
if log_level = opts[:log_level] do
Logger.configure(level: String.to_existing_atom(log_level))
end
# Start ecto_sql explicitly before as we don't need
# to restart those apps if migrated.
{:ok, _} = Application.ensure_all_started(:ecto_sql)
for repo <- repos do
ensure_repo(repo, args)
paths = ensure_migrations_paths(repo, opts)
pool = repo.config()[:pool]
fun =
if Code.ensure_loaded?(pool) and function_exported?(pool, :unboxed_run, 2) do
&pool.unboxed_run(&1, fn -> migrator.(&1, paths, :down, opts) end)
else
&migrator.(&1, paths, :down, opts)
end
case Ecto.Migrator.with_repo(repo, fun, [mode: :temporary] ++ opts) do
{:ok, _migrated, _apps} ->
:ok
{:error, error} ->
Mix.raise("Could not start repo #{inspect(repo)}, error: #{inspect(error)}")
end
end
:ok
end
end