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

Structural shape comparison — spec §6.2's "divergence detection is
structural, not deparse-text". Pure: takes an *expected* shape (from
`PhoenixKit.Migrations.ExpectedSchema.Object.shape_at/2`, already selected
for the right comment/revision by `PhoenixKit.Migrations.Repair.Scope`) and
an *observed* shape (fetched from the target server by
`PhoenixKit.Migrations.Repair.Probe`), and reports whether — and how — they
differ.

## Which fields are compared, and why

Per class, only the fields that decompose the underlying catalog fact
structurally are compared; raw `pg_get_*def`/`definition` text is used only
where nothing else exists (spec: "raw-text equality only where no
structural decomposition exists"):

  * `:column` — `type`, `not_null`, `default` (all three already
    `pg_get_expr`/`format_type`-normalized *by the querying server* on
    both sides — the expected shape by the generation server at manifest
    build time, the observed shape by the target server at verify time;
    §6.3's version preflight bounds how far apart those two are allowed to
    be). `pos` is deliberately excluded — column ordinal position is
    accidental (`ADD COLUMN` always appends), never a divergence worth
    reporting. `not_null` is *conditionally* excluded too: when the
    expected shape is `not_null: true` with `default: nil`,
    `PhoenixKit.Migrations.Repair.Executor`'s own additive-only create
    deliberately omits `NOT NULL` (adding one with no default to a
    possibly-populated table fails outright — spec D7/§6.2), so a column
    repair itself just recreated from nothing is *permanently* nullable by
    the engine's own design. Comparing `not_null` there would manufacture
    an error-severity finding no `mix phoenix_kit.repair` run can ever
    clear — including blocking `--adopt`'s clean gate on a floor slice
    that otherwise legitimately converged — out of the one outcome the
    additive-only contract already accepts as the best it can safely do.
    Any *other* expected shape (nullable, or not_null with a real default)
    still compares `not_null` normally.
  * `:sequence` — every field (`data_type`/`start`/`increment`/`min`/
    `max`/`cache`/`cycle`) is schema-declaration metadata, not
    runtime state (`seqstart` is the *declared* start, not `last_value` —
    unaffected by how many times `nextval()` has run).
  * `:function` — `returns`, `language`, `body_md5` (an md5 of `prosrc`
    alone — a meaningful fingerprint, not raw deparse text). `definition`
    (full `pg_get_functiondef`) is excluded — it is exactly the kind of
    text spec §6.2 warns rendering can differ on across PG majors.
  * `:index` — `unique`, `method`, `keys`, `opclasses` structurally;
    `predicate`/`definition` only as a **secondary**, lower-confidence
    signal folded into the same mismatch (still `:error` severity per
    spec's explicit list — this module does not invent a softer tier for
    it, it only labels *which* fields disagreed so an operator can judge a
    cross-major artifact for themselves). `NULLS NOT DISTINCT` (PG15+) has
    no structured field of its own; no version in the chain declares it, and
    until one does it is covered by `definition`, which `pg_get_indexdef`
    renders it into. A version that starts using it should promote it to a
    structured field rather than rely on that text comparison.
  * `:constraint` — dispatches on `type` (`pg_constraint.contype`):
    `"f"` (foreign key) compares `columns`/`foreign_table`/
    `foreign_columns`/`on_delete`/`on_update` — all decomposed,
    never `definition`; `"p"`/`"u"` (primary key/unique) compare
    `columns`; `"c"` (check) and `"x"` (exclusion) have no further
    decomposition available in the catalog snapshot and fall back to
    `definition` text (spec's explicit exception for this exact case).
  * `:table`/`:extension` — no shape to diff (existence is the whole
    story; `Object.shape/0` is `%{}` for both) — `compare/3` always
    returns `:match`.
  * `:seed` — **presence-only, deliberately** — `compare/3` always returns
    `:match`. Spec §6.2 lists `:missing`+creatable, wrong column shapes,
    divergent indexes/constraints, `:create_failed`, and failed data
    invariants as the reportable categories; a seed row's *values* are
    never one of them, and for good reason — spec §5.1/§6.2's seed policy
    is "strict `DO NOTHING` (never clobber operator-tuned values)", so an
    admin who edited a seeded setting after install has produced exactly
    the state repair must leave alone, not something to flag as drift.
    Only presence is ever checked (`PhoenixKit.Migrations.Repair.Probe`
    executes the object's `check` — a `SELECT EXISTS (...)` on the
    business key — directly; there is no "observed shape" fetch for
    seeds at all, so this clause never actually receives divergent
    `values` to compare in practice).

`compare/3` never looks at `:missing` (`observed == nil`) — that is a
presence question the caller (`PhoenixKit.Migrations.Repair`) answers
before ever calling here; `compare/3` assumes both shapes are non-nil.

# `result`

```elixir
@type result() :: :match | {:mismatch, [String.t()]}
```

`:match`, or a mismatch with the field names that disagreed.

# `compare`

```elixir
@spec compare(
  PhoenixKit.Migrations.ExpectedSchema.Object.class(),
  expected :: map(),
  observed :: map()
) ::
  result()
```

Compares `expected` against `observed` for `class`. Field lists in
`{:mismatch, reasons}` are human-readable, one entry per disagreeing
aspect — never a raw diff dump — so a report can print them directly.

# `deparse_text_marker`

```elixir
@spec deparse_text_marker() :: String.t()
```

The marker prefix stamped on rendering-derived (not structural) reasons.

# `deparse_text_only?`

```elixir
@spec deparse_text_only?({:mismatch, [String.t()]} | :match) :: boolean()
```

True when EVERY reason in a mismatch rests on deparse rendering rather than
a structural field — the case a cross-major server can produce with no real
drift behind it.

---

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