Packages

phoenix_kit

1.7.160
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 v63.ex
Raw

lib/phoenix_kit/migrations/postgres/v63.ex

defmodule PhoenixKit.Migrations.Postgres.V63 do
@moduledoc """
V63: UUID Companion Column Safety Net — Round 2
Adds missing `uuid` and `_uuid` FK companion columns identified after V62.
All operations are idempotent — safe to run on any installation.
## Issues Fixed
### 1. `phoenix_kit_ai_accounts` missing `uuid` column
V61 used the wrong table name in `@tables_missing_uuid` (listed
`phoenix_kit_ai_requests` instead of `phoenix_kit_ai_accounts`). This
left `ai_accounts` as the only legacy table without a `uuid` identity
column.
### 2. `phoenix_kit_ai_requests` missing `account_uuid` companion
The `account_id` integer FK had no UUID companion. Backfilled via JOIN
to `phoenix_kit_ai_accounts` once that table has its `uuid` column.
### 3. `phoenix_kit_email_orphaned_events` missing `matched_email_log_uuid`
The `matched_email_log_id` integer FK had no UUID companion. Backfilled
via JOIN to `phoenix_kit_email_logs`.
### 4. `phoenix_kit_invoices` missing `subscription_uuid` companion
The `subscription_id` integer FK had no UUID companion. Backfilled via
JOIN to `phoenix_kit_subscriptions`.
### 5. `phoenix_kit_shop_cart_items` missing `variant_uuid` companion
The `variant_id` integer FK had no UUID companion. No backfill possible
(no variants table in schema); column added as nullable for future use.
"""
use Ecto.Migration
def up(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Flush any pending migration commands from earlier versions
flush()
# Ensure uuid_generate_v7() exists (created in V40, be defensive)
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;
""")
# Flush so uuid_generate_v7() is available for subsequent queries
flush()
# 1. Add uuid column to ai_accounts (must come before account_uuid backfill)
add_uuid_to_ai_accounts(prefix, escaped_prefix)
# Flush so ai_accounts.uuid is visible for the JOIN backfill below
flush()
# 2. Add account_uuid companion to ai_requests
add_account_uuid_to_ai_requests(prefix, escaped_prefix)
# 3. Add matched_email_log_uuid companion to email_orphaned_events
add_matched_email_log_uuid(prefix, escaped_prefix)
# 4. Add subscription_uuid companion to invoices
add_subscription_uuid_to_invoices(prefix, escaped_prefix)
# 5. Add variant_uuid companion to shop_cart_items
add_variant_uuid_to_cart_items(prefix, escaped_prefix)
execute("COMMENT ON TABLE #{prefix_table("phoenix_kit", prefix)} IS '63'")
end
def down(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
remove_column_if_exists(:phoenix_kit_shop_cart_items, :variant_uuid, prefix, escaped_prefix)
remove_column_if_exists(:phoenix_kit_invoices, :subscription_uuid, prefix, escaped_prefix)
remove_column_if_exists(
:phoenix_kit_email_orphaned_events,
:matched_email_log_uuid,
prefix,
escaped_prefix
)
remove_column_if_exists(:phoenix_kit_ai_requests, :account_uuid, prefix, escaped_prefix)
remove_uuid_from_ai_accounts(prefix, escaped_prefix)
execute("COMMENT ON TABLE #{prefix_table("phoenix_kit", prefix)} IS '62'")
end
# ---------------------------------------------------------------------------
# Individual fixups
# ---------------------------------------------------------------------------
defp add_uuid_to_ai_accounts(prefix, escaped_prefix) do
table = :phoenix_kit_ai_accounts
if table_exists?(table, escaped_prefix) and
not column_exists?(table, :uuid, escaped_prefix) do
table_name = prefix_table("phoenix_kit_ai_accounts", prefix)
execute("ALTER TABLE #{table_name} ADD COLUMN uuid UUID DEFAULT uuid_generate_v7()")
execute("UPDATE #{table_name} SET uuid = uuid_generate_v7() WHERE uuid IS NULL")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS phoenix_kit_ai_accounts_uuid_idx
ON #{table_name}(uuid)
""")
execute("ALTER TABLE #{table_name} ALTER COLUMN uuid SET NOT NULL")
end
end
defp add_account_uuid_to_ai_requests(prefix, escaped_prefix) do
table = :phoenix_kit_ai_requests
if table_exists?(table, escaped_prefix) and
not column_exists?(table, :account_uuid, escaped_prefix) do
table_name = prefix_table("phoenix_kit_ai_requests", prefix)
accounts_table = prefix_table("phoenix_kit_ai_accounts", prefix)
execute("ALTER TABLE #{table_name} ADD COLUMN account_uuid UUID")
# Backfill only if ai_accounts has uuid column (it should, we just added it)
if column_exists?(:phoenix_kit_ai_accounts, :uuid, escaped_prefix) do
execute("""
UPDATE #{table_name} r
SET account_uuid = a.uuid
FROM #{accounts_table} a
WHERE r.account_id = a.id
AND r.account_uuid IS NULL
AND r.account_id IS NOT NULL
""")
end
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_ai_requests_account_uuid_idx
ON #{table_name}(account_uuid)
""")
end
end
defp add_matched_email_log_uuid(prefix, escaped_prefix) do
table = :phoenix_kit_email_orphaned_events
if table_exists?(table, escaped_prefix) and
not column_exists?(table, :matched_email_log_uuid, escaped_prefix) do
table_name = prefix_table("phoenix_kit_email_orphaned_events", prefix)
email_logs_table = prefix_table("phoenix_kit_email_logs", prefix)
execute("ALTER TABLE #{table_name} ADD COLUMN matched_email_log_uuid UUID")
if table_exists?(:phoenix_kit_email_logs, escaped_prefix) do
# Wrapped in a DO block with EXCEPTION handler so that a type mismatch
# (e.g. email_logs.uuid is character varying on some installs) does not
# abort the outer migration transaction. The ::uuid cast handles the
# varchar case; V70 re-runs the backfill once the column type is fixed.
execute("""
DO $$
BEGIN
UPDATE #{table_name} e
SET matched_email_log_uuid = l.uuid::uuid
FROM #{email_logs_table} l
WHERE e.matched_email_log_id = l.id
AND e.matched_email_log_uuid IS NULL
AND e.matched_email_log_id IS NOT NULL;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'PhoenixKit: skipping matched_email_log_uuid backfill — %', SQLERRM;
END $$;
""")
end
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_email_orphaned_events_matched_log_uuid_idx
ON #{table_name}(matched_email_log_uuid)
""")
end
end
defp add_subscription_uuid_to_invoices(prefix, escaped_prefix) do
table = :phoenix_kit_invoices
if table_exists?(table, escaped_prefix) and
not column_exists?(table, :subscription_uuid, escaped_prefix) do
table_name = prefix_table("phoenix_kit_invoices", prefix)
subscriptions_table = prefix_table("phoenix_kit_subscriptions", prefix)
execute("ALTER TABLE #{table_name} ADD COLUMN subscription_uuid UUID")
if table_exists?(:phoenix_kit_subscriptions, escaped_prefix) do
execute("""
UPDATE #{table_name} i
SET subscription_uuid = s.uuid
FROM #{subscriptions_table} s
WHERE i.subscription_id = s.id
AND i.subscription_uuid IS NULL
AND i.subscription_id IS NOT NULL
""")
end
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_invoices_subscription_uuid_idx
ON #{table_name}(subscription_uuid)
""")
end
end
defp add_variant_uuid_to_cart_items(prefix, escaped_prefix) do
table = :phoenix_kit_shop_cart_items
if table_exists?(table, escaped_prefix) and
not column_exists?(table, :variant_uuid, escaped_prefix) do
table_name = prefix_table("phoenix_kit_shop_cart_items", prefix)
# No variants table exists — add nullable column for future use
execute("ALTER TABLE #{table_name} ADD COLUMN variant_uuid UUID")
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_shop_cart_items_variant_uuid_idx
ON #{table_name}(variant_uuid)
""")
end
end
defp remove_uuid_from_ai_accounts(prefix, escaped_prefix) do
table = :phoenix_kit_ai_accounts
if table_exists?(table, escaped_prefix) and
column_exists?(table, :uuid, escaped_prefix) do
table_name = prefix_table("phoenix_kit_ai_accounts", prefix)
execute("DROP INDEX IF EXISTS phoenix_kit_ai_accounts_uuid_idx")
execute("ALTER TABLE #{table_name} DROP COLUMN IF EXISTS uuid")
end
end
defp remove_column_if_exists(table, column, prefix, escaped_prefix) do
col_str = Atom.to_string(column)
if table_exists?(table, escaped_prefix) and
column_exists?(table, column, escaped_prefix) do
table_name = prefix_table(Atom.to_string(table), prefix)
execute("ALTER TABLE #{table_name} DROP COLUMN IF EXISTS #{col_str}")
end
end
# ---------------------------------------------------------------------------
# Helpers (same pattern as V61)
# ---------------------------------------------------------------------------
defp table_exists?(table, escaped_prefix) do
table_name = Atom.to_string(table)
case repo().query(
"""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = '#{table_name}'
AND table_schema = '#{escaped_prefix}'
)
""",
[],
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)
case repo().query(
"""
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{table_name}'
AND column_name = '#{column_name}'
AND table_schema = '#{escaped_prefix}'
)
""",
[],
log: false
) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp prefix_table(table_name, nil), do: table_name
defp prefix_table(table_name, "public"), do: "public.#{table_name}"
defp prefix_table(table_name, prefix), do: "#{prefix}.#{table_name}"
end