Skip to content
Merged
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
26 changes: 20 additions & 6 deletions lib/ecto/migration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ defmodule Ecto.Migration do
field type with database-specific options, you can pass atoms containing
these options like `:"int unsigned"`, `:"time without time zone"`, etc.

## Flushing
## Executing and flushing

Instructions inside of migrations are not executed immediately. Instead
they are performed after the relevant `up`, `change`, or `down` callback
Expand All @@ -164,6 +164,10 @@ defmodule Ecto.Migration do
...
end

However `flush/0` will raise if it would be called from `change` function when doing a rollback.
To avoid that we recommend to use `execute/2` with anonymous functions instead.
For more information and example usage please take a look at `execute/2` function.

## Comments

Migrations where you create or alter a table support specifying table
Expand Down Expand Up @@ -740,7 +744,7 @@ defmodule Ecto.Migration do
end

@doc """
Executes arbitrary SQL or a keyword command.
Executes arbitrary SQL, anonymous function or a keyword command.

Reversible commands can be defined by calling `execute/2`.

Expand All @@ -750,8 +754,9 @@ defmodule Ecto.Migration do

execute create: "posts", capped: true, size: 1024

execute(fn -> repo().query!("select 'Anonymous function query …';", [], [log: :info]) end)
"""
def execute(command) when is_binary(command) or is_list(command) do
def execute(command) when is_binary(command) or is_function(command, 0) or is_list(command) do
Runner.execute command
end

Expand All @@ -766,11 +771,20 @@ defmodule Ecto.Migration do

## Examples

execute "CREATE EXTENSION postgres_fdw", "DROP EXTENSION postgres_fdw"
defmodule MyApp.MyMigration do
use Ecto.Migration

def change do
execute "CREATE EXTENSION postgres_fdw", "DROP EXTENSION postgres_fdw"
execute(&execute_up/0, &execute_down/0)
end

defp execute_up, do: repo().query!("select 'Up query …';", [], [log: :info])
defp execute_down, do: repo().query!("select 'Down query …';", [], [log: :info])
end
"""
def execute(up, down) when (is_binary(up) or is_list(up)) and
(is_binary(down) or is_list(down)) do
def execute(up, down) when (is_binary(up) or is_function(up, 0) or is_list(up)) and
(is_binary(down) or is_function(down, 0) or is_list(down)) do
Runner.execute %Command{up: up, down: down}
end

Expand Down
5 changes: 5 additions & 0 deletions lib/ecto/migration/runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ defmodule Ecto.Migration.Runner 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())
Expand Down
59 changes: 58 additions & 1 deletion test/ecto/migrator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,40 @@ defmodule Ecto.MigratorTest do
end
end

defmodule ExecuteOneAnonymousFunctionMigration do
use Ecto.Migration

require Logger

def change do
execute(fn -> Logger.info("This should fail on rollback.") end)
end
end

defmodule ExecuteTwoAnonymousFunctionsMigration do
use Ecto.Migration

require Logger

@disable_ddl_transaction true

@migrate_first "select 'This is a first part of ecto.migrate';"
@migrate_middle "select 'In the middle of ecto.migrate';"
@migrate_second "select 'This is a second part of ecto.migrate';"
@rollback_first "select 'This is a first part of ecto.rollback';"
@rollback_middle "select 'In the middle of ecto.rollback';"
@rollback_second "select 'This is a second part of ecto.rollback';"

def change do
execute(@migrate_first, @rollback_second)
execute(&execute_up/0, &execute_down/0)
execute(@migrate_second, @rollback_first)
end

defp execute_up, do: Logger.info(@migrate_middle)
defp execute_down, do: Logger.info(@rollback_middle)
end

defmodule InvalidMigration do
use Ecto.Migration
end
Expand Down Expand Up @@ -169,7 +203,30 @@ defmodule Ecto.MigratorTest do
"""
end

@tag :current
test "execute one anonymous function" do
module = ExecuteOneAnonymousFunctionMigration
num = System.unique_integer([:positive])
capture_log(fn -> :ok = up(TestRepo, num, module, [log: false]) end)
message = "no function clause matching in Ecto.Migration.Runner.command/1"
assert_raise(FunctionClauseError, message, fn -> down(TestRepo, num, module, [log: false]) end)
end

test "execute two anonymous functions" do
module = ExecuteTwoAnonymousFunctionsMigration
num = System.unique_integer([:positive])
args = [TestRepo, num, module, [log: :info]]

for {name, direction} <- [migrate: :up, rollback: :down] do
output = capture_log(fn -> :ok = apply(Ecto.Migrator, direction, args) end)
lines = String.split(output, "\n")
assert Enum.at(lines, 1) =~ "== Running #{num} #{inspect(module)}.change/0"
assert Enum.at(lines, 3) =~ ~s[execute "select 'This is a first part of ecto.#{name}';"]
assert Enum.at(lines, 5) =~ "select 'In the middle of ecto.#{name}';"
assert Enum.at(lines, 7) =~ ~s[execute "select 'This is a second part of ecto.#{name}';"]
assert Enum.at(lines, 9) =~ ~r"Migrated #{num} in \d.\ds"
end
end

test "flush" do
num = System.unique_integer([:positive])
assert :ok == up(TestRepo, num, EmptyUpDownMigration, log: false)
Expand Down