Created
September 4, 2025 19:48
-
-
Save brettinternet/e24f3963159c8cea38b3d3fd8509e7ac to your computer and use it in GitHub Desktop.
test multi repo transactions in sandbox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| defmodule TestRepo do | |
| describe "Multi.run transaction isolation" do | |
| defmodule TestItem do | |
| @moduledoc false | |
| use MyApp.Schema | |
| schema "test_transaction_items" do | |
| field :name, :string | |
| field :value, :string | |
| timestamps() | |
| end | |
| def changeset(item, attrs) do | |
| item | |
| |> Ecto.Changeset.cast(attrs, [:name, :value]) | |
| |> Ecto.Changeset.validate_required([:name]) | |
| end | |
| end | |
| setup do | |
| Sandbox.checkout(Repo) | |
| Repo.query!(""" | |
| CREATE TEMPORARY TABLE test_transaction_items ( | |
| id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |
| name text NOT NULL, | |
| value text, | |
| inserted_at timestamp NOT NULL DEFAULT NOW(), | |
| updated_at timestamp NOT NULL DEFAULT NOW() | |
| ) | |
| """) | |
| :ok | |
| end | |
| test "SQL logging: MyApp.Repo vs repo argument comparison" do | |
| item_name = "test_item_#{System.unique_integer()}" | |
| # Capture all SQL queries | |
| queries = [] | |
| Logger.configure(level: :debug) | |
| :telemetry.attach("sql-compare", [:my_app, :repo, :query], fn name, measurements, metadata, queries -> | |
| query_info = %{ | |
| query: metadata[:query], | |
| params: metadata[:params], | |
| time: measurements[:total_time], | |
| event: name | |
| } | |
| Agent.update(queries, fn list -> [query_info | list] end) | |
| end, queries) | |
| # Start an Agent to collect queries | |
| {:ok, query_agent} = Agent.start_link(fn -> [] end) | |
| :telemetry.detach("sql-compare") | |
| :telemetry.attach("sql-compare", [:my_app, :repo, :query], fn name, measurements, metadata, agent -> | |
| query_info = %{ | |
| query: String.trim(metadata[:query]), | |
| params: metadata[:params], | |
| time: measurements[:total_time], | |
| source: if(String.contains?(metadata[:query], "BEGIN"), do: "BEGIN", | |
| else: if(String.contains?(metadata[:query], "COMMIT"), do: "COMMIT", | |
| else: if(String.contains?(metadata[:query], "INSERT"), do: "INSERT", | |
| else: if(String.contains?(metadata[:query], "SELECT"), do: "SELECT", else: "OTHER")))) | |
| } | |
| Agent.update(agent, fn list -> [query_info | list] end) | |
| end, query_agent) | |
| IO.puts("\n" <> String.duplicate("=", 80)) | |
| IO.puts("π TESTING: MyApp.Repo vs repo argument - SQL Transaction Analysis") | |
| IO.puts(String.duplicate("=", 80)) | |
| # Test 1: MyApp.Repo pattern | |
| IO.puts("\nπ₯ Pattern 1: Using MyApp.Repo directly") | |
| Agent.update(query_agent, fn _ -> [] end) # Clear queries | |
| multi_result_my_app = | |
| Ecto.Multi.new() | |
| |> Ecto.Multi.run(:create_item, fn _repo, _changes -> | |
| IO.puts(" π Creating item with MyApp.Repo...") | |
| %TestItem{} | |
| |> TestItem.changeset(%{name: item_name <> "_my_app", value: "my_app_repo"}) | |
| |> MyApp.Repo.insert() | |
| end) | |
| |> Ecto.Multi.run(:verify, fn repo, %{create_item: item} -> | |
| IO.puts(" π Verifying with transaction repo...") | |
| case repo.get(TestItem, item.id) do | |
| nil -> {:error, "not found"} | |
| found -> {:ok, found} | |
| end | |
| end) | |
| |> Repo.transaction() | |
| my_app_queries = Agent.get(query_agent, & &1) |> Enum.reverse() | |
| IO.puts(" π MyApp.Repo pattern queries:") | |
| Enum.with_index(my_app_queries, 1) |> Enum.each(fn {query, idx} -> | |
| IO.puts(" #{idx}. [#{query.source}] #{String.slice(query.query, 0, 100)}#{if String.length(query.query) > 100, do: "...", else: ""}") | |
| end) | |
| # Test 2: Transaction repo pattern | |
| IO.puts("\nβ Pattern 2: Using repo argument") | |
| Agent.update(query_agent, fn _ -> [] end) # Clear queries | |
| multi_result_transaction = | |
| Ecto.Multi.new() | |
| |> Ecto.Multi.run(:create_item, fn repo, _changes -> | |
| IO.puts(" π Creating item with transaction repo...") | |
| %TestItem{} | |
| |> TestItem.changeset(%{name: item_name <> "_transaction", value: "transaction_repo"}) | |
| |> repo.insert() | |
| end) | |
| |> Ecto.Multi.run(:verify, fn repo, %{create_item: item} -> | |
| IO.puts(" π Verifying with transaction repo...") | |
| case repo.get(TestItem, item.id) do | |
| nil -> {:error, "not found"} | |
| found -> {:ok, found} | |
| end | |
| end) | |
| |> Repo.transaction() | |
| transaction_queries = Agent.get(query_agent, & &1) |> Enum.reverse() | |
| IO.puts(" π Transaction repo pattern queries:") | |
| Enum.with_index(transaction_queries, 1) |> Enum.each(fn {query, idx} -> | |
| IO.puts(" #{idx}. [#{query.source}] #{String.slice(query.query, 0, 100)}#{if String.length(query.query) > 100, do: "...", else: ""}") | |
| end) | |
| # Analysis | |
| IO.puts("\nπ ANALYSIS:") | |
| IO.puts(" MyApp.Repo queries: #{length(my_app_queries)}") | |
| IO.puts(" Transaction repo queries: #{length(transaction_queries)}") | |
| my_app_begins = Enum.count(my_app_queries, &(&1.source == "BEGIN")) | |
| transaction_begins = Enum.count(transaction_queries, &(&1.source == "BEGIN")) | |
| IO.puts(" MyApp.Repo BEGIN statements: #{my_app_begins}") | |
| IO.puts(" Transaction repo BEGIN statements: #{transaction_begins}") | |
| if my_app_begins == transaction_begins do | |
| IO.puts(" π€ RESULT: Both patterns use the same number of transactions!") | |
| IO.puts(" This suggests Ecto.Sandbox makes them behave identically in tests.") | |
| else | |
| IO.puts(" π― RESULT: Different transaction behavior detected!") | |
| end | |
| :telemetry.detach("sql-compare") | |
| Agent.stop(query_agent) | |
| # Both should succeed in test environment | |
| assert {:ok, %{verify: _}} = multi_result_my_app | |
| assert {:ok, %{verify: _}} = multi_result_transaction | |
| IO.puts(String.duplicate("=", 80)) | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment