PhoenixKit.Migrations.Postgres.V163 (phoenix_kit v1.7.236)

Copy Markdown View Source

V163: UUID primary-key integrity.

Repairs any phoenix_kit_* table whose uuid column is the wrong type, nullable, or not the primary key — the state V40/V56/V74 are each supposed to make impossible, and which a production install reached anyway.

The reported state

On a database upgraded through the whole chain since V01, phoenix_kit_email_events had uuid as character varying(255), nullable, no default, and the table had no primary key at all. 149 other tables were correct, so this was one table falling out of the conversion rather than a broken upgrade.

Why the chain missed it, twice

  1. V40's guard tests EXISTENCE, not TYPE. An older release created that column as Ecto :string, so unless column_exists?(table, :uuid, …) was already true and V40 skipped the table wholesale — not just the ADD COLUMN, but the backfill, the SET NOT NULL and the unique index with it. The table is in V40's @tables_to_migrate; being listed did not help.

  2. V56's type conversion shipped after some hosts had already passed V56. ensure_all_uuid_columns_native_type/2 — which converts exactly this varchar column — was added to V56 on 2026-03-02, seventeen days after V56 itself (2026-02-13). A recorded version never re-runs, so every host that crossed V56 in that window kept the broken column permanently. V56's NOT NULL and index repairs also run off hardcoded table lists that phoenix_kit_email_events appears in none of.

V74 then dropped the legacy bigint id but could not promote uuid to primary key — wrong type, nullable — and did not verify its own documented post-condition ("after V74, every PhoenixKit table has uuid as its PK"). Nothing raised.

Why this migration is catalog-driven

Every previous attempt enumerated tables by hand, and this table was missing from every list. This one asks the catalog which tables are actually broken, so a table nobody remembered to list is repaired anyway — and so a table that is already correct is skipped without needing to be named.

Large tables are deferred, not silently rewritten

ALTER COLUMN … TYPE uuid rewrites the table under an ACCESS EXCLUSIVE lock, SET NOT NULL scans it under the same lock, and ADD PRIMARY KEY builds a unique index under it too. All three are O(rows), so the size limit gates the whole repair rather than only the rewrite — a keyless table needs no type change and would otherwise have had an index built over every row. On a big events table behind PgBouncer that is connection-pool exhaustion during mix ecto.migrate, not a pause, so above two million rows the table is left exactly as it was and logged with the command to run in a maintenance window.

This migration never raises on the happy path. A library does not own its hosts' deploy runbooks, and turning a latent problem (one audit table without a PK, on a version where no schema maps to it) into a failed deploy across a fleet is a worse outcome than the problem. mix phoenix_kit.doctor is the loud channel and reports precisely what was deferred.

Summary

Functions

down(opts)

up(opts)