Packages

phoenix_kit

1.7.44
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 v40.ex
Raw

lib/phoenix_kit/migrations/postgres/v40.ex

defmodule PhoenixKit.Migrations.Postgres.V40 do
@moduledoc """
PhoenixKit V40 Migration: UUID Column Addition for Legacy Tables
This migration adds UUID columns to all legacy tables that currently use
bigserial primary keys. This is Phase 1 of the graceful UUID migration
strategy, designed to be completely non-breaking for existing installations.
## Strategy
This migration is designed to work with PhoenixKit as a library dependency:
- **Non-breaking**: Only adds new columns, doesn't change existing PKs
- **Backward compatible**: Existing code using integer IDs continues to work
- **Forward compatible**: New code can start using UUIDs immediately
- **Optional module aware**: Skips tables that don't exist (disabled modules)
## Changes
For each of the 33 legacy tables:
1. Adds a `uuid` column (UUID type, using UUIDv7 for time-ordering)
2. Backfills existing records with generated UUIDv7 values
3. Creates a unique index on the uuid column
4. Sets the column to NOT NULL after backfill
5. Keeps DEFAULT for database-level inserts (Ecto changesets override with UUIDv7)
## UUIDv7
This migration uses UUIDv7 (time-ordered UUIDs) which provide:
- Time-based ordering (first 48 bits are Unix timestamp in milliseconds)
- Better index locality than random UUIDs
- Sortable by creation time
- Compatible with standard UUID format
A PostgreSQL function `uuid_generate_v7()` is created to generate UUIDv7
values at the database level.
## Tables Affected
### Core Auth (V01)
- phoenix_kit_users
- phoenix_kit_users_tokens
- phoenix_kit_user_roles
- phoenix_kit_user_role_assignments
### Settings & Referrals (V03-V04)
- phoenix_kit_settings
- phoenix_kit_referral_codes
- phoenix_kit_referral_code_usage
### Email System (V07, V09, V15, V22)
- phoenix_kit_email_logs
- phoenix_kit_email_events
- phoenix_kit_email_blocklist
- phoenix_kit_email_templates
- phoenix_kit_email_orphaned_events
- phoenix_kit_email_metrics
### OAuth (V16)
- phoenix_kit_user_oauth_providers
### Entities (V17)
- phoenix_kit_entities
- phoenix_kit_entity_data
### Audit (V22)
- phoenix_kit_audit_logs
### Billing (V31, V33)
- phoenix_kit_currencies
- phoenix_kit_billing_profiles
- phoenix_kit_orders
- phoenix_kit_invoices
- phoenix_kit_transactions
- phoenix_kit_payment_methods
- phoenix_kit_subscription_plans
- phoenix_kit_subscriptions
- phoenix_kit_payment_provider_configs
- phoenix_kit_webhook_events
### AI System (V32, V34, V38)
- phoenix_kit_ai_endpoints
- phoenix_kit_ai_requests
- phoenix_kit_ai_prompts
### DB Sync (V37)
- phoenix_kit_db_sync_connections
- phoenix_kit_db_sync_transfers
### Admin Notes (V39)
- phoenix_kit_admin_notes
## Performance Considerations
- Uses batched updates for large tables to avoid long locks
- Checks table existence before migration (for optional modules)
- UUIDv7 provides better index performance than random UUIDs
## Usage
# Migrate up
PhoenixKit.Migrations.Postgres.up(prefix: "public", version: 40)
# Rollback
PhoenixKit.Migrations.Postgres.down(prefix: "public", version: 39)
"""
use Ecto.Migration
@tables_to_migrate [
# Core Auth (V01)
:phoenix_kit_users,
:phoenix_kit_users_tokens,
:phoenix_kit_user_roles,
:phoenix_kit_user_role_assignments,
# Settings & Referrals (V03-V04)
:phoenix_kit_settings,
:phoenix_kit_referral_codes,
:phoenix_kit_referral_code_usage,
# Email System (V07, V09, V15, V22)
:phoenix_kit_email_logs,
:phoenix_kit_email_events,
:phoenix_kit_email_blocklist,
:phoenix_kit_email_templates,
:phoenix_kit_email_orphaned_events,
:phoenix_kit_email_metrics,
# OAuth (V16)
:phoenix_kit_user_oauth_providers,
# Entities (V17)
:phoenix_kit_entities,
:phoenix_kit_entity_data,
# Audit (V22)
:phoenix_kit_audit_logs,
# Billing (V31, V33)
:phoenix_kit_currencies,
:phoenix_kit_billing_profiles,
:phoenix_kit_orders,
:phoenix_kit_invoices,
:phoenix_kit_transactions,
:phoenix_kit_payment_methods,
:phoenix_kit_subscription_plans,
:phoenix_kit_subscriptions,
:phoenix_kit_payment_provider_configs,
:phoenix_kit_webhook_events,
# AI System (V32, V34, V38)
:phoenix_kit_ai_endpoints,
:phoenix_kit_ai_requests,
:phoenix_kit_ai_prompts,
# DB Sync (V37)
:phoenix_kit_db_sync_connections,
:phoenix_kit_db_sync_transfers,
# Admin Notes (V39)
:phoenix_kit_admin_notes
]
# Tables that may have large amounts of data and need batched updates
@large_tables [
:phoenix_kit_users,
:phoenix_kit_users_tokens,
:phoenix_kit_email_logs,
:phoenix_kit_email_events,
:phoenix_kit_audit_logs,
:phoenix_kit_entity_data,
:phoenix_kit_ai_requests
]
# Batch size for large table updates
@batch_size 10_000
@doc """
Run the V40 migration to add UUID columns to all legacy tables.
"""
def up(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Step 1: Ensure pgcrypto extension exists
execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
# Step 2: Create UUIDv7 generation function if it doesn't exist
# UUIDv7 format: timestamp (48 bits) + version (4 bits) + random (12 bits) + variant (2 bits) + random (62 bits)
execute("""
CREATE OR REPLACE FUNCTION uuid_generate_v7()
RETURNS uuid AS $$
DECLARE
unix_ts_ms bytea;
uuid_bytes bytea;
BEGIN
-- Get current timestamp in milliseconds
unix_ts_ms := substring(int8send(floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint) FROM 3);
-- Build UUIDv7: 6 bytes timestamp + 2 bytes random (with version) + 8 bytes random (with variant)
uuid_bytes := unix_ts_ms || gen_random_bytes(10);
-- Set version 7 (0111xxxx in byte 7)
uuid_bytes := set_byte(uuid_bytes, 6, (get_byte(uuid_bytes, 6) & 15) | 112);
-- Set variant (10xxxxxx in byte 9)
uuid_bytes := set_byte(uuid_bytes, 8, (get_byte(uuid_bytes, 8) & 63) | 128);
RETURN encode(uuid_bytes, 'hex')::uuid;
END
$$ LANGUAGE plpgsql VOLATILE;
""")
# Step 3: Process each table
for table <- @tables_to_migrate do
add_uuid_column_to_table(table, prefix, escaped_prefix)
end
# Step 4: Update version comment
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '40'")
end
@doc """
Rollback the V40 migration by removing UUID columns.
"""
def down(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Remove UUID columns from all tables (in reverse order)
for table <- Enum.reverse(@tables_to_migrate) do
remove_uuid_column_from_table(table, prefix, escaped_prefix)
end
# Update version comment
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '39'")
end
# Add UUID column to a single table with all safety checks
defp add_uuid_column_to_table(table, prefix, escaped_prefix) do
table_name = prefix_table_name(Atom.to_string(table), prefix)
# Check if table exists (for optional modules that might not be enabled)
if table_exists?(table, escaped_prefix) do
# Check if uuid column already exists (idempotency)
unless column_exists?(table, :uuid, escaped_prefix) do
# Step 1: Add UUID column with UUIDv7 default
# The default ensures database-level inserts work correctly.
# Ecto changesets will generate UUIDv7 in Elixir, which takes precedence.
execute("""
ALTER TABLE #{table_name}
ADD COLUMN uuid UUID DEFAULT uuid_generate_v7()
""")
# Step 2: Backfill existing records with UUIDv7
# Use batched updates for large tables to avoid long locks
if table in @large_tables do
backfill_uuids_batched(table_name)
else
# Small tables can be updated in one go
execute("""
UPDATE #{table_name}
SET uuid = uuid_generate_v7()
WHERE uuid IS NULL
""")
end
# Step 3: Create unique index
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS #{table}_uuid_idx
ON #{table_name}(uuid)
""")
# Step 4: Set NOT NULL constraint after backfill
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid SET NOT NULL
""")
# Note: We KEEP the DEFAULT so database-level inserts continue to work.
# This is important for backward compatibility with code that doesn't
# explicitly set the uuid field. Ecto changesets will generate UUIDv7
# in Elixir, which takes precedence over the default.
end
end
end
# Backfill UUIDs in batches to avoid long locks on large tables
defp backfill_uuids_batched(table_name) do
# Use a loop to update in batches
# This is done via a DO block to handle it in a single migration step
execute("""
DO $$
DECLARE
batch_count INTEGER := 0;
updated_rows INTEGER;
BEGIN
LOOP
UPDATE #{table_name}
SET uuid = uuid_generate_v7()
WHERE id IN (
SELECT id FROM #{table_name}
WHERE uuid IS NULL
LIMIT #{@batch_size}
);
GET DIAGNOSTICS updated_rows = ROW_COUNT;
batch_count := batch_count + 1;
-- Exit when no more rows to update
EXIT WHEN updated_rows = 0;
-- Small delay between batches to reduce lock contention
PERFORM pg_sleep(0.01);
END LOOP;
END $$;
""")
end
# Remove UUID column from a single table
defp remove_uuid_column_from_table(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 (handle nil prefix properly)
index_name = prefix_index_name(table, prefix)
execute("""
DROP INDEX IF EXISTS #{index_name}
""")
# Drop column
execute("""
ALTER TABLE #{table_name}
DROP COLUMN IF EXISTS uuid
""")
end
end
# Helper to build prefixed index name, handling nil prefix
defp prefix_index_name(table, nil), do: "#{table}_uuid_idx"
defp prefix_index_name(table, "public"), do: "public.#{table}_uuid_idx"
defp prefix_index_name(table, prefix), do: "#{prefix}.#{table}_uuid_idx"
# Check if a table exists in the database
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
# Check if a column exists in a table
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
# Helper to build prefixed table name
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}"
end