diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex index d791cdc7..849d6863 100644 --- a/lib/ecto/migration.ex +++ b/lib/ecto/migration.ex @@ -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 @@ -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 @@ -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`. @@ -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 @@ -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 diff --git a/lib/ecto/migration/runner.ex b/lib/ecto/migration/runner.ex index 813c7006..2e769ea5 100644 --- a/lib/ecto/migration/runner.ex +++ b/lib/ecto/migration/runner.ex @@ -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()) diff --git a/test/ecto/migrator_test.exs b/test/ecto/migrator_test.exs index 82c3b889..acb43f07 100644 --- a/test/ecto/migrator_test.exs +++ b/test/ecto/migrator_test.exs @@ -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 @@ -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)