# `MailglassInbound.Internal.Prune`
[🔗](https://github.com/szTheory/mailglass/blob/v2.3.0/lib/mailglass_inbound/internal/prune.ex#L1)

Oban-independent batched retention sweep for inbound tables. This is the
workhorse: `mix mailglass.inbound.prune` and the
optional `MailglassInbound.Prune.Worker` cron both call `prune/0`.

Mirrors `Mailglass.Webhook.Pruner`'s STRUCTURE (`:infinity` disables a class,
per-table telemetry) but UPGRADES the unbounded `delete_all` to a batched idiom
each table deletes `LIMIT 1000` rows at a time
(`FOR UPDATE SKIP LOCKED`), looping until a batch deletes `< 1000`. The whole
sweep is serialized by a session `pg_try_advisory_lock` — a concurrent second
run returns `{:ok, :locked_out}` and deletes nothing.

## Schema qualification

The prune sweep holds a direct reference to the raw host repo (not the
`MailglassInbound.Repo` facade) because it needs `checkout/1` to pin one
connection for the advisory lock session. The facade cannot rewrite this path,
so any mailglass-table SQL issued from this module MUST carry an explicit
`prefix: MailglassInbound.Config.schema()` inline for correctness.
The single `repo.delete_all(...)` in `delete_batched/3` carries
this prefix. The two advisory-lock `repo.query!` calls (`pg_try_advisory_lock` /
`pg_advisory_unlock`) and `repo.checkout/1` are intentionally UNprefixed — they
are session-scoped, schema-agnostic SQL that touch no mailglass table (mirrors the
core `Mailglass.Repo.query!/2` SET LOCAL exemption). Any future raw mailglass-table
SQL added to this module's direct-repo path MUST follow the same inline-prefix rule.

## Window split — three physical tables, four windows

  * `mailglass_inbound_replay_runs` WHERE `source = :replay` AND age > replay_runs_days (30d)
  * `mailglass_inbound_replay_runs` WHERE `source = :fresh`  AND age > execution_runs_days (90d)
  * `mailglass_inbound_evidence` WHERE age > evidence_days (90d)
  * `mailglass_inbound_records` WHERE age > records_days (90d)

Deletes run child-first: replay_runs (both source filters) -> evidence
-> records. FKs are `on_delete: :nothing`, so a mis-ordered delete fails loudly
on the FK (the designed safety net — do NOT switch to CASCADE).
Prune operations must remain tenant-scoped and operator-confirmed before destructive actions.

Because the FKs are `:nothing`, a parent window can never be shorter than a child
that references it, or the child-first sweep would leave a surviving child whose
parent the next delete tries to remove — tripping a `foreign_key_violation`
`MailglassInbound.Config.retention/0` enforces this by clamping
`evidence_days >= max(execution_runs_days, replay_runs_days)` and
`records_days >= evidence_days`, so the default windows (evidence 90d, not the
former 30d) never invert against `:fresh` runs aged 30-90 days.

`source` is filtered via `ExecutionRun` (which maps the `:source` column);
NEVER `ReplayRun` (no `:source` field — Pitfall 4).

# `lock_key`

```elixir
@spec lock_key() :: 6_318_741_290_553_217_001
```

The session advisory-lock key the prune sweep acquires. Exposed so tests can
acquire it from a separate connection to exercise the single-run guard.

# `prune`

```elixir
@spec prune(keyword()) :: {:ok, map()} | {:ok, :locked_out}
```

Run the retention sweep. Returns `{:ok, counts}` where `counts` carries the
per-table deletion counts (+ per-table batch iteration counts) and `:status`,
or `{:ok, :locked_out}` when another sweep holds the advisory lock.

---

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