# `PhoenixKit.Migrations.Repair.Environment`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/phoenix_kit/migrations/repair/environment.ex#L1)

Spec §6.3's environment rules: pooled-connection detection and the
advisory lock. `classify_config/1` is pure (the config-based half of
`mix phoenix_kit.doctor`'s existing PgBouncer heuristic,
`doctor.ex:174-195`, reused rather than re-derived); `pooled?/1` and the
lock functions execute real queries and have no unit test.

## Detection is two-layered, on purpose

Config alone (a non-5432 port, a hostname containing "pgbouncer") is a
fast, always-available *hint* — it is what `mix phoenix_kit.doctor`
already prints a warning from. It is not proof: a direct Postgres
connection can legitimately run on a non-standard port, and a
transaction-pooling proxy can sit behind a hostname that says nothing
about it. The authoritative signal is behavioral: `pg_backend_pid()`
sampled twice over the SAME checked-out connection, as two separate
(non-transactional) statements. A direct connection — or a
session-pooling proxy, which behaves like one for this purpose — answers
with the same pid both times, because both statements ride the one
physical server backend for as long as the connection is checked out.
Transaction-pooling PgBouncer can hand each statement a different backend
connection, because nothing binds them together outside an explicit
transaction. `pooled?/1` therefore checks out **one** connection
(`c:Ecto.Repo.checkout/2`) before sampling — sampling via two plain
`repo.query!/3` calls without checking out first would let *Ecto's own*
pool (nothing to do with PgBouncer) hand the two queries to different
workers and manufacture a false positive on a perfectly direct database.

Config classification and pid-sampling are combined with OR: either one
saying "pooled" is enough to require `--unsafe-pooled` — false positives
(an operator has to pass one extra flag) are the safe failure direction;
false negatives (skipping `VALIDATE`/the advisory lock against a pooled
connection that fooled both checks) are not.

# `config_verdict`

```elixir
@type config_verdict() :: :maybe_pooled | :direct
```

The config-only heuristic's verdict — a hint, never authoritative on its own.

# `classify_config`

```elixir
@spec classify_config(keyword()) :: config_verdict()
```

The config-based half of the detection (`doctor.ex`'s existing heuristic,
reused verbatim): a non-`5432` port, or a hostname containing
`"pgbouncer"`, is `:maybe_pooled`.

    iex> Environment.classify_config(port: 5432, hostname: "db.internal")
    :direct
    iex> Environment.classify_config(port: 6432, hostname: "db.internal")
    :maybe_pooled
    iex> Environment.classify_config(port: 5432, hostname: "pgbouncer.internal")
    :maybe_pooled
    iex> Environment.classify_config(url: "ecto://user:pass@pgbouncer:6432/db")
    :maybe_pooled

# `lock_key`

```elixir
@spec lock_key() :: non_neg_integer()
```

The advisory lock key repair uses — exposed for tests/diagnostics, never meant to be passed elsewhere.

# `pooled?`

```elixir
@spec pooled?(Ecto.Repo.t()) :: boolean()
```

The authoritative, behavioral check — see moduledoc. Checks out one
connection and compares `pg_backend_pid()` across two separate statements
on it. Fails toward `true` (pooled, the safer assumption) on any error —
an environment we cannot prove is direct is treated as pooled, never the
other way around.

# `pooled?`

```elixir
@spec pooled?(
  Ecto.Repo.t(),
  keyword()
) :: boolean()
```

Combines `classify_config/1` and `pooled?/1` with OR (see moduledoc).
`config` is the repo's `Application.get_env(app, repo, [])` keyword list.

# `with_lock`

```elixir
@spec with_lock(Ecto.Repo.t(), (-&gt; result)) :: result when result: term()
```

Acquires `lock_key/0` (session-level `pg_advisory_lock/1`, blocking) on
`repo`'s checked-out connection, runs `fun`, then releases it — even if
`fun` raises. Must run on a **direct** connection (§6.3): PgBouncer in
transaction-pooling mode can hand the lock and unlock calls to different
backend connections, silently defeating it. `--unsafe-pooled` skips
calling this at all (`PhoenixKit.Migrations.Repair` never calls
`with_lock/2` when the environment was classified pooled and the flag was
given).

---

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