# `Mailglass.Credo.NoRawSearchPathMutation`
[🔗](https://github.com/szTheory/mailglass/blob/v2.7.0/credo_checks/no_raw_search_path_mutation.ex#L1)

## Basics

> #### This check is disabled by default. {: .neutral}
>
> [Learn how to enable it](`e:credo:config_file.html#checks`) 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 LOCAL` persists
    for the remainder of the transaction, and `Ecto.Migrator` inserts its
    `schema_migrations` version row INSIDE that same transaction, AFTER
    the migration body. The pin redirects Ecto's own bookkeeping INSERT to
    a `search_path` holding no `schema_migrations` table, raising
    `42P01 ... relation "schema_migrations" does not exist`. Observed
    live: `MAILGLASS_SCHEMA=mailglass mix test
    test/mailglass/shipped_migration_divergence_test.exs` failed 4 tests /
    4 failures on exactly this.
  * `RESET search_path` — cannot poison (it restores the startup-packet
    value), but from `:auto` mode 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-level `SET`, banned for the same reason as the first form.

## What is permitted

  * `SHOW search_path` — read-only.
  * `search_path` in the Postgrex `:parameters` connection 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 a `CREATE FUNCTION` attribute 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 a `CREATE FUNCTION` statement, 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.exs` for 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](`e:credo:check_params.html`) can be applied.

Parameters can be configured via the [`.credo.exs` config file](`e:credo:config_file.html`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
