Skip to content

Instantly share code, notes, and snippets.

@brettinternet
Created September 4, 2025 19:48
Show Gist options
  • Select an option

  • Save brettinternet/e24f3963159c8cea38b3d3fd8509e7ac to your computer and use it in GitHub Desktop.

Select an option

Save brettinternet/e24f3963159c8cea38b3d3fd8509e7ac to your computer and use it in GitHub Desktop.
test multi repo transactions in sandbox
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