# `PhoenixKit.Migrations.Repair.Environment`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.28.1/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 hint, which
`mix phoenix_kit.doctor` reports beside the probe); `probe/1`, `pooled?/1`
and the lock functions execute real queries.

## 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.

# `probe_result`

```elixir
@type probe_result() ::
  :transaction_pooled | :not_detected | {:inconclusive, String.t()}
```

What the behavioral probe observed. `:not_detected` is NOT "direct": a
session-pooling proxy keeps one backend for the checkout too, so the same
pid twice rules out transaction pooling and nothing more.

# `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
    iex> Environment.classify_config(url: "ecto://user:pass@db.internal/db")
    :direct

# `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.

# `probe`

```elixir
@spec probe(Ecto.Repo.t()) :: probe_result()
```

The behavioral check, reporting what it could establish rather than a
boolean — `pooled?/1` folds an inconclusive probe into "pooled" (the safe
default before a repair), which is the wrong thing to *print*: a diagnostic
must say "could not tell" when it could not tell.

# `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*
