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 alreadypg_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).posis deliberately excluded — column ordinal position is accidental (ADD COLUMNalways appends), never a divergence worth reporting.not_nullis conditionally excluded too: when the expected shape isnot_null: truewithdefault: nil,PhoenixKit.Migrations.Repair.Executor's own additive-only create deliberately omitsNOT 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. Comparingnot_nullthere would manufacture an error-severity finding nomix phoenix_kit.repairrun 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 comparesnot_nullnormally.:sequence— every field (data_type/start/increment/min/max/cache/cycle) is schema-declaration metadata, not runtime state (seqstartis the declared start, notlast_value— unaffected by how many timesnextval()has run).:function—returns,language,body_md5(an md5 ofprosrcalone — a meaningful fingerprint, not raw deparse text).definition(fullpg_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,opclassesstructurally;predicate/definitiononly as a secondary, lower-confidence signal folded into the same mismatch (still:errorseverity 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 bydefinition, whichpg_get_indexdefrenders it into. A version that starts using it should promote it to a structured field rather than rely on that text comparison.:constraint— dispatches ontype(pg_constraint.contype):"f"(foreign key) comparescolumns/foreign_table/foreign_columns/on_delete/on_update— all decomposed, neverdefinition;"p"/"u"(primary key/unique) comparecolumns;"c"(check) and"x"(exclusion) have no further decomposition available in the catalog snapshot and fall back todefinitiontext (spec's explicit exception for this exact case).:table/:extension— no shape to diff (existence is the whole story;Object.shape/0is%{}for both) —compare/3always returns:match.:seed— presence-only, deliberately —compare/3always 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 "strictDO 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.Probeexecutes the object'scheck— aSELECT EXISTS (...)on the business key — directly; there is no "observed shape" fetch for seeds at all, so this clause never actually receives divergentvaluesto 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.
Summary
Functions
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.
The marker prefix stamped on rendering-derived (not structural) reasons.
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.
Types
@type result() :: :match | {:mismatch, [String.t()]}
:match, or a mismatch with the field names that disagreed.
Functions
@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.
@spec deparse_text_marker() :: String.t()
The marker prefix stamped on rendering-derived (not structural) reasons.
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.