From d4159d8d299d863fa8a4fb25bb2c51b33acf884a Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 6 Mar 2026 21:56:07 +0200 Subject: [PATCH] Fix V77/V78 migration crashes when UUID columns are missing V56 conditionally adds UUID columns but skips tables that don't exist yet. V77 and V78 assumed these columns always exist, causing hard crashes on upgrades where optional modules were enabled after V56 ran. V77: detect whether role_uuid exists via information_schema and fall back to id/role_id columns when it doesn't. V78: check if uuid column exists on ai_prompts before creating the FK constraint, backfilling it with V56's idempotent pattern if missing. Co-Authored-By: Claude Opus 4.6 --- lib/phoenix_kit/migrations/postgres/v77.ex | 51 ++++++++++++++++------ lib/phoenix_kit/migrations/postgres/v78.ex | 48 ++++++++++++++++++++ 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/lib/phoenix_kit/migrations/postgres/v77.ex b/lib/phoenix_kit/migrations/postgres/v77.ex index 614d002e..9dc83a70 100644 --- a/lib/phoenix_kit/migrations/postgres/v77.ex +++ b/lib/phoenix_kit/migrations/postgres/v77.ex @@ -35,7 +35,7 @@ defmodule PhoenixKit.Migrations.Postgres.V77 do rename_setting(table, "tickets_allow_reopen", "customer_service_allow_reopen") rename_setting(table, "auto_granted_perm:tickets", "auto_granted_perm:customer_service") - rename_role_permission(perms_table, "tickets", "customer_service") + rename_role_permission(perms_table, "tickets", "customer_service", prefix) execute("COMMENT ON TABLE #{prefix_str(prefix)}phoenix_kit IS '77'") end @@ -57,22 +57,47 @@ defmodule PhoenixKit.Migrations.Postgres.V77 do # Renames role permission module_key. If target already exists for the same role, # deletes the source row to avoid unique constraint violations. - defp rename_role_permission(table, from_key, to_key) do + # Dynamically detects whether uuid/role_uuid columns exist (V56 may have skipped them) + # and falls back to id/role_id when they don't. + defp rename_role_permission(table, from_key, to_key, prefix) do + schema = prefix || "public" + execute(""" DO $$ DECLARE + has_role_uuid BOOLEAN; r RECORD; BEGIN - FOR r IN SELECT uuid, role_uuid FROM #{table} WHERE module_key = '#{from_key}' LOOP - IF EXISTS ( - SELECT 1 FROM #{table} - WHERE module_key = '#{to_key}' AND role_uuid = r.role_uuid - ) THEN - DELETE FROM #{table} WHERE uuid = r.uuid; - ELSE - UPDATE #{table} SET module_key = '#{to_key}' WHERE uuid = r.uuid; - END IF; - END LOOP; + SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = '#{schema}' + AND table_name = 'phoenix_kit_role_permissions' + AND column_name = 'role_uuid' + ) INTO has_role_uuid; + + IF has_role_uuid THEN + FOR r IN SELECT uuid, role_uuid FROM #{table} WHERE module_key = '#{from_key}' LOOP + IF EXISTS ( + SELECT 1 FROM #{table} + WHERE module_key = '#{to_key}' AND role_uuid = r.role_uuid + ) THEN + DELETE FROM #{table} WHERE uuid = r.uuid; + ELSE + UPDATE #{table} SET module_key = '#{to_key}' WHERE uuid = r.uuid; + END IF; + END LOOP; + ELSE + FOR r IN SELECT id, role_id FROM #{table} WHERE module_key = '#{from_key}' LOOP + IF EXISTS ( + SELECT 1 FROM #{table} + WHERE module_key = '#{to_key}' AND role_id = r.role_id + ) THEN + DELETE FROM #{table} WHERE id = r.id; + ELSE + UPDATE #{table} SET module_key = '#{to_key}' WHERE id = r.id; + END IF; + END LOOP; + END IF; END $$; """) end @@ -81,7 +106,7 @@ defmodule PhoenixKit.Migrations.Postgres.V77 do table = "#{prefix_str(prefix)}phoenix_kit_settings" perms_table = "#{prefix_str(prefix)}phoenix_kit_role_permissions" - rename_role_permission(perms_table, "customer_service", "tickets") + rename_role_permission(perms_table, "customer_service", "tickets", prefix) rename_setting(table, "auto_granted_perm:customer_service", "auto_granted_perm:tickets") rename_setting(table, "customer_service_allow_reopen", "tickets_allow_reopen") rename_setting(table, "customer_service_attachments_enabled", "tickets_attachments_enabled") diff --git a/lib/phoenix_kit/migrations/postgres/v78.ex b/lib/phoenix_kit/migrations/postgres/v78.ex index d91fbebd..80d085dd 100644 --- a/lib/phoenix_kit/migrations/postgres/v78.ex +++ b/lib/phoenix_kit/migrations/postgres/v78.ex @@ -37,6 +37,12 @@ defmodule PhoenixKit.Migrations.Postgres.V78 do ) if table_exists?(:phoenix_kit_ai_prompts, prefix) do + # V56 may have skipped adding the uuid column to ai_prompts if the table + # didn't exist at V56 time. Ensure it exists before creating the FK. + unless column_exists?(:phoenix_kit_ai_prompts, :uuid, prefix) do + ensure_uuid_column_and_index(:phoenix_kit_ai_prompts, prefix) + end + execute """ DO $$ BEGIN @@ -113,6 +119,48 @@ defmodule PhoenixKit.Migrations.Postgres.V78 do exists end + defp column_exists?(table_name, column_name, prefix) do + query = """ + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = '#{prefix}' + AND table_name = '#{table_name}' + AND column_name = '#{column_name}' + ) + """ + + %{rows: [[exists]]} = PhoenixKit.RepoHelper.repo().query!(query) + exists + end + + # Replicates V56's idempotent pattern for adding a uuid column with + # backfill, NOT NULL constraint, and unique index. + defp ensure_uuid_column_and_index(table_name, prefix) do + full_table = "#{prefix_str(prefix)}#{table_name}" + index_name = "#{table_name}_uuid_index" + + execute(""" + ALTER TABLE #{full_table} + ADD COLUMN IF NOT EXISTS uuid UUID DEFAULT uuid_generate_v7() + """) + + execute(""" + UPDATE #{full_table} + SET uuid = uuid_generate_v7() + WHERE uuid IS NULL + """) + + execute(""" + ALTER TABLE #{full_table} + ALTER COLUMN uuid SET NOT NULL + """) + + execute(""" + CREATE UNIQUE INDEX IF NOT EXISTS #{index_name} + ON #{full_table}(uuid) + """) + end + defp prefix_str("public"), do: "public." defp prefix_str(prefix), do: "#{prefix}." end