PhoenixKit PostgreSQL Migration System
This module handles versioned migrations for PhoenixKit, supporting incremental updates and rollbacks between different schema versions.
Migration Versions
V169 - Anonymous entity submissions, and one duplicate foreign key ⚡ LATEST
Makes phoenix_kit_entity_data.created_by_uuid nullable: the public entity
form is deliberately unauthenticated and has no creator to record, so on a
freshly migrated database every anonymous submission failed with a
not_null_violation. Long-lived installs were already storing NULL there.
Recorded in V164's @relaxed_after_v57 and in the V135 baseline so repair and
a fresh install agree with it.
Also drops the duplicate foreign key V135 created on
phoenix_kit_ai_requests.prompt_uuid, keeping the legacy
phoenix_kit_ai_requests_prompt_uuid_fkey — the name the installed base
carries and the one Ecto derives by default — so no live database needs a
rename.
V168 - The remaining slug indexes
Finishes what V167 started. An audit of every schema declaring a slug
unique_constraint/3 found two more with nothing to translate:
phoenix_kit_tickets (plain btree since V135, while Ticket declares
unique_constraint(:slug) and get_ticket_by_slug/2 fetches with
one()) and phoenix_kit_post_groups (no slug index at all, while
PostGroup names a composite [:user_uuid, :slug] index that exists
nowhere). The other six were already backed correctly.
Post-group slugs are unique per user, so that index is on
(user_uuid, slug) and the dedup partitions by the pair. Existing
duplicates are suffixed -2, -3 … following Slug.ensure_unique/2,
the oldest row keeping the bare slug.
V167 - Unique post slugs
Makes phoenix_kit_posts_slug_index unique. It had been a plain btree
since V135 while its sibling phoenix_kit_post_tags.slug was unique, so
Post's unique_constraint(:slug) had no index to translate and
get_post_by_slug/2 — which fetches with one() — raised
Ecto.MultipleResultsError on any URL two posts shared.
Existing duplicates are suffixed -2, -3 … following
Slug.ensure_unique/2, keeping the reachable post over a draft and then
the oldest. Two live posts on one slug raises instead: one of them has
to lose a working URL, and that is the operator's call.
V166 - Frozen comment attribution
Adds author_display_name, attribution_mode, attributed_project_uuid
and attributed_label to phoenix_kit_comments. A name resolved at
render time rewrites history — someone leaves or fills in a profile and
every comment they wrote is silently re-signed — so what the reader was
shown is pinned at write. The same applies to speaking on a project's
behalf, which is a choice made at the time and not a fact recomputed from
current membership. user_uuid is never cleared: posting as the project
changes what the PUBLIC sees and nothing else, so moderation and audit
keep their actor.
Existing rows are left NULL rather than backfilled — inventing a display history we do not have would be worse than resolving those rows live.
V165 - Cross-module mentions and access requests
Adds phoenix_kit_mentions (the reverse index for @/# tokens — the
canonical mention lives in the text, this answers "what links here" and
gives notification fan-out something to diff) and
phoenix_kit_access_requests (asking the owner for access to a record a
mention pointed at but the reader cannot open). Neither target carries a
foreign key: both point into ~28 optional packages' tables.
V164 - Repair the V56/V57 flush-order bug's fallout, and converge two
prefix-unsafe historical shapes
- Also folds in what an earlier draft carried as a separate V164: V68
(partial
idx_publishing_posts_group_slug) and V65 (thephoenix_kit_subscription_plans_slug_uidx->..._types_slug_uidxrename) each issued a BARE, unqualified DROP/ALTER guarded byIF EXISTS: effective onpublic, a silent no-op in a named schema, so the two install paths diverged and theV135baseline — generated into a named schema — kept the unintended shape. This version idempotently converges both onto the historicalpublic/intended shape, and is a no-op on every real public install. This release ships ONE migration, so it lives here rather than in a second version - V56/V57 queued
UUIDFKColumns.up/1'sADD COLUMNs immediately beforeadd_constraints/1's immediatecolumn_exists?/NOT NULL guards with noflush()between them (V57 had none at all) — harmless on an incremental chain run, but on a single-shot run (fresh install) the guards ran before Postgres had ever seen the queued columns, so ~46*_uuidFK columns across ~33 tables were left nullable instead of NOT NULL, andphoenix_kit_comments.fk_comments_user_uuidwas never created at all — V72 later found it missing and guessedON DELETE CASCADEinstead of V56/V57's own declaredSET NULL - V56/V57 now carry the missing
flush(), and V72's guess is nowSET NULL, so this only repairs installs whose single-shot run already happened before those fixes; it is a no-op everywhere else - Per affected column: sets NOT NULL only if it currently has zero NULL rows; otherwise warns (table/column/row count) and leaves it nullable — never backfills live data with an invented value
- Corrects
fk_comments_user_uuidfrom CASCADE to SET NULL if the buggy shape is present - Repair-only:
down/1restamps the comment, never undoes the fix
V163 - UUID primary-key integrity (catalog-driven repair, upstream #688)
- Repairs any
phoenix_kit_*table whoseuuidcolumn is the wrong type, nullable, or not the primary key — the state V40/V56/V74 each assumed impossible and a production install reached anyway (phoenix_kit_email_events:varchar(255), nullable, no PK at all) - Catalog-driven on purpose: every earlier attempt enumerated tables by hand and this one was missing from every list
- Above two million rows the
ALTER COLUMN ... TYPE uuidrewrite and theADD PRIMARY KEYare DEFERRED and logged with the command to run in a maintenance window, rather than takingACCESS EXCLUSIVEon a large table mid-deploy;mix phoenix_kit.doctoris the loud channel - Runs BEFORE V164 by construction, which is the order V164 needs: a
foreign key cannot reference a column with no unique/primary key, so
promoting
uuidto PK here is what lets V164's FK repair validate - NOTE: upstream's own moduledoc for this version is missing — its
heading landed above V162's body and V162's heading was lost. The
section below is written from
v163.ex's own moduledoc; carry this correction back in the PR
V162 - Payment-option linkage on billing orders
Adds a nullable payment_option_uuid FK (+ index) to
phoenix_kit_orders, pointing at phoenix_kit_payment_options. The
order's payment_method is a small closed vocabulary; the payment
OPTION is the operator-configured row the customer actually chose, and
the choice used to be discarded at checkout. ON DELETE SET NULL so
deleting an option neither fails nor destroys order history.
V161 - Case-insensitive phoenix_kit_users.username (citext)
usernamewasVARCHAR(255)(V08's:string) — comparison semantics come from the column type, not the Ecto schema field, so every lookup (get_user_by_username/1,unsafe_validate_unique, the unique index itself) was exact-match;aliceandAlicecould both register- Converts the column to
citext, same fix already applied toemailin V01 and the CRM party email columns in V151 - Pre-check (mirrors V106's down-step) raises on any existing
case-insensitive collision before the DDL runs, naming the offending
value;
WHERE username IS NOT NULLguards against nullable rows false-colliding underGROUP BY varchar→citextis binary-coercible (pg_cast.castmethod = 'b'), confirmed live — no table rewrite; the column's B-tree index does get rebuilt (also confirmed live), which is what makes it enforce case-insensitive uniqueness right after theALTER
V160 - Settings value widened to TEXT
phoenix_kit_settings.valuewasVARCHAR(255)(V03's:string) whileSettings.Settingvalidated it atmax: 1000— anything in between passed the changeset and then raised a rawPostgrex.Error- Surfaced by list-valued settings: the sitemap's default exclude patterns serialize to ~450 characters, so saving them always crashed
- Catalog-only change in PostgreSQL: no rewrite, no long lock
V159 - Publishing categories + post view counters
phoenix_kit_publishing_categories— hierarchical per-group taxonomy (nullableparent_uuidself-FK, V103 catalogue shape);slugunique per group;name_i18nJSONB per-language display names;positionfor manual ordering; group delete cascades, parent delete lifts children to the root (ON DELETE SET NULL)phoenix_kit_publishing_post_categories— post↔category M:N (post-level, WordPress semantics — not per-version); both sides cascadephoenix_kit_publishing_post_views— per-day view rollups keyed(post_uuid, view_date), incremented in place; totals areSUM(count); dedup/bot filtering are app-side, no reader PII stored
V158 - Broadcast attachments (accumulator)
- Adds
attachments JSONB NOT NULL DEFAULT '[]'tophoenix_kit_newsletters_broadcasts— an ordered list of Storage file uuids attached to every email of the broadcast; soft references (no FK) per this table'scrm_list_uuidprecedent, with ajsonb_typeof = 'array'CHECK as the DB-level shape backstop - Shipped in 1.7.211 — the accumulator is closed; the next restructuring section opens V159
V157 - Image annotation kind
- Widens
phoenix_kit_annotations_kind_checkto allow'image' - Pairs with the schema's
@kinds(also widened) so Etcher's:imagetool — exposed in the media viewer's toolbar by PR #660 — can actually persist; same regression shape as V130's"marker"
V156 - Legacy newsletters lists migrated into CRM, tables dropped
- Requires a coordinated release with the newsletters module — drops tables/columns an older newsletters release still reads; see V156's moduledoc warning
- Data: every
phoenix_kit_newsletters_listsrow copied tophoenix_kit_crm_lists(same slug — reused if a CRM list already has it),subscribable = true - Data: a
phoenix_kit_crm_contactsrow per distinct user with a legacy membership (reused if one already exists by email), linked to that user'suser_uuidvia a straight UPDATE againstphoenix_kit_users— never creates a user,connect_user/2's placeholder-minting is structurally unreachable from this migration - Data: legacy memberships copied to
phoenix_kit_crm_list_members, status mapped (active→subscribed,unsubscribed→removed),subscribed_at/unsubscribed_atpreserved verbatim (notnow());subscriber_countrecounted after - Re-points every
newsletters_listbroadcast still referencing a migrated list tosource_type = 'crm_list'+crm_list_uuid; any broadcast an orphanedlist_uuidcouldn't be re-pointed from (should be none —ON DELETE RESTRICTguarantees referential integrity, see moduledoc) has that uuid preserved intosource_params->>'legacy_list_uuid'first - Drops
fk_newsletters_broadcasts_list+list_uuidcolumn, thenphoenix_kit_newsletters_list_membersandphoenix_kit_newsletters_liststhemselves down/1restores the two tables (V79 shape) and the FK/column (nullable, matching V152) — structure only, migrated/re-pointed data is not moved back
V155 - Delivery CRM contact id + per-broadcast dedup
- Adds
crm_contact_uuid(bare, nullable UUID, no FK — same soft-ref pattern ascrm_list_uuid) tophoenix_kit_newsletters_deliveries, plus a plain index on it - Replaces
phoenix_kit_newsletters_deliveries_recipient_check(same name) with a widened CHECK: still requires an addressable recipient (user_uuidorrecipient_email), and now additionally forbids a row claimed by bothuser_uuidandcrm_contact_uuidat once — deliberately NOT a strict XOR; see V155's moduledoc for why - Adds three partial unique indexes —
(broadcast_uuid, user_uuid),(broadcast_uuid, crm_contact_uuid),(broadcast_uuid, recipient_email), eachWHERE ... IS NOT NULL— the first DB-level per-broadcast delivery dedup;insert_allpreviously had noON CONFLICTguard at all - Adds
source_params JSONB NOT NULL DEFAULT '{}'tophoenix_kit_newsletters_broadcasts, for the newuser_group(core-role) recipient source — a role set, so JSONB rather than another scalar soft-ref uuid column. Shape:%{"role_uuids" => [...], "role_names_snapshot" => [...]}— uuids resolve (a role's name is mutable), the name snapshot is display-only
V154 - OpenGraph templates + assignments (phoenix_kit_og)
- Adds
phoenix_kit_og_templates(reusable OG canvas designs; JSONBcanvaselement list) andphoenix_kit_og_assignments(binds a template to amodule_key × scope_type × scope_uuidscope with a JSONBslot_mapping). Uniqueness via a partial-index pair (NULLscope_uuidis the module-wide default tier);template_uuidcascades on delete. Powers thephoenix_kit_ogplugin.
V153 - Folder header size defaults to small
- Flips
phoenix_kit_media_folders.header_sizecolumn default from 'medium' (V134) to 'small', and backfills existing 'medium' rows to 'small' ('medium' was the old default, so it reads as untouched; 'large' is a deliberate choice and is left alone)
V152 - Newsletters/CRM/Core restructuring (accumulator)
- Unreleased — per the "one open migration" rule, every DDL step of the restructuring plan lands in V152 as its own section until it ships; later stages append here rather than opening V153.
- Section: send profiles move to core Email. Creates
phoenix_kit_email_send_profiles— same shape V145 gavephoenix_kit_newsletters_send_profiles, now owned by core'sPhoenixKit.Emailnamespace. Copies every row across byuuid, then drops the V145 table.idx_nl_send_profiles_*indexes becomeidx_email_send_profiles_*. Does not touchphoenix_kit_newsletters_broadcasts.send_profile_uuid— still a bare UUID with no FK, so it points at the same row regardless of which table now owns it.
V150 - Readable device name on session tokens
- Adds nullable
browser+ostophoenix_kit_users_tokens, parsed from the User-Agent at login, so the Active Sessions list and admin all-sessions view show a device name for every session without the known-devices/geo machinery (which stays gated behind new-login alerts).
V149 - Catalogue item-supplier sourcing info + CRM xref
- Adds
phoenix_kit_cat_item_supplier_info(per-item, per-supplier SKU / unit cost / currency / lead time / MOQ;supplier_uuidsoft ref to a CRM party or localcat_supplier) and a softcrm_company_uuidxref onphoenix_kit_cat_suppliers. No primary among these rows — the item's default supplier is the V146primary_supplier_uuidscalar.
V148 - CRM party roles (suppliers, clients)
- Adds
phoenix_kit_crm_party_rolesfor thephoenix_kit_crmmodule: polymorphic role edge marking a CRM company or contact assupplier,client, or other commercial role. One party can hold several roles;valid_from/valid_tolifecycle,is_activefilter, role-scopedmetadata. No FK onroleable_uuid; unique on(roleable_type, roleable_uuid, role).
V147 - Known-device geo-location
- Adds nullable
location(City, Country) tophoenix_kit_user_known_devices. Resolved once at new-device time byPhoenixKit.Users.LoginAlertsand stored so the user's Active Sessions list can show sign-in location without a per-render geo lookup.
V146 - Catalogue item primary supplier
- Adds nullable
primary_supplier_uuidFK (ON DELETE SET NULL) + partial index tophoenix_kit_cat_items— an item's default supplier, independent of manufacturer (generic/unbranded materials; tie-break when a manufacturer has several suppliers). Backs thephoenix_kit_cataloguefeature from its commit 2e47cdf.
V145 - Newsletters Send Settings (send profiles)
- Adds
phoenix_kit_newsletters_send_profiles: named send configurations referencing a core Integrations connection (integration_uuid, no FK) plus per-account send parameters (from-name/email, reply-to, signature, rate limits,advancedper-provider extras jsonb). - Multiple profiles may share one integration; at most one may be
is_default, enforced by a partial unique index onis_default. - Adds
send_profile_uuid(bare UUID, no FK) tophoenix_kit_newsletters_broadcastsso a broadcast can pin which send profile delivers it.
V144 - Manufacturing/Warehouse module tables consolidation
- Consolidates 5 objects previously created by
phoenix_kit_manufacturing's andphoenix_kit_warehouse's ownmigration_module/0into core's migration chain:phoenix_kit_machines,phoenix_kit_machine_type_assignments,phoenix_kit_machine_operations,phoenix_kit_warehouse_transfers(+ itsnumbersequence), andphoenix_kit_warehouse_min_stock. machine_type_uuid/operation_uuidon the two join tables are soft references (no FK) to the entities package. Upgrade path for hosts on the publishedphoenix_kit_manufacturing0.2.0 (module V1): the join table already exists there with a live FK onmachine_type_uuid— this migration drops it unconditionally. Warehouse tables are fresh-install-only DDL (phoenix_kit_warehouse0.1.0 never published migrations for them, so no upgrade case exists).- The pre-V5 manufacturing directory tables (
phoenix_kit_machine_types,phoenix_kit_operations,phoenix_kit_defect_reasons) are not re-created; each is dropped only if present and empty, left in place with a databaseNOTICEwhen non-empty — see the PR body for the manual data-migration note on such hosts. - Rollback mirrors the five creates; see
V144.down/1's moduledoc for the upgrade-host caveat (can't distinguish a pre-existingmachine_type_assignmentstable from one V144 created).
V143 - Known-device history for new-login alerts
- Adds
phoenix_kit_user_known_devices(IP + hashed user-agent per user, unique per(user_uuid, ip_address, user_agent_hash)) so a login from an unrecognized device can be told apart from a familiar one. - Backs the
new_login_alert_enabledsetting anduser.new_login_detectedactivity action.
V142 - Wider role-permission keys
- Widens
phoenix_kit_role_permissions.module_keyfromVARCHAR(50)toVARCHAR(120)so fine-grained sub-permissions can be stored as composed dotted keys ("calendar.view_others"— base and sub parts are each capped at 50 chars, so a composed key can reach 101). - Rollback deletes rows over 50 chars (sub-permission grants are additive and re-grantable) before narrowing the column back.
V141 - Calendar events + participants
- Adds
phoenix_kit_calendar_eventsfor thephoenix_kit_calendarmodule: one implicit personal calendar per user (owner_uuidFK, CASCADE on user delete). Timed events use an exclusive-end UTC pair; all-day events use an exclusive-end DATE pair; a CHECK enforces exactly one pair per row matching theall_dayflag, with end > start. Status is active/cancelled.location_uuidloosely links a stored location (name snapshotted into thelocationstring — no cross-module FK). - Adds
phoenix_kit_calendar_event_participants: loosekind+target_uuidreferences (user / staff_person / crm_contact / crm_company / free_text) with adisplay_namesnapshot andadded_by_uuidaudit. Visibility is resolved LIVE at query time against the physical staff/CRM tables, so a company participant means "current members" and no module code is needed. Partial uniques dedup targets per event and free-text case-insensitively. - Extended in place while unreleased (idempotent-additive statements).
- Rollback drops both tables.
V140 - Warehouse module tables
- Creates
phoenix_kit_warehouse_stock,phoenix_kit_warehouse_inventory_documents,phoenix_kit_warehouse_internal_orders,phoenix_kit_warehouse_supplier_orders,phoenix_kit_warehouse_goods_receipts, andphoenix_kit_warehouse_goods_issues— the storage layer for the standalonephoenix_kit_warehousepackage. internal_ordersandgoods_issueshave no FK to any order table — the relationship lives in a genericsource_refsJSONB column instead, resolved by a host-registered callback so the package has zero dependency on any particular "order" concept. GIN-indexed for reverse lookups.- Intra-module FKs preserved:
supplier_orders.internal_order_uuid→internal_orders;goods_receipts.supplier_order_uuid→supplier_orders;goods_issues.internal_order_uuid→internal_orders. item_uuid,location_uuid,storage_folder_uuid,supplier_uuidare plain UUID columns — no FK, so the database does not enforce referential integrity for them (delete semantics still undecided).- No data is copied from any existing table — these tables are empty until a consuming app populates them.
V139 - Dashboard config column
- Adds a JSONB
configcolumn (NOT NULL DEFAULT '{}') tophoenix_kit_dashboardsfor per-dashboard presentation settings, read and written whole likelayout. Backs the dashboards plugin module. - Idempotent (
ADD COLUMN IF NOT EXISTS); rollback drops the column.
V138 - CRM v1 interaction tracker
- Adds five
phoenix_kit_crm_*tables for the CRM module's first data model:contacts(profile + optionaluser_uuidlogin link, partial-unique so it's 1:1 only among linked rows),companies,company_memberships(M:N contact↔company with free-formrole_in_company+department+is_primaryon the edge),interactions(logged interaction: type/when/body/subject contact/owner user), andinteraction_parties(flat resolvable "who was involved":raw_namealways kept,contact_uuid/staff_person_uuidresolve when matched under an exclusive-arc CHECK,party_snapshotJSONB freezes the party's profile as-of-then).staff_person_uuidis a soft ref (no FK) so the optional staff module stays optional.
V136 - Staff employment history
- Adds
phoenix_kit_staff_employments— a per-person history of employment spans (employment type, translatablejob_title, org placement viaprimary_department_uuid+ aprimary_team_uuidsnapshot, date range withemployment_end_date IS NULL= the open/current span,work_location,notes). A partial unique index enforces one open span per person. The matchingphoenix_kit_staff_peoplecolumns are kept as a denormalized mirror of the current span (written by the app'ssync_current/1), not dropped. Backfills one open span per existing person from those columns (guarded, retry-safe; people with no employment data are skipped).
V135 - Structured staff skills
- Replaces the free-text
phoenix_kit_staff_people.skillscolumn with a first-class translatablephoenix_kit_staff_skillsentity + aphoenix_kit_staff_person_skillsjoin. Each skill carries its own per-skill, translatable proficiency levels (levelsJSONB array of{id, name, translations}) and anallow_multiple_levelsboolean; the join'sproficiency_levelsJSONB array holds the selected level ids. Migrates the comma-separated free-text into structured rows (case-insensitive dedup, guarded for retry-safety) and drops the column. Lossy by design: per-localetranslations["skills"]overrides don't map to structured skills and are stripped. Also adds a partial index onphoenix_kit_staff_people(date_of_birth)(active + non-null DOB only) forStaff.upcoming_birthdays/1.
V01 - V134 - Baseline (consolidated into V135 by the squash)
Every version from V01 (initial auth/roles/settings foundation) through
V134 (media-folder header customization) has been collapsed into the
V135 baseline module — this release's @initial_version (the squash
floor; spec dev_docs/plans/2026-07-14-squash-migrations-spec.md).
V135.up/1 applies the FINAL post-V134 shape of every table, index,
constraint, function, extension, and seed directly — no intermediate
drops/renames/backfills are replayed. See
dev_docs/plans/2026-07-14-squash-inventory.md for the full
per-version history this consolidates (seeds, drops/renames, hazards)
and PhoenixKit.Migrations.ExpectedSchema
(lib/phoenix_kit/migrations/expected_schema.ex, tool-generated,
@moduledoc false) for the machine-readable manifest the baseline was
generated from.
Installs below V135 cannot upgrade directly to this release --
up/1/down/1 raise PhoenixKit.Migrations.BelowFloorError — they
must first apply the frozen pre-squash 1.7.x bridge release up to at
least V135, then move to this release (spec §7.2's two-stage rollout /
§5.2's registry guards).
Migration Paths
Fresh Installation (0 -> Current)
Applies the V135 baseline (the consolidated V01..V134 shape), then
every delta V136..V164 in sequence (plan_up/3's fresh-install clamp,
spec §5.2 D13).
Incremental Updates
- Below V135: rejected with
PhoenixKit.Migrations.BelowFloorError-- apply the 1.7.x bridge release first (spec §7.2). - At V135 (the floor) or above: runs each delta module from
current + 1through the target version in sequence.
Rollback Support
- Down to any version above V135: runs each delta module's
down/1in reverse, fromcurrentdown totarget + 1. - Down to V135 or below: the delta range stops at V136 (never
dispatches a deleted below-floor module);
V135.down/1is then applied directly for a fullversion: 0teardown (Oban included). A target strictly between 0 and V135 clamps to V135 instead of guessing an unreproducible intermediate shape (spec §5.2's{:clamped, ...}).
Usage Examples
# Update to the latest version
PhoenixKit.Migrations.Postgres.up(prefix: "myapp")
# Update to a specific version
PhoenixKit.Migrations.Postgres.up(prefix: "myapp", version: 150)
# Rollback to a specific version
PhoenixKit.Migrations.Postgres.down(prefix: "myapp", version: 149)
# Complete rollback (tears down the V135 baseline too)
PhoenixKit.Migrations.Postgres.down(prefix: "myapp", version: 0)PostgreSQL Features
- Schema prefix support for multi-tenant applications
- Optimized indexes for performance
- Foreign key constraints with proper cascading
- Extension support (citext)
- Version tracking with table comments
Summary
Types
Routing decision down/1 acts on — pure, see plan_down/3.
Routing decision up/1 acts on — pure, see plan_up/3.
Functions
The release an below-floor host must install before this one.
Heal version comment if schema artifacts exist for a higher version.
Get current migrated version from database in runtime context (outside migrations).
Types
@type down_plan() :: {:raise, db_version :: pos_integer(), floor :: pos_integer()} | {:teardown, Range.t(), floor :: pos_integer()} | {:clamped, Range.t(), floor :: pos_integer()} | {:run, Range.t()} | :noop
Routing decision down/1 acts on — pure, see plan_down/3.
@type up_plan() :: {:raise, db_version :: pos_integer(), floor :: pos_integer()} | {:run, Range.t()} | {:run_delta, Range.t()} | :noop
Routing decision up/1 acts on — pure, see plan_up/3.
Functions
@spec bridge_version() :: String.t()
The release an below-floor host must install before this one.
Exposed because mix phoenix_kit.update refuses below-floor installs at
GENERATION time, before this module's raise sites are ever reached — so the
notice the operator sees first has to name the same version those raises do.
Heal version comment if schema artifacts exist for a higher version.
V83 had a bug where the COMMENT ON TABLE statement used an incorrect prefix, leaving the comment at the previous version even though the migration ran successfully. This function detects and corrects the mismatch.
Returns {:healed, new_version} if the comment was fixed, or :ok if
no healing was needed.
Get current migrated version from database in runtime context (outside migrations).
This function can be called from Mix tasks and other non-migration contexts.