Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/ecto/migration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,19 @@ defmodule Ecto.Migration do
such as migrations. For a repository named `MyApp.FooRepo`, `:priv` defaults to
"priv/foo_repo" and migrations should be placed at "priv/foo_repo/migrations"

* `:migrations_paths` - a list of paths where migrations are located. This option
allows you to specify multiple migration directories that will all be used when
running migrations. Relative paths are considered relative to the application root
(the directory containing `mix.exs`). If this option is not set, the default path
is derived from the `:priv` configuration. For example:

config :app, App.Repo,
migrations_paths: ["priv/repo/migrations", "priv/repo/tenant_migrations"]

When using this option, all specified paths will be checked for migrations, and
migrations will be sorted by version across all directories as if they were in
a single directory.

* `:start_apps_before_migration` - A list of applications to be started before
running migrations. Used by `Ecto.Migrator.with_repo/3` and the migration tasks:

Expand Down
79 changes: 72 additions & 7 deletions lib/ecto/migrator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,78 @@ defmodule Ecto.Migrator do
This function accepts an optional second parameter to customize the
migrations directory. This can be used to specify a custom migrations
path.

> #### Discouraged {: .warning}
>
> If your repository is configured with multiple migration paths via
> `:migrations_paths`, this function will raise an error. Use
> `migrations_paths/1` instead.

"""
@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))

case config[:migrations_paths] do
[_first, _second | _rest] ->
raise ArgumentError, """
cannot use migrations_path/1 when multiple migration paths are configured.

The repository #{inspect(repo)} has #{length(config[:migrations_paths])} migration paths configured
via :migrations_paths. Please use migrations_paths/1 instead to get all configured paths.
"""

_other ->
hd(migrations_paths(repo, directory: directory))
end
end

@doc """
Gets the migrations paths from a repository configuration.

This function checks the repository configuration for the `:migrations_paths`
option. If found, it returns a list of absolute paths by resolving any relative
paths against the application directory. If not found, it returns a single-element
list containing the default migrations path.

Relative paths in the `:migrations_paths` configuration are considered relative
to the root of the application (the directory containing `mix.exs`).

## Examples

# In config/config.exs
config :my_app, MyApp.Repo,
migrations_paths: ["priv/repo/migrations", "priv/repo/tenant_migrations"]

"""
@spec migrations_paths(Ecto.Repo.t(), Keyword.t()) :: [String.t()]
def migrations_paths(repo, opts \\ []) do
config = repo.config()

case config[:migrations_paths] do
nil ->
priv =
config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"

app = Keyword.fetch!(config, :otp_app)
directory = Keyword.get(opts, :directory, "migrations")
[Application.app_dir(app, Path.join(priv, directory))]

paths when is_list(paths) ->
app = Keyword.fetch!(config, :otp_app)

Enum.map(paths, fn path ->
if Path.type(path) == :absolute do
path
else
Application.app_dir(app, path)
end
end)

other ->
raise ArgumentError,
":migrations_paths must be a list of paths, got: #{inspect(other)}"
end
end

@doc """
Expand Down Expand Up @@ -372,13 +437,13 @@ defmodule Ecto.Migrator do

Equivalent to:

Ecto.Migrator.run(repo, [Ecto.Migrator.migrations_path(repo)], direction, opts)
Ecto.Migrator.run(repo, Ecto.Migrator.migrations_paths(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)
run(repo, migrations_paths(repo), direction, opts)
end

@doc ~S"""
Expand Down Expand Up @@ -464,12 +529,12 @@ defmodule Ecto.Migrator do

Equivalent to:

Ecto.Migrator.migrations(repo, [Ecto.Migrator.migrations_path(repo)])
Ecto.Migrator.migrations(repo, Ecto.Migrator.migrations_paths(repo))

"""
@spec migrations(Ecto.Repo.t()) :: [{:up | :down, id :: integer(), name :: String.t()}]
def migrations(repo) do
migrations(repo, [migrations_path(repo)])
migrations(repo, migrations_paths(repo))
end

@doc """
Expand Down
36 changes: 35 additions & 1 deletion lib/mix/ecto_sql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,45 @@ defmodule Mix.EctoSQL do

@doc """
Ensures the given repository's migrations paths exists on the file system.

This function checks for migrations paths in the following order:
1. Command-line options (`--migrations_path`)
2. Repository configuration (`:migrations_paths`)
3. Default path based on `:priv` configuration or "priv/repo/migrations"
"""
@spec ensure_migrations_paths(Ecto.Repo.t(), Keyword.t()) :: [String.t()]
def ensure_migrations_paths(repo, opts) do
paths = Keyword.get_values(opts, :migrations_path)
paths = if paths == [], do: [Path.join(source_repo_priv(repo), "migrations")], else: paths

paths =
if paths == [] do
# Use repo config if available, otherwise fall back to default
config = repo.config()

case config[:migrations_paths] do
nil ->
[Path.join(source_repo_priv(repo), "migrations")]

config_paths when is_list(config_paths) ->
app = Keyword.fetch!(config, :otp_app)
# In Mix context, we use deps_paths or cwd for path resolution
base_dir = Mix.Project.deps_paths()[app] || File.cwd!()

Enum.map(config_paths, fn path ->
if Path.type(path) == :absolute do
path
else
Path.join(base_dir, path)
end
end)

other ->
raise ArgumentError,
":migrations_paths must be a list of paths, got: #{inspect(other)}"
end
else
paths
end

if not Mix.Project.umbrella?() do
for path <- paths, not File.dir?(path) do
Expand Down
Loading