Packages

phoenix_kit

1.7.39
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit migrations postgres v56.ex
Raw

lib/phoenix_kit/migrations/postgres/v56.ex

defmodule PhoenixKit.Migrations.Postgres.V56 do
@moduledoc """
V56: UUID Column Consistency Fix
Comprehensive fix for UUID column issues across all PhoenixKit tables.
Ensures every table with a uuid column has:
- DEFAULT uuid_generate_v7() (not gen_random_uuid())
- NOT NULL constraint
- Unique index
Also adds the missing uuid column to `phoenix_kit_consent_logs` (V43 schema
expects it but the migration never created it).
All operations are idempotent — safe to run on fresh installs where V40
already set things up correctly, and on upgrades where uuid_repair.ex or
V45/V46/V53 left columns in an inconsistent state.
Existing UUID values are NOT changed — they remain valid UUIDs regardless
of version. Only defaults, constraints, and indexes are updated.
## Issues Fixed
### 1. Wrong DEFAULT (gen_random_uuid → uuid_generate_v7)
V45, V46, V53, and V55 used `gen_random_uuid()` (UUIDv4) instead of the
`uuid_generate_v7()` function created in V40.
Additionally, `uuid_repair.ex` (pre-1.7.0 upgrade path) adds uuid columns
with `gen_random_uuid()`, and V40 then skips those tables because the column
already exists — leaving them with the wrong default.
### 2. Missing NOT NULL constraint
V46 and V53 tables, plus uuid_repair.ex tables, may have nullable uuid columns.
### 3. Missing unique indexes
V45 tables and uuid_repair.ex tables may lack a unique index on the uuid column.
### 4. Missing uuid column entirely
V43 created `phoenix_kit_consent_logs` without a uuid column, but the Ecto
schema (`consent_log.ex`) declares `field :uuid, Ecto.UUID, read_after_writes: true`.
Additionally, `phoenix_kit_payment_methods`, `phoenix_kit_ai_endpoints`,
`phoenix_kit_ai_prompts`, `phoenix_kit_sync_connections`, and
`phoenix_kit_subscription_plans` were created without uuid columns but are
referenced as FK source tables in UUIDFKColumns (the backfill SQL does
`SET uuid_fk = s.uuid FROM source_table s`).
### 5. Wrong DEFAULT on UUID primary keys (gen_random_uuid → uuid_generate_v7)
V55 Comments module tables use UUID as primary key with `gen_random_uuid()`.
These need the same DEFAULT fix but on the `id` column instead of `uuid`.
## Tables Fixed
### V43 — Legal Module (missing uuid column entirely)
- phoenix_kit_consent_logs
### V45 — Shop Module (DEFAULT + unique index)
- phoenix_kit_shop_categories
- phoenix_kit_shop_products
- phoenix_kit_shop_shipping_methods
- phoenix_kit_shop_carts
- phoenix_kit_shop_cart_items
- phoenix_kit_payment_options
### V46 — Shop Extensions (DEFAULT + NOT NULL)
- phoenix_kit_shop_config
- phoenix_kit_shop_import_logs
- phoenix_kit_shop_import_configs
### V53 — Permissions (DEFAULT + NOT NULL)
- phoenix_kit_role_permissions
### V55 — Comments Module (DEFAULT on UUID primary key)
- phoenix_kit_comments
- phoenix_kit_comments_likes
- phoenix_kit_comments_dislikes
### uuid_repair.ex path — Core tables (DEFAULT + NOT NULL + unique index)
These are only affected on pre-1.7.0 upgrades where uuid_repair.ex ran before
V40, causing V40 to skip them. On fresh installs V40 handles them correctly,
and the operations here are no-ops.
- phoenix_kit_users
- phoenix_kit_users_tokens
- phoenix_kit_user_roles
- phoenix_kit_user_role_assignments
- phoenix_kit_settings
- phoenix_kit_email_templates
"""
use Ecto.Migration
alias PhoenixKit.Migrations.UUIDFKColumns
# Tables missing the uuid column entirely (schema expects it, migration never created it)
# consent_logs: V43 created without uuid column
# payment_methods, ai_endpoints, ai_prompts, sync_connections: created in billing/AI/sync
# migrations without uuid column, but referenced as FK sources in UUIDFKColumns
@tables_missing_column [
:phoenix_kit_consent_logs,
:phoenix_kit_payment_methods,
:phoenix_kit_ai_endpoints,
:phoenix_kit_ai_prompts,
:phoenix_kit_sync_connections,
:phoenix_kit_subscription_plans
]
# Tables using UUID as primary key with wrong DEFAULT (V55 Comments module)
@tables_fix_uuid_pk [
:phoenix_kit_comments,
:phoenix_kit_comments_likes,
:phoenix_kit_comments_dislikes
]
# All tables that may have the wrong DEFAULT (includes tables that need the column added)
@all_tables [
# V43 — Legal (column added by this migration)
:phoenix_kit_consent_logs,
# Billing/AI/Sync — FK source tables (column added by this migration)
:phoenix_kit_payment_methods,
:phoenix_kit_ai_endpoints,
:phoenix_kit_ai_prompts,
:phoenix_kit_sync_connections,
:phoenix_kit_subscription_plans,
# V45 — Shop Module
:phoenix_kit_shop_categories,
:phoenix_kit_shop_products,
:phoenix_kit_shop_shipping_methods,
:phoenix_kit_shop_carts,
:phoenix_kit_shop_cart_items,
:phoenix_kit_payment_options,
# V46 — Shop Extensions
:phoenix_kit_shop_config,
:phoenix_kit_shop_import_logs,
:phoenix_kit_shop_import_configs,
# V53 — Permissions
:phoenix_kit_role_permissions,
# uuid_repair.ex path — Core tables
:phoenix_kit_users,
:phoenix_kit_users_tokens,
:phoenix_kit_user_roles,
:phoenix_kit_user_role_assignments,
:phoenix_kit_settings,
:phoenix_kit_email_templates
]
# Tables that may have nullable uuid columns
@tables_ensure_not_null [
# V43 table (column added by this migration)
:phoenix_kit_consent_logs,
# Billing/AI/Sync — FK source tables (column added by this migration)
:phoenix_kit_payment_methods,
:phoenix_kit_ai_endpoints,
:phoenix_kit_ai_prompts,
:phoenix_kit_sync_connections,
:phoenix_kit_subscription_plans,
# V46 tables (created nullable)
:phoenix_kit_shop_config,
:phoenix_kit_shop_import_logs,
:phoenix_kit_shop_import_configs,
# V53 table (created nullable)
:phoenix_kit_role_permissions,
# uuid_repair.ex tables (no NOT NULL added)
:phoenix_kit_users,
:phoenix_kit_users_tokens,
:phoenix_kit_user_roles,
:phoenix_kit_user_role_assignments,
:phoenix_kit_settings,
:phoenix_kit_email_templates
]
# Tables that may be missing a unique index on uuid
@tables_ensure_index [
# V43 table (column added by this migration)
:phoenix_kit_consent_logs,
# Billing/AI/Sync — FK source tables (column added by this migration)
:phoenix_kit_payment_methods,
:phoenix_kit_ai_endpoints,
:phoenix_kit_ai_prompts,
:phoenix_kit_sync_connections,
:phoenix_kit_subscription_plans,
# V45 tables (no unique index created)
:phoenix_kit_shop_categories,
:phoenix_kit_shop_products,
:phoenix_kit_shop_shipping_methods,
:phoenix_kit_shop_carts,
:phoenix_kit_shop_cart_items,
:phoenix_kit_payment_options,
# uuid_repair.ex tables (no index created)
:phoenix_kit_users,
:phoenix_kit_users_tokens,
:phoenix_kit_user_roles,
:phoenix_kit_user_role_assignments,
:phoenix_kit_settings,
:phoenix_kit_email_templates
]
def up(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Ensure uuid_generate_v7() function exists (created in V40, but be safe)
execute("""
CREATE OR REPLACE FUNCTION uuid_generate_v7()
RETURNS uuid AS $$
DECLARE
unix_ts_ms bytea;
uuid_bytes bytea;
BEGIN
unix_ts_ms := substring(int8send(floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint) FROM 3);
uuid_bytes := unix_ts_ms || gen_random_bytes(10);
uuid_bytes := set_byte(uuid_bytes, 6, (get_byte(uuid_bytes, 6) & 15) | 112);
uuid_bytes := set_byte(uuid_bytes, 8, (get_byte(uuid_bytes, 8) & 63) | 128);
RETURN encode(uuid_bytes, 'hex')::uuid;
END
$$ LANGUAGE plpgsql VOLATILE;
""")
# Fix 0: Add missing uuid columns (V43 consent_logs)
for table <- @tables_missing_column do
add_uuid_column(table, prefix, escaped_prefix)
end
# Fix 1: Ensure correct DEFAULT on all tables
for table <- @all_tables do
fix_uuid_default(table, prefix, escaped_prefix)
end
# Fix 2: Backfill NULL uuids and ensure NOT NULL constraint
for table <- @tables_ensure_not_null do
fix_not_null(table, prefix, escaped_prefix)
end
# Fix 3: Ensure unique index exists
for table <- @tables_ensure_index do
fix_unique_index(table, prefix, escaped_prefix)
end
# Fix 4: Fix UUID primary key defaults (V55 Comments tables)
for table <- @tables_fix_uuid_pk do
fix_uuid_pk_default(table, prefix, escaped_prefix)
end
# Fix 5: Add UUID FK columns alongside integer FKs
UUIDFKColumns.up(opts)
# Fix 6: Add NOT NULL + FK constraints on UUID FK columns
UUIDFKColumns.add_constraints(opts)
# Fix 7: Add unique indexes on UUID FK columns (needed for ON CONFLICT in Ecto)
add_uuid_unique_indexes(prefix, escaped_prefix)
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '56'")
end
def down(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Revert Fix 7: Drop UUID unique indexes
drop_uuid_unique_indexes(prefix, escaped_prefix)
# Revert Fix 6: Drop FK constraints + NOT NULL before dropping columns
UUIDFKColumns.drop_constraints(opts)
# Revert Fix 5: Drop UUID FK columns
UUIDFKColumns.down(opts)
# Only revert V45/V46/V53 tables (not the uuid_repair core tables,
# which should have been correct from V40 on most installs)
# Revert unique indexes for V45 tables only
for table <- v45_tables() do
revert_unique_index(table, prefix, escaped_prefix)
end
# Revert NOT NULL for V46 + V53 tables only
for table <- v46_v53_tables() do
revert_not_null(table, prefix, escaped_prefix)
end
# Revert DEFAULT for V45/V46/V53 tables only
for table <- v45_v46_v53_tables() do
revert_uuid_default(table, prefix, escaped_prefix)
end
# Revert UUID PK defaults for V55 Comments tables
for table <- @tables_fix_uuid_pk do
revert_uuid_pk_default(table, prefix, escaped_prefix)
end
# Drop uuid column from tables where we added it
for table <- @tables_missing_column do
drop_uuid_column(table, prefix, escaped_prefix)
end
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '55'")
end
# Table groups for rollback (don't revert core table fixes)
defp v45_tables do
[
:phoenix_kit_shop_categories,
:phoenix_kit_shop_products,
:phoenix_kit_shop_shipping_methods,
:phoenix_kit_shop_carts,
:phoenix_kit_shop_cart_items,
:phoenix_kit_payment_options
]
end
defp v46_v53_tables do
[
:phoenix_kit_shop_config,
:phoenix_kit_shop_import_logs,
:phoenix_kit_shop_import_configs,
:phoenix_kit_role_permissions
]
end
defp v45_v46_v53_tables, do: v45_tables() ++ v46_v53_tables()
# Fix 0: Add uuid column to tables that are missing it entirely
defp add_uuid_column(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and not column_exists?(table, :uuid, escaped_prefix) do
execute("""
ALTER TABLE #{table_name}
ADD COLUMN uuid UUID DEFAULT uuid_generate_v7()
""")
# Backfill existing rows
execute("""
UPDATE #{table_name}
SET uuid = uuid_generate_v7()
WHERE uuid IS NULL
""")
end
end
# Fix 1: Set DEFAULT to uuid_generate_v7()
defp fix_uuid_default(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :uuid, escaped_prefix) do
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid SET DEFAULT uuid_generate_v7()
""")
end
end
# Fix 2: Backfill NULLs and set NOT NULL
defp fix_not_null(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :uuid, escaped_prefix) do
# Backfill any NULL uuid values with UUIDv7
execute("""
UPDATE #{table_name}
SET uuid = uuid_generate_v7()
WHERE uuid IS NULL
""")
# Set NOT NULL (no-op if already NOT NULL)
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid SET NOT NULL
""")
end
end
# Fix 3: Add unique index on uuid column
defp fix_unique_index(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
index_name = prefix_index_name(table, prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :uuid, escaped_prefix) do
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS #{index_name}
ON #{table_name}(uuid)
""")
end
end
# Fix 4: Fix UUID primary key default (tables using UUID as PK, not secondary column)
defp fix_uuid_pk_default(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :id, escaped_prefix) do
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN id SET DEFAULT uuid_generate_v7()
""")
end
end
# Rollback helpers
defp revert_uuid_default(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :uuid, escaped_prefix) do
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid SET DEFAULT gen_random_uuid()
""")
end
end
defp revert_not_null(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :uuid, escaped_prefix) do
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid DROP NOT NULL
""")
end
end
defp drop_uuid_column(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :uuid, escaped_prefix) do
# Drop index first
index_name = prefix_index_name(table, prefix)
execute("DROP INDEX IF EXISTS #{index_name}")
execute("""
ALTER TABLE #{table_name}
DROP COLUMN uuid
""")
end
end
defp revert_uuid_pk_default(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
if table_exists?(table, escaped_prefix) and column_exists?(table, :id, escaped_prefix) do
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN id SET DEFAULT gen_random_uuid()
""")
end
end
defp revert_unique_index(table, prefix, escaped_prefix) do
if table_exists?(table, escaped_prefix) do
index_name = prefix_index_name(table, prefix)
execute("""
DROP INDEX IF EXISTS #{index_name}
""")
end
end
# Rollback helpers for Fix 7
defp drop_uuid_unique_indexes(prefix, escaped_prefix) do
indexes = [
{:phoenix_kit_user_role_assignments,
"phoenix_kit_role_assignments_user_uuid_role_uuid_idx"},
{:phoenix_kit_role_permissions, "phoenix_kit_role_permissions_role_uuid_module_key_idx"},
{:phoenix_kit_user_oauth_providers, "phoenix_kit_oauth_providers_user_uuid_provider_idx"}
]
for {table, index_name} <- indexes do
if table_exists?(table, escaped_prefix) do
idx = if prefix && prefix != "public", do: "#{prefix}.#{index_name}", else: index_name
execute("DROP INDEX IF EXISTS #{idx}")
end
end
end
# Fix 6: UUID unique indexes for ON CONFLICT support
defp add_uuid_unique_indexes(prefix, escaped_prefix) do
indexes = [
{:phoenix_kit_user_role_assignments, [:user_uuid, :role_uuid],
"phoenix_kit_role_assignments_user_uuid_role_uuid_idx"},
{:phoenix_kit_role_permissions, [:role_uuid, :module_key],
"phoenix_kit_role_permissions_role_uuid_module_key_idx"},
{:phoenix_kit_user_oauth_providers, [:user_uuid, :provider],
"phoenix_kit_oauth_providers_user_uuid_provider_idx"}
]
for {table, columns, index_name} <- indexes do
if table_exists?(table, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
cols = Enum.join(columns, ", ")
idx = if prefix && prefix != "public", do: "#{prefix}.#{index_name}", else: index_name
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS #{idx}
ON #{table_name}(#{cols})
""")
end
end
end
# Helpers
defp table_exists?(table, escaped_prefix) do
table_name = Atom.to_string(table)
query = """
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = '#{table_name}'
AND table_schema = '#{escaped_prefix}'
)
"""
case repo().query(query, [], log: false) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp column_exists?(table, column, escaped_prefix) do
table_name = Atom.to_string(table)
column_name = Atom.to_string(column)
query = """
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{table_name}'
AND column_name = '#{column_name}'
AND table_schema = '#{escaped_prefix}'
)
"""
case repo().query(query, [], log: false) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp prefix_table_name(table_name, nil), do: table_name
defp prefix_table_name(table_name, "public"), do: "public.#{table_name}"
defp prefix_table_name(table_name, prefix), do: "#{prefix}.#{table_name}"
defp prefix_index_name(table, nil), do: "#{table}_uuid_idx"
defp prefix_index_name(table, "public"), do: "#{table}_uuid_idx"
defp prefix_index_name(table, prefix), do: "#{prefix}_#{table}_uuid_idx"
end