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,148 @@
defmodule Mix.Ecto do
@moduledoc """
Conveniences for writing Ecto related Mix tasks.
"""
@doc """
Parses the repository option from the given command line args list.
If no repo option is given, it is retrieved from the application environment.
"""
@spec parse_repo([term]) :: [Ecto.Repo.t]
def parse_repo(args) do
parse_repo(args, [])
end
defp parse_repo([key, value|t], acc) when key in ~w(--repo -r) do
parse_repo t, [Module.concat([value])|acc]
end
defp parse_repo([_|t], acc) do
parse_repo t, acc
end
defp parse_repo([], []) do
apps =
if apps_paths = Mix.Project.apps_paths() do
Enum.filter(Mix.Project.deps_apps(), &is_map_key(apps_paths, &1))
else
[Mix.Project.config()[:app]]
end
apps
|> Enum.flat_map(fn app ->
Application.load(app)
Application.get_env(app, :ecto_repos, [])
end)
|> Enum.uniq()
|> case do
[] ->
Mix.shell().error """
warning: could not find Ecto repos in any of the apps: #{inspect apps}.
You can avoid this warning by passing the -r flag or by setting the
repositories managed by those applications in your config/config.exs:
config #{inspect hd(apps)}, ecto_repos: [...]
"""
[]
repos ->
repos
end
end
defp parse_repo([], acc) do
Enum.reverse(acc)
end
@doc """
Ensures the given module is an Ecto.Repo.
"""
@spec ensure_repo(module, list) :: Ecto.Repo.t
def ensure_repo(repo, args) do
# Do not pass the --force switch used by some tasks downstream
args = List.delete(args, "--force")
Mix.Task.run("app.config", args)
case Code.ensure_compiled(repo) do
{:module, _} ->
if function_exported?(repo, :__adapter__, 0) do
repo
else
Mix.raise "Module #{inspect repo} is not an Ecto.Repo. " <>
"Please configure your app accordingly or pass a repo with the -r option."
end
{:error, error} ->
Mix.raise "Could not load #{inspect repo}, error: #{inspect error}. " <>
"Please configure your app accordingly or pass a repo with the -r option."
end
end
@doc """
Asks if the user wants to open a file based on ECTO_EDITOR.
By default, it attempts to open the file and line using the
`file:line` notation. For example, if your editor is called
`subl`, it will open the file as:
subl path/to/file:line
It is important that you choose an editor command that does
not block nor that attempts to run an editor directly in the
terminal. Command-line based editors likely need extra
configuration so they open up the given file and line in a
separate window.
Custom editors are supported by using the `__FILE__` and
`__LINE__` notations, for example:
ECTO_EDITOR="my_editor +__LINE__ __FILE__"
and Elixir will properly interpolate values.
"""
@spec open?(binary, non_neg_integer) :: boolean
def open?(file, line \\ 1) do
editor = System.get_env("ECTO_EDITOR") || ""
if editor != "" do
command =
if editor =~ "__FILE__" or editor =~ "__LINE__" do
editor
|> String.replace("__FILE__", inspect(file))
|> String.replace("__LINE__", Integer.to_string(line))
else
"#{editor} #{inspect(file)}:#{line}"
end
Mix.shell().cmd(command)
true
else
false
end
end
@doc """
Gets a path relative to the application path.
Raises on umbrella application.
"""
def no_umbrella!(task) do
if Mix.Project.umbrella?() do
Mix.raise "Cannot run task #{inspect task} from umbrella project root. " <>
"Change directory to one of the umbrella applications and try again"
end
end
@doc """
Returns `true` if module implements behaviour.
"""
def ensure_implements(module, behaviour, message) do
all = Keyword.take(module.__info__(:attributes), [:behaviour])
unless [behaviour] in Keyword.values(all) do
Mix.raise "Expected #{inspect module} to implement #{inspect behaviour} " <>
"in order to #{message}"
end
end
end

View File

@@ -0,0 +1,77 @@
defmodule Mix.Tasks.Ecto.Create do
use Mix.Task
import Mix.Ecto
@shortdoc "Creates the repository storage"
@switches [
quiet: :boolean,
repo: [:string, :keep],
no_compile: :boolean,
no_deps_check: :boolean
]
@aliases [
r: :repo,
q: :quiet
]
@moduledoc """
Create the storage for the given repository.
The repositories to create 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 create
multiple repositories, set `:ecto_repos` accordingly or pass the `-r`
flag multiple times.
## Examples
$ mix ecto.create
$ mix ecto.create -r Custom.Repo
## Command line options
* `-r`, `--repo` - the repo to create
* `--quiet` - do not log output
* `--no-compile` - do not compile before creating
* `--no-deps-check` - do not compile before creating
"""
@impl true
def run(args) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
Enum.each(repos, fn repo ->
ensure_repo(repo, args)
ensure_implements(
repo.__adapter__(),
Ecto.Adapter.Storage,
"create storage for #{inspect(repo)}"
)
case repo.__adapter__().storage_up(repo.config()) do
:ok ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has been created")
end
{:error, :already_up} ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has already been created")
end
{:error, term} when is_binary(term) ->
Mix.raise("The database for #{inspect(repo)} couldn't be created: #{term}")
{:error, term} ->
Mix.raise("The database for #{inspect(repo)} couldn't be created: #{inspect(term)}")
end
end)
end
end

View File

@@ -0,0 +1,106 @@
defmodule Mix.Tasks.Ecto.Drop do
use Mix.Task
import Mix.Ecto
@shortdoc "Drops the repository storage"
@default_opts [force: false, force_drop: false]
@aliases [
f: :force,
q: :quiet,
r: :repo
]
@switches [
force: :boolean,
force_drop: :boolean,
quiet: :boolean,
repo: [:keep, :string],
no_compile: :boolean,
no_deps_check: :boolean
]
@moduledoc """
Drop the storage for the given repository.
The repositories to drop 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 drop
multiple repositories, set `:ecto_repos` accordingly or pass the `-r`
flag multiple times.
## Examples
$ mix ecto.drop
$ mix ecto.drop -r Custom.Repo
## Command line options
* `-r`, `--repo` - the repo to drop
* `-q`, `--quiet` - run the command quietly
* `-f`, `--force` - do not ask for confirmation when dropping the database.
Configuration is asked only when `:start_permanent` is set to true
(typically in production)
* `--force-drop` - force the database to be dropped even
if it has connections to it (requires PostgreSQL 13+)
* `--no-compile` - do not compile before dropping
* `--no-deps-check` - do not compile before dropping
"""
@impl true
def run(args) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts = Keyword.merge(@default_opts, opts)
Enum.each(repos, fn repo ->
ensure_repo(repo, args)
ensure_implements(
repo.__adapter__(),
Ecto.Adapter.Storage,
"drop storage for #{inspect(repo)}"
)
if skip_safety_warnings?() or
opts[:force] or
Mix.shell().yes?(
"Are you sure you want to drop the database for repo #{inspect(repo)}?"
) do
drop_database(repo, opts)
end
end)
end
defp skip_safety_warnings? do
Mix.Project.config()[:start_permanent] != true
end
defp drop_database(repo, opts) do
config =
opts
|> Keyword.take([:force_drop])
|> Keyword.merge(repo.config())
case repo.__adapter__().storage_down(config) do
:ok ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has been dropped")
end
{:error, :already_down} ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has already been dropped")
end
{:error, term} when is_binary(term) ->
Mix.raise("The database for #{inspect(repo)} couldn't be dropped: #{term}")
{:error, term} ->
Mix.raise("The database for #{inspect(repo)} couldn't be dropped: #{inspect(term)}")
end
end
end

View File

@@ -0,0 +1,30 @@
defmodule Mix.Tasks.Ecto do
use Mix.Task
@shortdoc "Prints Ecto help information"
@moduledoc """
Prints Ecto tasks and their information.
$ mix ecto
"""
@impl true
def run(args) do
{_opts, args} = OptionParser.parse!(args, strict: [])
case args do
[] -> general()
_ -> Mix.raise "Invalid arguments, expected: mix ecto"
end
end
defp general() do
Application.ensure_all_started(:ecto)
Mix.shell().info "Ecto v#{Application.spec(:ecto, :vsn)}"
Mix.shell().info "A toolkit for data mapping and language integrated query for Elixir."
Mix.shell().info "\nAvailable tasks:\n"
Mix.Tasks.Help.run(["--search", "ecto."])
end
end

View File

@@ -0,0 +1,110 @@
defmodule Mix.Tasks.Ecto.Gen.Repo do
use Mix.Task
import Mix.Ecto
import Mix.Generator
@shortdoc "Generates a new repository"
@switches [
repo: [:string, :keep],
]
@aliases [
r: :repo,
]
@moduledoc """
Generates a new repository.
The repository will be placed in the `lib` directory.
## Examples
$ mix ecto.gen.repo -r Custom.Repo
This generator will automatically open the config/config.exs
after generation if you have `ECTO_EDITOR` set in your environment
variable.
## Command line options
* `-r`, `--repo` - the repo to generate
"""
@impl true
def run(args) do
no_umbrella!("ecto.gen.repo")
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
repo =
case Keyword.get_values(opts, :repo) do
[] -> Mix.raise "ecto.gen.repo expects the repository to be given as -r MyApp.Repo"
[repo] -> Module.concat([repo])
[_ | _] -> Mix.raise "ecto.gen.repo expects a single repository to be given"
end
config = Mix.Project.config()
underscored = Macro.underscore(inspect(repo))
base = Path.basename(underscored)
file = Path.join("lib", underscored) <> ".ex"
app = config[:app] || :YOUR_APP_NAME
opts = [mod: repo, app: app, base: base]
create_directory Path.dirname(file)
create_file file, repo_template(opts)
config_path = config[:config_path] || "config/config.exs"
case File.read(config_path) do
{:ok, contents} ->
check = String.contains?(contents, "import Config")
config_first_line = get_first_config_line(check) <> "\n"
new_contents = config_first_line <> "\n" <> config_template(opts)
Mix.shell().info [:green, "* updating ", :reset, config_path]
File.write! config_path, String.replace(contents, config_first_line, new_contents)
{:error, _} ->
create_file config_path, "import Config\n\n" <> config_template(opts)
end
open?(config_path, 3)
Mix.shell().info """
Don't forget to add your new repo to your supervision tree
(typically in lib/#{app}/application.ex):
def start(_type, _args) do
children = [
#{inspect repo},
]
And to add it to the list of Ecto repositories in your
configuration files (so Ecto tasks work as expected):
config #{inspect app},
ecto_repos: [#{inspect repo}]
"""
end
defp get_first_config_line(true), do: "import Config"
defp get_first_config_line(false), do: "use Mix.Config"
embed_template :repo, """
defmodule <%= inspect @mod %> do
use Ecto.Repo,
otp_app: <%= inspect @app %>,
adapter: Ecto.Adapters.Postgres
end
"""
embed_template :config, """
config <%= inspect @app %>, <%= inspect @mod %>,
database: "<%= @app %>_<%= @base %>",
username: "user",
password: "pass",
hostname: "localhost"
"""
end