Repository: elixir-ecto/ecto
Pull request #4605 (2025-04-14 18:41:11)

Add Repo.prepare_transaction/2
### What I'm doing

Adding a new callback `prepare_transaction/2` to `Ecto.Repo`.

### Why?

To offer the possibility of further modifying the given `Ecto.Multi` or options in transaction operations before it is transformed and sent to the database. @josevalim [suggested](https://github.com/elixir-ecto/postgrex/pull/709#issuecomment-2773787388) to consider adding this function to inject the `commit_comment` option if the users wanted to extract that comment based on the `stacktrace`.

### Approach

1. Create the new `prepare_transaction/2` callback.
2. Provide a default implementation of the callback.
3. Invoke that callback from the `transaction` operation in `Ecto.Repo`.
4. Add unit tests.
5. Update the `Ecto.TestAdapter` to include the `opts` given to the transaction operation.
6. Update broken unit tests (after the previous change).

### Alternate approaches considered

As mentioned in this [discussion](https://github.com/elixir-ecto/postgrex/pull/709#issuecomment-2773559339), if the user wants to inject a `commit_comment` based on the `stacktrace`, the only option available today is the following _hack_:

```elixir
defoverridable transaction: 1, transaction: 2

def transaction(fun_or_multi, opts \\ []) do
  # The opts[:stacktrace] option is not available at this point, so, we probably have to invoke
  # Ecto.Repo.Supervisor.tuplet/2 twice. Sad times :-(
  # Is that, or do we avoid the `super` at the end and do everything ourselves here?
  opts = Keyword.put_new_lazy(opts, :commit_comment, fn -> extract_comment(opts) end)

  super(fun_or_multi, opts)
end
```

And we certainly don't want to promote these things ;)

### I’d like feedback on

The current implementation of `prepare_transaction/2` takes a `fun_or_multi` as the first argument, but it doesn't make too much sense to accept the `fun` part (a function with arity zero or one) because I don't see how that would be useful or how it can be extended.  The original implementation of `prepare_transaction/2` accepts a `fun` to keep things as simple as possible. As always, I'm open to suggestions.


Patch:
diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex
index dd22b107..12e0f4d3 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
@@ -968,6 +971,8 @@ defmodule Ecto.Repo do
   to the same schema. Ordering is guaranteed to be kept. Results not found in
   the database will be returned as `nil`.
 
+  Preloaded association will be discarded and need to be preloaded again.
+
   ## Example
 
       MyRepo.reload(post)
@@ -1274,6 +1279,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 +1534,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.


Test patch:
diff --git a/test/ecto/changeset_test.exs b/test/ecto/changeset_test.exs
index b70216f0..a7b966fa 100644
--- a/test/ecto/changeset_test.exs
+++ b/test/ecto/changeset_test.exs
@@ -3,16 +3,9 @@ defmodule Ecto.ChangesetTest do
   import Ecto.Changeset
   require Ecto.Query
 
-  defmacrop assert_eq_macro_to_string(ast, post_1_13, pre_1_13) do
-    # AST is represented as string differently on versions pre 1.13
-    if Version.match?(System.version(), ">= 1.13.0-dev") do
-      quote do
-        assert Macro.to_string(unquote(ast)) == unquote(post_1_13)
-      end
-    else
-      quote do
-        assert Macro.to_string(unquote(ast)) == unquote(pre_1_13)
-      end
+  defmacrop assert_eq_macro_to_string(ast, string) do
+    quote do
+      assert Macro.to_string(unquote(ast)) == unquote(string)
     end
   end
 
@@ -2372,8 +2365,8 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0)", "not(&0.id() == ^0)")
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0)")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys without query option for loaded schema" do
@@ -2386,13 +2379,9 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(
-        pk_expr,
-        "not (&0.id() == ^0 and &0.token() == ^1)",
-        "not(&0.id() == ^0 and &0.token() == ^1)"
-      )
+      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0 and &0.token() == ^1)")
 
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for single primary key with query option for loaded schema" do
@@ -2408,14 +2397,10 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(
-        query_expr,
-        "is_nil(&0.published_at())",
-        "is_nil(&0.published_at())"
-      )
+      assert_eq_macro_to_string(query_expr, "is_nil(&0.published_at())")
 
-      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0)", "not(&0.id() == ^0)")
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0)")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys with query option for loaded schema" do
@@ -2431,19 +2416,11 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(
-        query_expr,
-        "is_nil(&0.published_at())",
-        "is_nil(&0.published_at())"
-      )
+      assert_eq_macro_to_string(query_expr, "is_nil(&0.published_at())")
 
-      assert_eq_macro_to_string(
-        pk_expr,
-        "not (&0.id() == ^0 and &0.token() == ^1)",
-        "not(&0.id() == ^0 and &0.token() == ^1)"
-      )
+      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0 and &0.token() == ^1)")
 
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for single primary key without query option when schema wasn't loaded" do
@@ -2453,7 +2430,7 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys without query option when schema wasn't loaded" do
@@ -2463,7 +2440,7 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for single primary key with query option when schema wasn't loaded" do
@@ -2476,13 +2453,9 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(
-        query_expr,
-        "is_nil(&0.published_at())",
-        "is_nil(&0.published_at())"
-      )
+      assert_eq_macro_to_string(query_expr, "is_nil(&0.published_at())")
 
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys with query option when schema wasn't loaded" do
@@ -2495,13 +2468,9 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(
-        query_expr,
-        "is_nil(&0.published_at())",
-        "is_nil(&0.published_at())"
-      )
+      assert_eq_macro_to_string(query_expr, "is_nil(&0.published_at())")
 
-      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0")
     end
 
     test "only queries the db when necessary" do
@@ -2555,7 +2524,7 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(check_expr, "&0.origin() == ^0", "&0.origin() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.origin() == ^0")
     end
   end
 
@@ -3696,7 +3665,13 @@ defmodule Ecto.ChangesetTest do
     end
 
     test "redacts all non-primary-key fields when schema sets @schema_redact :all_except_primary_keys" do
-      changeset = Ecto.Changeset.cast(%RedactAllExceptPrimaryKeysSchema{}, %{username: "Hunter", password: "hunter2"}, [:username, :password])
+      changeset =
+        Ecto.Changeset.cast(
+          %RedactAllExceptPrimaryKeysSchema{},
+          %{username: "Hunter", password: "hunter2"},
+          [:username, :password]
+        )
+
       assert inspect(changeset) =~ "id"
       refute inspect(changeset) =~ "hunter2"
       assert inspect(changeset) =~ "**redacted**"
diff --git a/test/ecto/multi_test.exs b/test/ecto/multi_test.exs
index dcbb0f69..ebe8ccce 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/query/builder/dynamic_test.exs b/test/ecto/query/builder/dynamic_test.exs
index 0d3328fc..d11e11be 100644
--- a/test/ecto/query/builder/dynamic_test.exs
+++ b/test/ecto/query/builder/dynamic_test.exs
@@ -27,52 +27,25 @@ defmodule Ecto.Query.Builder.DynamicTest do
       assert params == [{1, {0, :foo}}]
     end
 
-    # TODO: AST is represented as string differently on versions pre 1.13
-    if Version.match?(System.version(), ">= 1.13.0-dev") do
-      test "with dynamic interpolation" do
-        dynamic = dynamic([p], p.bar == ^2)
-        dynamic = dynamic([p], p.foo == ^1 and ^dynamic or p.baz == ^3)
-        assert {expr, _, params, [], _, _} = fully_expand(query(), dynamic)
-        assert Macro.to_string(expr) ==
-               "(&0.foo() == ^0 and &0.bar() == ^1) or &0.baz() == ^2"
-        assert params == [{1, {0, :foo}}, {2, {0, :bar}}, {3, {0, :baz}}]
-      end
-    else
-      test "with dynamic interpolation" do
-        dynamic = dynamic([p], p.bar == ^2)
-        dynamic = dynamic([p], p.foo == ^1 and ^dynamic or p.baz == ^3)
-        assert {expr, _, params, [], _, _} = fully_expand(query(), dynamic)
-        assert Macro.to_string(expr) ==
-               "&0.foo() == ^0 and &0.bar() == ^1 or &0.baz() == ^2"
-        assert params == [{1, {0, :foo}}, {2, {0, :bar}}, {3, {0, :baz}}]
-      end
+    test "with dynamic interpolation" do
+      dynamic = dynamic([p], p.bar == ^2)
+      dynamic = dynamic([p], p.foo == ^1 and ^dynamic or p.baz == ^3)
+      assert {expr, _, params, [], _, _} = fully_expand(query(), dynamic)
+      assert Macro.to_string(expr) ==
+              "(&0.foo() == ^0 and &0.bar() == ^1) or &0.baz() == ^2"
+      assert params == [{1, {0, :foo}}, {2, {0, :bar}}, {3, {0, :baz}}]
     end
 
-    # TODO: AST is represented as string differently on versions pre 1.13
-    if Version.match?(System.version(), ">= 1.13.0-dev") do
-      test "with subquery and dynamic interpolation" do
-        dynamic = dynamic([p], p.sq in subquery(query()))
-        dynamic = dynamic([p], p.bar1 == ^"bar1" or ^dynamic or p.bar3 == ^"bar3")
-        dynamic = dynamic([p], p.foo == ^"foo" and ^dynamic and p.baz == ^"baz")
-        assert {expr, binding, params, [_subquery], _, _} = fully_expand(query(), dynamic)
-        assert Macro.to_string(binding) == "[p]"
-        assert Macro.to_string(expr) ==
-              "&0.foo() == ^0 and (&0.bar1() == ^1 or &0.sq() in {:subquery, 0} or &0.bar3() == ^3) and\n  &0.baz() == ^4"
-        assert params == [{"foo", {0, :foo}}, {"bar1", {0, :bar1}}, {:subquery, 0},
-                          {"bar3", {0, :bar3}}, {"baz", {0, :baz}}]
-      end
-    else
-      test "with subquery and dynamic interpolation" do
-        dynamic = dynamic([p], p.sq in subquery(query()))
-        dynamic = dynamic([p], p.bar1 == ^"bar1" or ^dynamic or p.bar3 == ^"bar3")
-        dynamic = dynamic([p], p.foo == ^"foo" and ^dynamic and p.baz == ^"baz")
-        assert {expr, binding, params, [_subquery], _, _} = fully_expand(query(), dynamic)
-        assert Macro.to_string(binding) == "[p]"
-        assert Macro.to_string(expr) ==
-              "&0.foo() == ^0 and (&0.bar1() == ^1 or &0.sq() in {:subquery, 0} or &0.bar3() == ^3) and &0.baz() == ^4"
-        assert params == [{"foo", {0, :foo}}, {"bar1", {0, :bar1}}, {:subquery, 0},
-                          {"bar3", {0, :bar3}}, {"baz", {0, :baz}}]
-      end
+    test "with subquery and dynamic interpolation" do
+      dynamic = dynamic([p], p.sq in subquery(query()))
+      dynamic = dynamic([p], p.bar1 == ^"bar1" or ^dynamic or p.bar3 == ^"bar3")
+      dynamic = dynamic([p], p.foo == ^"foo" and ^dynamic and p.baz == ^"baz")
+      assert {expr, binding, params, [_subquery], _, _} = fully_expand(query(), dynamic)
+      assert Macro.to_string(binding) == "[p]"
+      assert Macro.to_string(expr) ==
+            "&0.foo() == ^0 and (&0.bar1() == ^1 or &0.sq() in {:subquery, 0} or &0.bar3() == ^3) and\n  &0.baz() == ^4"
+      assert params == [{"foo", {0, :foo}}, {"bar1", {0, :bar1}}, {:subquery, 0},
+                        {"bar3", {0, :bar3}}, {"baz", {0, :baz}}]
     end
 
     test "with nested dynamic interpolation" do
diff --git a/test/ecto/query/builder/join_test.exs b/test/ecto/query/builder/join_test.exs
index fc95f61b..96c6f251 100644
--- a/test/ecto/query/builder/join_test.exs
+++ b/test/ecto/query/builder/join_test.exs
@@ -50,23 +50,12 @@ defmodule Ecto.Query.Builder.JoinTest do
     end
   end
 
-  # TODO: AST is represented as string differently on versions pre 1.13
-  if Version.match?(System.version(), ">= 1.13.0-dev") do
-    test "accepts keywords on :on" do
-      assert %{joins: [join]} =
-              join("posts", :inner, [p], c in "comments", on: [post_id: p.id, public: true])
-      assert Macro.to_string(join.on.expr) ==
-            "&1.post_id() == &0.id() and\n  &1.public() == %Ecto.Query.Tagged{tag: nil, type: {1, :public}, value: true}"
-      assert join.on.params == []
-    end
-  else
-    test "accepts keywords on :on" do
-      assert %{joins: [join]} =
-              join("posts", :inner, [p], c in "comments", on: [post_id: p.id, public: true])
-      assert Macro.to_string(join.on.expr) ==
-            "&1.post_id() == &0.id() and &1.public() == %Ecto.Query.Tagged{tag: nil, type: {1, :public}, value: true}"
-      assert join.on.params == []
-    end
+  test "accepts keywords on :on" do
+    assert %{joins: [join]} =
+            join("posts", :inner, [p], c in "comments", on: [post_id: p.id, public: true])
+    assert Macro.to_string(join.on.expr) ==
+          "&1.post_id() == &0.id() and\n  &1.public() == %Ecto.Query.Tagged{tag: nil, type: {1, :public}, value: true}"
+    assert join.on.params == []
   end
 
   test "accepts queries on interpolation" do
diff --git a/test/ecto/query/builder/select_test.exs b/test/ecto/query/builder/select_test.exs
index eab58b0c..e27adbbc 100644
--- a/test/ecto/query/builder/select_test.exs
+++ b/test/ecto/query/builder/select_test.exs
@@ -296,123 +296,64 @@ defmodule Ecto.Query.Builder.SelectTest do
       assert length(query.select.params) == 1
     end
 
-    # TODO: AST is represented as string differently on versions pre 1.13
-    if Version.match?(System.version(), ">= 1.13.0-dev") do
-      test "supports multiple nested partly dynamic subqueries" do
-        created_by_id = 8
-        ignore_template_id = 9
-
-        subquery0 =
-          from(t in "tasks",
-            where: t.list_id == parent_as(:list).id and t.created_by_id == ^created_by_id,
-            select: max(t.due_on)
-          )
+    test "supports multiple nested partly dynamic subqueries" do
+      created_by_id = 8
+      ignore_template_id = 9
+
+      subquery0 =
+        from(t in "tasks",
+          where: t.list_id == parent_as(:list).id and t.created_by_id == ^created_by_id,
+          select: max(t.due_on)
+        )
 
-        subquery1 =
-          from(t in "templates", where: parent_as(^:list).from_template_id == t.id, select: t.title)
-
-        subquery2 =
-          from(u in "users", where: parent_as(^:list).created_by_id == u.id, select: u.email)
-
-        ref =
-          dynamic(
-            [l],
-            fragment(
-              "CASE WHEN ? THEN ? ELSE ? END",
-              l.from_template_id == ^ignore_template_id,
-              "",
-              subquery(subquery1)
-            )
-          )
+      subquery1 =
+        from(t in "templates", where: parent_as(^:list).from_template_id == t.id, select: t.title)
 
-        query =
-          from(l in "lists",
-            as: :list,
-            select: %{
-              title: l.archived_at,
-              maxdue: subquery(subquery0),
-              user_email: subquery(subquery2)
-            },
-            select_merge: ^%{template_name: ref}
-          )
-
-        assert Macro.to_string(query.select.expr) == """
-              %{\n\
-                title: &0.archived_at(),\n\
-                maxdue: {:subquery, 0},\n\
-                user_email: {:subquery, 1},\n\
-                template_name:\n\
-                  fragment(\n\
-                    {:raw, "CASE WHEN "},\n\
-                    {:expr, &0.from_template_id() == ^2},\n\
-                    {:raw, " THEN "},\n\
-                    {:expr, ""},\n\
-                    {:raw, " ELSE "},\n\
-                    {:expr, {:subquery, 2}},\n\
-                    {:raw, " END"}\n\
-                  )\n\
-              }\
-              """
-
-        assert length(query.select.subqueries) == 3
-        assert query.select.params == [{:subquery, 0}, {:subquery, 1}, {ignore_template_id, {0, :from_template_id}}, {:subquery, 2}]
-      end
-    else
-      test "supports multiple nested partly dynamic subqueries" do
-        created_by_id = 8
-        ignore_template_id = 9
-
-        subquery0 =
-          from(t in "tasks",
-            where: t.list_id == parent_as(:list).id and t.created_by_id == ^created_by_id,
-            select: max(t.due_on)
-          )
+      subquery2 =
+        from(u in "users", where: parent_as(^:list).created_by_id == u.id, select: u.email)
 
-        subquery1 =
-          from(t in "templates", where: parent_as(^:list).from_template_id == t.id, select: t.title)
-
-        subquery2 =
-          from(u in "users", where: parent_as(^:list).created_by_id == u.id, select: u.email)
-
-        ref =
-          dynamic(
-            [l],
-            fragment(
-              "CASE WHEN ? THEN ? ELSE ? END",
-              l.from_template_id == ^ignore_template_id,
-              "",
-              subquery(subquery1)
-            )
+      ref =
+        dynamic(
+          [l],
+          fragment(
+            "CASE WHEN ? THEN ? ELSE ? END",
+            l.from_template_id == ^ignore_template_id,
+            "",
+            subquery(subquery1)
           )
+        )
 
-        query =
-          from(l in "lists",
-            as: :list,
-            select: %{
-              title: l.archived_at,
-              maxdue: subquery(subquery0),
-              user_email: subquery(subquery2)
-            },
-            select_merge: ^%{template_name: ref}
-          )
+      query =
+        from(l in "lists",
+          as: :list,
+          select: %{
+            title: l.archived_at,
+            maxdue: subquery(subquery0),
+            user_email: subquery(subquery2)
+          },
+          select_merge: ^%{template_name: ref}
+        )
 
-        assert Macro.to_string(query.select.expr) == """
-              %{\
-              title: &0.archived_at(), \
-              maxdue: {:subquery, 0}, \
-              user_email: {:subquery, 1}, \
-              template_name:\
-               fragment({:raw, "CASE WHEN "},\
-               {:expr, &0.from_template_id() == ^2},\
-               {:raw, " THEN "}, {:expr, ""},\
-               {:raw, " ELSE "}, {:expr, {:subquery, 2}},\
-               {:raw, " END"})\
-              }\
-              """
-
-        assert length(query.select.subqueries) == 3
-        assert query.select.params == [{:subquery, 0}, {:subquery, 1}, {ignore_template_id, {0, :from_template_id}}, {:subquery, 2}]
-      end
+      assert Macro.to_string(query.select.expr) == """
+            %{\n\
+              title: &0.archived_at(),\n\
+              maxdue: {:subquery, 0},\n\
+              user_email: {:subquery, 1},\n\
+              template_name:\n\
+                fragment(\n\
+                  {:raw, "CASE WHEN "},\n\
+                  {:expr, &0.from_template_id() == ^2},\n\
+                  {:raw, " THEN "},\n\
+                  {:expr, ""},\n\
+                  {:raw, " ELSE "},\n\
+                  {:expr, {:subquery, 2}},\n\
+                  {:raw, " END"}\n\
+                )\n\
+            }\
+            """
+
+      assert length(query.select.subqueries) == 3
+      assert query.select.params == [{:subquery, 0}, {:subquery, 1}, {ignore_template_id, {0, :from_template_id}}, {:subquery, 2}]
     end
 
     test "supports interpolated atom names in selected_as/2" do
diff --git a/test/ecto/query/inspect_test.exs b/test/ecto/query/inspect_test.exs
index 3f3ce545..f85c2c38 100644
--- a/test/ecto/query/inspect_test.exs
+++ b/test/ecto/query/inspect_test.exs
@@ -310,29 +310,15 @@ defmodule Ecto.Query.InspectTest do
              ~s{from p0 in Inspect.Post, lock: "FOOBAR"}
   end
 
-  # TODO: AST is represented as string differently on versions pre 1.13
-  if Version.match?(System.version(), ">= 1.13.0-dev") do
-    test "preload" do
-      assert i(from(x in Post, preload: :comments)) ==
-               ~s"from p0 in Inspect.Post, preload: [:comments]"
+  test "preload" do
+    assert i(from(x in Post, preload: :comments)) ==
+              ~s"from p0 in Inspect.Post, preload: [:comments]"
 
-      assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: y])) ==
-               ~s"from p0 in Inspect.Post, join: c1 in assoc(p0, :comments), preload: [comments: c1]"
+    assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: y])) ==
+              ~s"from p0 in Inspect.Post, join: c1 in assoc(p0, :comments), preload: [comments: c1]"
 
-      assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: {y, post: x}])) ==
-               ~s"from p0 in Inspect.Post, join: c1 in assoc(p0, :comments), preload: [comments: {c1, post: p0}]"
-    end
-  else
-    test "preload" do
-      assert i(from(x in Post, preload: :comments)) ==
-               ~s"from p0 in Inspect.Post, preload: [:comments]"
-
-      assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: y])) ==
-               ~s"from p0 in Inspect.Post, join: c1 in assoc(p0, :comments), preload: [comments: c1]"
-
-      assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: {y, post: x}])) ==
-               ~s"from p0 in Inspect.Post, join: c1 in assoc(p0, :comments), preload: [comments: {c1, [post: p0]}]"
-    end
+    assert i(from(x in Post, join: y in assoc(x, :comments), preload: [comments: {y, post: x}])) ==
+              ~s"from p0 in Inspect.Post, join: c1 in assoc(p0, :comments), preload: [comments: {c1, post: p0}]"
   end
 
   test "fragments" do
@@ -445,27 +431,14 @@ defmodule Ecto.Query.InspectTest do
            ) == string
   end
 
-  # TODO: AST is represented as string differently on versions pre 1.13
-  if Version.match?(System.version(), ">= 1.13.0-dev") do
-    test "container values" do
-      assert i(from(Post, select: <<1, 2, 3>>)) ==
-               "from p0 in Inspect.Post, select: \"\\x01\\x02\\x03\""
-
-      foo = <<1, 2, 3>>
-
-      assert i(from(p in Post, select: {p, ^foo})) ==
-               "from p0 in Inspect.Post, select: {p0, ^\"\\x01\\x02\\x03\"}"
-    end
-  else
-    test "container values" do
-      assert i(from(Post, select: <<1, 2, 3>>)) ==
-               "from p0 in Inspect.Post, select: <<1, 2, 3>>"
+  test "container values" do
+    assert i(from(Post, select: <<1, 2, 3>>)) ==
+              "from p0 in Inspect.Post, select: \"\\x01\\x02\\x03\""
 
-      foo = <<1, 2, 3>>
+    foo = <<1, 2, 3>>
 
-      assert i(from(p in Post, select: {p, ^foo})) ==
-               "from p0 in Inspect.Post, select: {p0, ^<<1, 2, 3>>}"
-    end
+    assert i(from(p in Post, select: {p, ^foo})) ==
+              "from p0 in Inspect.Post, select: {p0, ^\"\\x01\\x02\\x03\"}"
   end
 
   test "select" do
diff --git a/test/ecto/query/planner_test.exs b/test/ecto/query/planner_test.exs
index 5e32c127..790db850 100644
--- a/test/ecto/query/planner_test.exs
+++ b/test/ecto/query/planner_test.exs
@@ -482,67 +482,34 @@ defmodule Ecto.Query.PlannerTest do
              ~r"&1.post_id\(\) == &0.id\(\) and not[\s\(]is_nil\(&1.text\(\)\)\)?"
   end
 
-  # TODO: AST is represented as string differently on versions pre 1.13
-  if Version.match?(System.version(), ">= 1.13.0-dev") do
-    test "plan: nested joins associations with custom queries" do
-      query =
-        from(p in Post,
-          join: c1 in assoc(p, :special_comments),
-          join: p2 in assoc(c1, :post),
-          join: cp in assoc(c1, :comment_posts),
-          join: c2 in assoc(cp, :special_comment),
-          join: c3 in assoc(cp, :special_long_comment)
-        )
-        |> plan
-        |> elem(0)
-
-      assert [join1, join2, join3, join4, join5] = query.joins
-
-      assert {{"posts", _, _}, {"comments", _, _}, {"posts", _, _}, {"comment_posts", _, _},
-              {"comments", _, _}, {"comments", _, _}} = query.sources
-
-      assert Macro.to_string(join1.on.expr) =~
-               ~r"&1.post_id\(\) == &0.id\(\) and not[\s\(]is_nil\(&1.text\(\)\)\)?"
-
-      assert Macro.to_string(join2.on.expr) == "&2.id() == &1.post_id()"
-      assert Macro.to_string(join3.on.expr) == "&3.comment_id() == &1.id()"
-
-      assert Macro.to_string(join4.on.expr) ==
-               "&4.id() == &3.special_comment_id() and is_nil(&4.text())"
-
-      assert Macro.to_string(join5.on.expr) ==
-               "&5.id() == &3.special_long_comment_id() and\n  fragment({:raw, \"LEN(\"}, {:expr, &5.text()}, {:raw, \") > 100\"})"
-    end
-  else
-    test "plan: nested joins associations with custom queries" do
-      query =
-        from(p in Post,
-          join: c1 in assoc(p, :special_comments),
-          join: p2 in assoc(c1, :post),
-          join: cp in assoc(c1, :comment_posts),
-          join: c2 in assoc(cp, :special_comment),
-          join: c3 in assoc(cp, :special_long_comment)
-        )
-        |> plan
-        |> elem(0)
+  test "plan: nested joins associations with custom queries" do
+    query =
+      from(p in Post,
+        join: c1 in assoc(p, :special_comments),
+        join: p2 in assoc(c1, :post),
+        join: cp in assoc(c1, :comment_posts),
+        join: c2 in assoc(cp, :special_comment),
+        join: c3 in assoc(cp, :special_long_comment)
+      )
+      |> plan
+      |> elem(0)
 
-      assert [join1, join2, join3, join4, join5] = query.joins
+    assert [join1, join2, join3, join4, join5] = query.joins
 
-      assert {{"posts", _, _}, {"comments", _, _}, {"posts", _, _}, {"comment_posts", _, _},
-              {"comments", _, _}, {"comments", _, _}} = query.sources
+    assert {{"posts", _, _}, {"comments", _, _}, {"posts", _, _}, {"comment_posts", _, _},
+            {"comments", _, _}, {"comments", _, _}} = query.sources
 
-      assert Macro.to_string(join1.on.expr) =~
-               ~r"&1.post_id\(\) == &0.id\(\) and not[\s\(]is_nil\(&1.text\(\)\)\)?"
+    assert Macro.to_string(join1.on.expr) =~
+              ~r"&1.post_id\(\) == &0.id\(\) and not[\s\(]is_nil\(&1.text\(\)\)\)?"
 
-      assert Macro.to_string(join2.on.expr) == "&2.id() == &1.post_id()"
-      assert Macro.to_string(join3.on.expr) == "&3.comment_id() == &1.id()"
+    assert Macro.to_string(join2.on.expr) == "&2.id() == &1.post_id()"
+    assert Macro.to_string(join3.on.expr) == "&3.comment_id() == &1.id()"
 
-      assert Macro.to_string(join4.on.expr) ==
-               "&4.id() == &3.special_comment_id() and is_nil(&4.text())"
+    assert Macro.to_string(join4.on.expr) ==
+              "&4.id() == &3.special_comment_id() and is_nil(&4.text())"
 
-      assert Macro.to_string(join5.on.expr) ==
-               "&5.id() == &3.special_long_comment_id() and fragment({:raw, \"LEN(\"}, {:expr, &5.text()}, {:raw, \") > 100\"})"
-    end
+    assert Macro.to_string(join5.on.expr) ==
+              "&5.id() == &3.special_long_comment_id() and\n  fragment({:raw, \"LEN(\"}, {:expr, &5.text()}, {:raw, \") > 100\"})"
   end
 
   test "plan: raises on invalid binding index in join" do
diff --git a/test/ecto/repo/belongs_to_test.exs b/test/ecto/repo/belongs_to_test.exs
index e38352f6..c83cec8d 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 d3702074..932f829e 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 0bf30f1f..8145c8a2 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 56fd4be4..efc4322a 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 01754db6..4df27528 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.()}