Basics
This check is disabled by default.
Learn how to enable it via .credo.exs.
This check has a base priority of high and works with any version of Elixir.
Explanation
Test code MUST NOT issue a raw search_path mutation as SQL. Route it
through Mailglass.TestSupport.SandboxOwnership.with_search_path!/3,
which pins ONE pooled connection for the whole block, restores the prior
value on that same connection, and then RE-READS it to verify the restore
actually landed.
The failure mode, by name
SET search_path TO public without LOCAL is a SESSION-level write: it
persists on the physical Postgres connection for that connection's entire
lifetime. Under Sandbox :auto mode every Repo.query checks a
connection out of the 10-slot pool and returns it, so the poisoned
connection goes straight back into the pool. config/test.exs +
test/test_helper.exs give pool connections a startup search_path of
"<schema>, public", and the whole rest of the suite relies on it to
resolve unqualified relation names.
The result is pool poisoning: some later, wholly unrelated test draws
the poisoned connection and raises
(Postgrex.Error) ERROR 42P01 (undefined_table) relation "mailglass_deliveries" does not exist — a failure attributed to an
innocent module hundreds of tests away from the one that broke it. This is
not hypothetical: it is the confirmed root cause of the D-31 Class A
cascade on the MAILGLASS_SCHEMA=mailglass axis (seven victim modules,
two full misdiagnosis cycles). A throwaway probe confirmed it directly —
after ONE unscoped SET search_path TO public, all 40 subsequent pool
checkouts observed "public".
A trailing RESET search_path from on_exit does NOT undo it: that is a
SEPARATE pool checkout that lands on whichever connection it lands on,
which need not be the poisoned one.
What is banned, and why each form
Every form below is banned in SQL-statement position under test/:
SET search_path ...— session-scoped. The defect above.SET SESSION search_path ...— an explicit spelling of the same session-scoped write.SET LOCAL search_path ...— transaction-scoped, and therefore looks safe. It is not safe inside a migration:SET LOCALpersists for the remainder of the transaction, andEcto.Migratorinserts itsschema_migrationsversion row INSIDE that same transaction, AFTER the migration body. The pin redirects Ecto's own bookkeeping INSERT to asearch_pathholding noschema_migrationstable, raising42P01 ... relation "schema_migrations" does not exist. Observed live:MAILGLASS_SCHEMA=mailglass mix test test/mailglass/shipped_migration_divergence_test.exsfailed 4 tests / 4 failures on exactly this.RESET search_path— cannot poison (it restores the startup-packet value), but from:automode it is its own checkout on an arbitrary pooled connection, so it heals nothing observable while reading as a fix. A guard that reads as a guarantee without being one is the exact credibility failure this milestone exists to repair.set_config('search_path', ...)— the function-call spelling of a session-levelSET, banned for the same reason as the first form.
What is permitted
SHOW search_path— read-only.search_pathin the Postgrex:parametersconnection option (test/test_helper.exs) — that is the connection's STARTUP value, set once at pool-connect time on every connection, which is precisely the invariant the bans above protect.SET search_path = ''as aCREATE FUNCTIONattribute clause — a different construct entirely (it hardens a function body against search-path injection, and is not a session write). It is never in statement-initial position inside aCREATE FUNCTIONstatement, so it does not match.- The same literals as ASSERTION MATCH TARGETS (
body =~ "SET search_path = ''",String.contains?/2). A match target is compared, never executed, so it cannot poison anything. See:match_target_functions. - Anything inside
Mailglass.TestSupport.SandboxOwnership— the one sanctioned seam, which owns the same-connection, verified restore — its own mechanism test, and this check's own fixture corpus. Those three modules are the whole allowlist; see.credo.exsfor the per-entry justification.
Two-layer guard
This is the PREVENTION half. The detection half is
Mailglass.TestSupport.SandboxOwnership.with_search_path!/3's post-restore
re-read, which raises SearchPathError naming the offending module. The
D-31 Class A cascade recurred precisely because detection shipped without
prevention.
Check-Specific Parameters
Use the following parameters to configure this check:
:allowed_modules
Modules explicitly allowed to issue raw search_path mutations (the sanctioned seam and its own test).
This parameter defaults to [Mailglass.TestSupport.SandboxOwnership, Mailglass.TestSupport.SandboxOwnershipTest, Mailglass.Credo.NoRawSearchPathMutationTest].
:included_path_prefixes
Only files in these path prefixes are linted.
This parameter defaults to ["test/", "mailglass_inbound/test/"].
:match_target_functions
Function/operator names whose arguments are assertion MATCH TARGETS rather than executed SQL.
This parameter defaults to [:=~, :contains?].
General Parameters
Like with all checks, general params can be applied.
Parameters can be configured via the .credo.exs config file.