@@ -50,12 +50,46 @@ defmodule Trinity.DataCase do
5050 # test writes inside the test's sandbox and the rows go with it.
5151 rpid = Ecto.Adapters.SQL.Sandbox . start_owner! ( Trinity.Repo.Receipts , opts )
5252
53+ # SCR-376 and SCR-377: make each sandboxed transaction a writer before it reads anything.
54+ #
55+ # `Ecto.Adapters.SQL.Sandbox` begins every test's transaction with an explicit
56+ # `mode: :transaction`, which `Exqlite.Connection` resolves ahead of the configured
57+ # `default_transaction_mode: :immediate`, and which is a plain DEFERRED `BEGIN`. So slice 005's
58+ # fix does not reach the suite: a test that reads and then writes is upgrading, and SQLite
59+ # answers `SQLITE_BUSY` on an upgrade **without consulting the busy handler**, whatever
60+ # `busy_timeout` says. Five failures across three tables came from exactly that.
61+ #
62+ # A write that is the transaction's *first* statement is not an upgrade, so the handler is
63+ # consulted and the writer waits instead of failing. That is measured, not assumed:
64+ # `test/trinity/repo/sqlite_transaction_mode_test.exs` drives two connections through this
65+ # exact sequence and asserts both the success and that it genuinely contended.
66+ #
67+ # `WHERE 1 = 0` touches no row. The table is the repo's **own** migration table, read from its
68+ # configuration rather than assumed: `Trinity.Repo.Receipts` sets
69+ # `migration_source: "receipts_schema_migrations"` (config/config.exs), and hard-coding
70+ # `schema_migrations` here failed every test using this template, not only the ones that touch
71+ # receipts. The statement is here for the lock it takes, not for the rows it does not change.
72+ #
73+ # Cost: measured as negligible, because there is nothing to serialise. Every file using this
74+ # case template is `async: false` (the only `async: true` occurrence of it is this file's own
75+ # documentation), so sandboxed tests already run one at a time.
76+ Enum . each ( [ Trinity.Repo , Trinity.Repo.Receipts ] , & take_write_lock / 1 )
77+
5378 on_exit ( fn ->
5479 Ecto.Adapters.SQL.Sandbox . stop_owner ( rpid )
5580 Ecto.Adapters.SQL.Sandbox . stop_owner ( pid )
5681 end )
5782 end
5883
84+ # SQLite only. Postgres has no SQLITE_BUSY and no upgrade path to guard, so taking a lock there
85+ # would be a cost with no reason.
86+ defp take_write_lock ( repo ) do
87+ if repo . __adapter__ ( ) == Ecto.Adapters.SQLite3 do
88+ source = repo . config ( ) [ :migration_source ] || "schema_migrations"
89+ Ecto.Adapters.SQL . query! ( repo , "DELETE FROM #{ source } WHERE 1 = 0" , [ ] )
90+ end
91+ end
92+
5993 @ doc """
6094 A helper that transforms changeset errors into a map of messages.
6195
0 commit comments