PhoenixKit.Migrations.Repair.Differ (phoenix_kit v2.1.0)

Copy Markdown View Source

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"):

  • :columntype, 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).
  • :functionreturns, 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.
  • :indexunique, 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.
  • :seedpresence-only, deliberatelycompare/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.

Summary

Types

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

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

result()

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

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

Functions

compare(atom, expected, observed)

@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()

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

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

deparse_text_only?(arg1)

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