diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex index dd22b107ab..09582b3fdd 100644 --- a/lib/ecto/repo.ex +++ b/lib/ecto/repo.ex @@ -292,12 +292,12 @@ defmodule Ecto.Repo do def transaction(fun_or_multi, opts \\ []) do repo = get_dynamic_repo() - Ecto.Repo.Transaction.transaction( - __MODULE__, - repo, - fun_or_multi, + {adapter_meta, opts} = Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:transaction, opts)) - ) + + {fun_or_multi, opts} = prepare_transaction(fun_or_multi, opts) + + Ecto.Repo.Transaction.transaction(__MODULE__, repo, fun_or_multi, {adapter_meta, opts}) end def in_transaction? do @@ -618,6 +618,9 @@ defmodule Ecto.Repo do def prepare_query(operation, query, opts), do: {query, opts} defoverridable prepare_query: 3 + + def prepare_transaction(fun_or_multi, opts), do: {fun_or_multi, opts} + defoverridable prepare_transaction: 2 end end end @@ -1274,6 +1277,31 @@ defmodule Ecto.Repo do {Ecto.Query.t(), Keyword.t()} when operation: :all | :update_all | :delete_all | :stream | :insert_all + @doc """ + A user-customizable callback invoked on transaction operations. + + This callback can be used to further modify the given Ecto Multi and options in a transaction operation + before it is transformed and sent to the database. + + This callback is only invoked in transactions. + + ## Examples + + Imagine you want to prepend a SQL comment to commit statements using the `commit_comment` option on transactions. + + @impl true + def prepare_transaction(multi_or_fun, opts) do + opts = Keyword.put_new_lazy(opts, :commit_comment, fn -> extract_comment(opts) end) + {multi_or_fun, opts} + end + + The callback will be invoked for every transaction operation, and it will try to extract the appropriate commit comment, + that will be subsequently used by the adapters if they support this option. + """ + @doc group: "User callbacks" + @callback prepare_transaction(fun_or_multi :: fun | Ecto.Multi.t(), opts :: Keyword.t()) :: + {fun_or_multi :: fun | Ecto.Multi.t(), Keyword.t()} + @doc """ A user customizable callback invoked to retrieve default options for operations. @@ -1504,7 +1532,8 @@ defmodule Ecto.Repo do delete!: 2, insert_or_update: 2, insert_or_update!: 2, - prepare_query: 3 + prepare_query: 3, + prepare_transaction: 2 @doc """ Inserts all entries into the repository. diff --git a/test/ecto/multi_test.exs b/test/ecto/multi_test.exs index dcbb0f6944..ebe8ccce03 100644 --- a/test/ecto/multi_test.exs +++ b/test/ecto/multi_test.exs @@ -292,7 +292,7 @@ defmodule Ecto.MultiTest do assert [{:fun, {:run, _fun}}] = multi.operations assert {:ok, changes} = TestRepo.transaction(multi) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert changes[:fun] == {1, nil} end @@ -319,7 +319,7 @@ defmodule Ecto.MultiTest do assert [{:fun, {:run, _fun}}] = multi.operations assert {:ok, changes} = TestRepo.transaction(multi) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert changes[:fun] == {1, nil} end @@ -345,7 +345,7 @@ defmodule Ecto.MultiTest do assert [{:fun, {:run, _fun}}] = multi.operations assert {:ok, changes} = TestRepo.transaction(multi) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert changes[:fun] == {1, nil} end @@ -554,7 +554,7 @@ defmodule Ecto.MultiTest do |> Multi.delete_all(:delete_all, Comment) assert {:ok, changes} = TestRepo.transaction(multi) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert {:messages, [ @@ -598,7 +598,7 @@ defmodule Ecto.MultiTest do test "with empty multi" do assert {:ok, changes} = TestRepo.transaction(Multi.new()) - refute_received {:transaction, _} + refute_received {:transaction, _, _} assert changes == %{} end @@ -613,7 +613,7 @@ defmodule Ecto.MultiTest do |> Multi.delete(:delete, changeset) assert {:error, :run, "error from run", changes} = TestRepo.transaction(multi) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, _} assert {:messages, [{:insert, %{source: "comments"}}]} = Process.info(self(), :messages) assert %Comment{} = changes.insert @@ -636,7 +636,7 @@ defmodule Ecto.MultiTest do |> Multi.delete(:delete, changeset) assert {:error, :update, error, changes} = TestRepo.transaction(multi) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, _} assert {:messages, [{:insert, %{source: "comments"}}]} = Process.info(self(), :messages) assert %Comment{} = changes.insert @@ -657,14 +657,14 @@ defmodule Ecto.MultiTest do assert {:error, :invalid, invalid, %{}} = TestRepo.transaction(multi) assert invalid.data == changeset.data - refute_received {:transaction, _} + refute_received {:transaction, _, _} end test "checks error operation before starting transaction" do multi = Multi.new() |> Multi.error(:invalid, "error") assert {:error, :invalid, "error", %{}} = TestRepo.transaction(multi) - refute_received {:transaction, _} + refute_received {:transaction, _, _} end end diff --git a/test/ecto/repo/belongs_to_test.exs b/test/ecto/repo/belongs_to_test.exs index e38352f6c7..c83cec8d1f 100644 --- a/test/ecto/repo/belongs_to_test.exs +++ b/test/ecto/repo/belongs_to_test.exs @@ -153,9 +153,9 @@ defmodule Ecto.Repo.BelongsToTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} end @@ -174,7 +174,7 @@ defmodule Ecto.Repo.BelongsToTest do assert schema.assoc.sub_assoc_id == schema.assoc.sub_assoc.id # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} refute_received {:rollback, _} end @@ -212,9 +212,9 @@ defmodule Ecto.Repo.BelongsToTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} end @@ -444,7 +444,7 @@ defmodule Ecto.Repo.BelongsToTest do assert schema.assoc.sub_assoc.id # One transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} refute_received {:rollback, _} end @@ -469,9 +469,9 @@ defmodule Ecto.Repo.BelongsToTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} end diff --git a/test/ecto/repo/has_assoc_test.exs b/test/ecto/repo/has_assoc_test.exs index d370207400..932f829e4e 100644 --- a/test/ecto/repo/has_assoc_test.exs +++ b/test/ecto/repo/has_assoc_test.exs @@ -215,9 +215,9 @@ defmodule Ecto.Repo.HasAssocTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} end @@ -234,7 +234,7 @@ defmodule Ecto.Repo.HasAssocTest do assert schema.assoc.sub_assoc.id # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} refute_received {:rollback, _} end @@ -271,9 +271,9 @@ defmodule Ecto.Repo.HasAssocTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} end @@ -573,7 +573,7 @@ defmodule Ecto.Repo.HasAssocTest do assert schema.assoc.sub_assoc.id # One transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} refute_received {:rollback, _} end @@ -598,9 +598,9 @@ defmodule Ecto.Repo.HasAssocTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} end diff --git a/test/ecto/repo/many_to_many_test.exs b/test/ecto/repo/many_to_many_test.exs index 0bf30f1f9d..8145c8a2a3 100644 --- a/test/ecto/repo/many_to_many_test.exs +++ b/test/ecto/repo/many_to_many_test.exs @@ -274,9 +274,9 @@ defmodule Ecto.Repo.ManyToManyTest do refute changeset.valid? # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} refute_received {:insert_all, _, _} end @@ -294,7 +294,7 @@ defmodule Ecto.Repo.ManyToManyTest do assert hd(schema.assocs).sub_assoc.id # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} refute_received {:rollback, _} assert_received {:insert_all, %{source: "schemas_assocs"}, [[my_assoc_id: 1, my_schema_id: 1]]} end @@ -332,9 +332,9 @@ defmodule Ecto.Repo.ManyToManyTest do refute Map.has_key?(assoc.changes.sub_assoc.changes, :id) # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} refute_received {:insert_all, _, _} end @@ -624,7 +624,7 @@ defmodule Ecto.Repo.ManyToManyTest do assert hd(schema.assocs).sub_assoc.id # One transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} refute_received {:rollback, _} refute_received {:insert_all, _, _} end @@ -652,9 +652,9 @@ defmodule Ecto.Repo.ManyToManyTest do refute Map.has_key?(assoc.changes.sub_assoc.changes, :my_assoc_id) # Just one transaction was used - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert_received {:rollback, ^changeset} - refute_received {:transaction, _} + refute_received {:transaction, _, _} refute_received {:rollback, _} refute_received {:insert_all, _, _} end diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs index 56fd4be402..efc4322a6e 100644 --- a/test/ecto/repo_test.exs +++ b/test/ecto/repo_test.exs @@ -1593,13 +1593,13 @@ defmodule Ecto.RepoTest do test "does not run transaction without prepare" do TestRepo.insert!(%MySchema{id: 1}) - refute_received {:transaction, _} + refute_received {:transaction, _, _} end test "insert runs prepare callbacks in transaction" do changeset = prepare_changeset() TestRepo.insert!(changeset) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert Process.get(:ecto_repo) == TestRepo assert Process.get(:ecto_counter) == 2 end @@ -1652,7 +1652,7 @@ defmodule Ecto.RepoTest do test "update runs prepare callbacks in transaction" do changeset = prepare_changeset() TestRepo.update!(changeset) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert Process.get(:ecto_repo) == TestRepo assert Process.get(:ecto_counter) == 2 end @@ -1705,7 +1705,7 @@ defmodule Ecto.RepoTest do test "delete runs prepare callbacks in transaction" do changeset = prepare_changeset() TestRepo.delete!(changeset) - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert Process.get(:ecto_repo) == TestRepo assert Process.get(:ecto_counter) == 2 end @@ -1760,7 +1760,7 @@ defmodule Ecto.RepoTest do %MySchemaEmbedsMany{embeds: [embed]} = TestRepo.insert!(changeset) assert embed.x == "ONE" - assert_received {:transaction, _} + assert_received {:transaction, _, _} assert Process.get(:ecto_repo) == TestRepo assert Process.get(:ecto_counter) == 2 end @@ -2209,18 +2209,42 @@ defmodule Ecto.RepoTest do end end + describe "prepare_transaction" do + defmodule PrepareTransactionRepo do + use Ecto.Repo, otp_app: :ecto, adapter: Ecto.TestAdapter, stacktrace: true + + def prepare_transaction(fun_or_multi, opts) do + send(self(), {:prepare_transaction, fun_or_multi, opts}) + {fun_or_multi, Keyword.put(opts, :commit_comment, "my_comment")} + end + end + + setup do + _ = PrepareTransactionRepo.start_link(url: "ecto://user:pass@local/hello") + :ok + end + + test "transaction" do + fun = fn -> :ok end + opts = [commit_comment: "my_comment"] + assert {:ok, :ok} = PrepareTransactionRepo.transaction(fun) + assert_received {:prepare_transaction, _, _} + assert_received {:transaction, _fun, ^opts} + end + end + describe "transaction" do test "an arity zero function will be executed any it's value returned" do fun = fn -> :ok end assert {:ok, :ok} = TestRepo.transaction(fun) - assert_received {:transaction, _} + assert_received {:transaction, _, _} end test "an arity one function will be passed the repo as first argument" do fun = fn repo -> repo end assert {:ok, TestRepo} = TestRepo.transaction(fun) - assert_received {:transaction, _} + assert_received {:transaction, _, _} end end @@ -2245,7 +2269,6 @@ defmodule Ecto.RepoTest do ] end - test "update only saves changes for writable: :always" do %MySchemaWritable{id: 1} |> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12}) diff --git a/test/support/test_repo.exs b/test/support/test_repo.exs index 01754db61a..4df2752802 100644 --- a/test/support/test_repo.exs +++ b/test/support/test_repo.exs @@ -136,10 +136,10 @@ defmodule Ecto.TestAdapter do ## Transactions - def transaction(mod, _opts, fun) do + def transaction(mod, opts, fun) do # Makes transactions "trackable" in tests Process.put({mod, :in_transaction?}, true) - send(self(), {:transaction, fun}) + send(self(), {:transaction, fun, opts}) try do {:ok, fun.()}