Packages
phoenix_kit
1.7.154
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
Current section
Files
lib/phoenix_kit/migrations/postgres/v61.ex
defmodule PhoenixKit.Migrations.Postgres.V61 do
@moduledoc """
V61: UUID Column Safety Net for Tables Missed by V40
## Root Cause
V40 adds a `uuid` column to 33 legacy tables, but uses `repo().query()`
for its `table_exists?` checks. In Ecto migrations, `repo().query()` is
executed immediately against the database, while migration commands
(`execute`, `create`, `alter`) are **buffered** until `flush()` or
callback return.
During a fresh install (V01→V49 in one Ecto migration), V31 calls
`flush()` — making V01–V31 tables visible — but V32–V39 tables remain
buffered when V40 runs. V40 therefore skips all V32+ tables.
V56 added a safety net for 6 of these tables:
- consent_logs, payment_methods, ai_endpoints, ai_prompts,
sync_connections, subscription_plans
This migration covers the remaining 6 tables that V40 missed and V56
did not catch:
## Tables Fixed
| Table | Created in | Why missed |
|-------|-----------|------------|
| `phoenix_kit_admin_notes` | V39 | After V31 flush, before V40 |
| `phoenix_kit_ai_requests` | V32 | After V31 flush, before V40 |
| `phoenix_kit_subscriptions` | V33 | After V31 flush, before V40 |
| `phoenix_kit_payment_provider_configs` | V33 | After V31 flush, before V40 |
| `phoenix_kit_webhook_events` | V33 | After V31 flush, before V40 |
| `phoenix_kit_sync_transfers` | V37/V44 | After V31 flush, before V40 |
## Additional Fix
Adds `created_by_uuid` FK column to `phoenix_kit_scheduled_jobs` (V42).
This table uses a UUID native PK so it doesn't need a `uuid` identity
column, but its `created_by_id` integer FK was missing its UUID companion.
## Permanent Fix
V40 has been updated with `flush()` at the start of `up()` so this
issue cannot recur on new installations.
All operations are idempotent — safe to run on any installation.
"""
use Ecto.Migration
@tables_missing_uuid [
:phoenix_kit_admin_notes,
:phoenix_kit_ai_requests,
:phoenix_kit_subscriptions,
:phoenix_kit_payment_provider_configs,
:phoenix_kit_webhook_events,
:phoenix_kit_sync_transfers
]
def up(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Flush any pending commands from earlier versions
flush()
# Ensure uuid_generate_v7() 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;
""")
# Need to flush uuid_generate_v7() creation before using it
flush()
# Add uuid column to each missing table
for table <- @tables_missing_uuid do
add_uuid_column(table, prefix, escaped_prefix)
end
# Add created_by_uuid FK to scheduled_jobs
add_created_by_uuid_to_scheduled_jobs(prefix, escaped_prefix)
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '61'")
end
def down(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
# Remove created_by_uuid from scheduled_jobs
remove_created_by_uuid_from_scheduled_jobs(prefix, escaped_prefix)
# Remove uuid columns (in reverse order)
for table <- Enum.reverse(@tables_missing_uuid) do
remove_uuid_column(table, prefix, escaped_prefix)
end
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '60'")
end
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) do
unless column_exists?(table, :uuid, escaped_prefix) do
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 #{table}_uuid_idx
ON #{table_name}(uuid)
""")
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid SET NOT NULL
""")
end
end
end
defp remove_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
index_name = prefix_index_name(table, prefix)
execute("DROP INDEX IF EXISTS #{index_name}")
execute("""
ALTER TABLE #{table_name}
DROP COLUMN IF EXISTS uuid
""")
end
end
defp add_created_by_uuid_to_scheduled_jobs(prefix, escaped_prefix) do
table = :phoenix_kit_scheduled_jobs
if table_exists?(table, escaped_prefix) do
unless column_exists?(table, :created_by_uuid, escaped_prefix) do
table_name = prefix_table_name("phoenix_kit_scheduled_jobs", prefix)
execute("""
ALTER TABLE #{table_name}
ADD COLUMN created_by_uuid UUID
""")
# Backfill from users table
users_table = prefix_table_name("phoenix_kit_users", prefix)
execute("""
UPDATE #{table_name} t
SET created_by_uuid = u.uuid
FROM #{users_table} u
WHERE t.created_by_id = u.id
AND t.created_by_uuid IS NULL
AND t.created_by_id IS NOT NULL
""")
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_scheduled_jobs_created_by_uuid_idx
ON #{table_name}(created_by_uuid)
""")
end
end
end
defp remove_created_by_uuid_from_scheduled_jobs(prefix, escaped_prefix) do
table = :phoenix_kit_scheduled_jobs
if table_exists?(table, escaped_prefix) and
column_exists?(table, :created_by_uuid, escaped_prefix) do
table_name = prefix_table_name("phoenix_kit_scheduled_jobs", prefix)
execute("""
DROP INDEX IF EXISTS phoenix_kit_scheduled_jobs_created_by_uuid_idx
""")
execute("""
ALTER TABLE #{table_name}
DROP COLUMN IF EXISTS created_by_uuid
""")
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