Packages

phoenix_kit

1.7.103
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 v70.ex
Raw

lib/phoenix_kit/migrations/postgres/v70.ex

defmodule PhoenixKit.Migrations.Postgres.V70 do
@moduledoc """
V70: Re-backfill UUID FK columns silently skipped in V56/V63.
## Background
V56 introduced UUID FK companion columns (via UUIDFKColumns) with a backfill
that populated each FK column from the corresponding source table's `uuid`
column. Two bugs in V56/V63 could cause the backfill to be skipped silently:
1. **Type mismatch** — On databases where `phoenix_kit_email_logs.uuid` was
created as `character varying` instead of the native PostgreSQL `uuid` type
(because a manual migration pre-empted V40's proper ADD COLUMN), the
backfill UPDATE fails with `ERROR 42804 datatype_mismatch`.
2. **Broken rescue** — V56's `backfill_uuid_fk` had a `rescue _ -> :ok` clause
intended to swallow the error. However, a failed PostgreSQL statement puts
the connection's transaction in an aborted state (ERROR 25P02), so all
subsequent execute/1 calls fail even though Elixir caught the exception.
The first error (type mismatch) is reported; the migration rolls back; the
DB stays at v55. Alternatively, if the migration somehow succeeded, the
backfill was still skipped and `email_log_uuid` rows were filled with
random UUIDs by `UUIDFKColumns.add_constraints/1`'s NULL-fill fallback.
V63 had the same issue for `matched_email_log_uuid` without even a rescue.
## What This Migration Does
1. Converts `phoenix_kit_email_logs.uuid` to native `uuid` type if still
`character varying` (root-cause fix).
2. Re-backfills `email_log_uuid` in `phoenix_kit_email_events`:
- Rows whose `email_log_uuid` does NOT reference a real email log uuid
(random UUID written by the NULL-fill fallback) are reset to NULL first.
- Then the proper JOIN-based backfill is re-run.
3. Re-backfills `matched_email_log_uuid` in `phoenix_kit_email_orphaned_events`
(same pattern as above).
All operations are idempotent — safe on every install, including fresh ones
where the columns were backfilled correctly from the start.
"""
use Ecto.Migration
def up(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
flush()
# Step 1: Ensure phoenix_kit_email_logs.uuid is native uuid type.
fix_email_logs_uuid_type(prefix, escaped_prefix)
# Flush so the type change is visible in the same transaction.
flush()
# Step 2: Re-backfill email_log_uuid in email_events.
rebackfill_email_log_uuid(prefix, escaped_prefix)
# Step 3: Re-backfill matched_email_log_uuid in email_orphaned_events.
rebackfill_matched_email_log_uuid(prefix, escaped_prefix)
execute("COMMENT ON TABLE #{prefix_table("phoenix_kit", prefix)} IS '70'")
end
def down(%{prefix: prefix} = _opts) do
# No structural changes to reverse — backfill data is correct data.
execute("COMMENT ON TABLE #{prefix_table("phoenix_kit", prefix)} IS '69'")
end
# ---------------------------------------------------------------------------
# Step 1: Fix email_logs.uuid column type
# ---------------------------------------------------------------------------
defp fix_email_logs_uuid_type(prefix, escaped_prefix) do
table = "phoenix_kit_email_logs"
if table_exists?(table, escaped_prefix) do
uuid_type_query = """
SELECT data_type
FROM information_schema.columns
WHERE table_name = '#{table}'
AND column_name = 'uuid'
AND table_schema = '#{escaped_prefix}'
"""
case repo().query(uuid_type_query, [], log: false) do
{:ok, %{rows: [[dt]]}} when dt in ["character varying", "text", "character"] ->
table_name = prefix_table(table, prefix)
execute("""
ALTER TABLE #{table_name}
ALTER COLUMN uuid TYPE uuid USING uuid::uuid
""")
_ ->
:ok
end
end
end
# ---------------------------------------------------------------------------
# Step 2: Re-backfill email_log_uuid in email_events
# ---------------------------------------------------------------------------
defp rebackfill_email_log_uuid(prefix, escaped_prefix) do
events_table = "phoenix_kit_email_events"
logs_table = "phoenix_kit_email_logs"
if table_exists?(events_table, escaped_prefix) and
table_exists?(logs_table, escaped_prefix) and
column_exists?(events_table, "email_log_uuid", escaped_prefix) and
column_exists?(logs_table, "uuid", escaped_prefix) do
events = prefix_table(events_table, prefix)
logs = prefix_table(logs_table, prefix)
# Reset rows whose email_log_uuid does not reference any real email log.
# These were filled with random UUIDs by the V56 NULL-fill fallback and
# need to be re-backfilled from the correct JOIN.
execute("""
UPDATE #{events} e
SET email_log_uuid = NULL
WHERE e.email_log_id IS NOT NULL
AND e.email_log_uuid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM #{logs} l WHERE l.uuid = e.email_log_uuid
)
""")
# Re-run the proper JOIN-based backfill (idempotent: WHERE IS NULL).
execute("""
DO $$
DECLARE
batch_count INTEGER;
BEGIN
LOOP
UPDATE #{events} t
SET email_log_uuid = s.uuid
FROM #{logs} s
WHERE s.id = t.email_log_id
AND t.email_log_uuid IS NULL
AND t.email_log_id IS NOT NULL
AND t.ctid IN (
SELECT t2.ctid FROM #{events} t2
WHERE t2.email_log_uuid IS NULL
AND t2.email_log_id IS NOT NULL
LIMIT 10000
);
GET DIAGNOSTICS batch_count = ROW_COUNT;
EXIT WHEN batch_count = 0;
PERFORM pg_sleep(0.01);
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'PhoenixKit V70: email_log_uuid re-backfill skipped — %', SQLERRM;
END $$;
""")
end
end
# ---------------------------------------------------------------------------
# Step 3: Re-backfill matched_email_log_uuid in email_orphaned_events
# ---------------------------------------------------------------------------
defp rebackfill_matched_email_log_uuid(prefix, escaped_prefix) do
orphaned_table = "phoenix_kit_email_orphaned_events"
logs_table = "phoenix_kit_email_logs"
if table_exists?(orphaned_table, escaped_prefix) and
table_exists?(logs_table, escaped_prefix) and
column_exists?(orphaned_table, "matched_email_log_uuid", escaped_prefix) and
column_exists?(logs_table, "uuid", escaped_prefix) do
orphaned = prefix_table(orphaned_table, prefix)
logs = prefix_table(logs_table, prefix)
# Reset orphaned rows whose matched_email_log_uuid references nothing real.
execute("""
UPDATE #{orphaned} e
SET matched_email_log_uuid = NULL
WHERE e.matched_email_log_id IS NOT NULL
AND e.matched_email_log_uuid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM #{logs} l WHERE l.uuid = e.matched_email_log_uuid
)
""")
execute("""
DO $$
BEGIN
UPDATE #{orphaned} e
SET matched_email_log_uuid = l.uuid
FROM #{logs} 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 V70: matched_email_log_uuid re-backfill skipped — %', SQLERRM;
END $$;
""")
end
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
defp table_exists?(table_str, escaped_prefix) do
case repo().query(
"""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = '#{table_str}'
AND table_schema = '#{escaped_prefix}'
)
""",
[],
log: false
) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp column_exists?(table_str, column_str, escaped_prefix) do
case repo().query(
"""
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{table_str}'
AND column_name = '#{column_str}'
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