Packages
phoenix_kit
2.0.0
2.13.13
2.13.12
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
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/mix/tasks/phoenix_kit.repair_uuid.ex
defmodule Mix.Tasks.PhoenixKit.RepairUuid do
@shortdoc "Repairs phoenix_kit tables whose uuid column is not a proper primary key"
@moduledoc """
Repairs `phoenix_kit_*` tables whose `uuid` column is the wrong type, nullable,
or not the primary key.
V163 performs this repair automatically during `mix ecto.migrate`, but skips
any table large enough that the rewrite's `ACCESS EXCLUSIVE` lock would be an
outage rather than a pause. This task is the deliberate, operator-chosen path
for those — run it in a maintenance window.
mix phoenix_kit.repair_uuid # every table that needs it
mix phoenix_kit.repair_uuid phoenix_kit_email_events
mix phoenix_kit.repair_uuid --dry-run # show the SQL, change nothing
mix phoenix_kit.repair_uuid --prefix tenant_a
## What it costs
`ALTER COLUMN … TYPE uuid` rewrites the table and holds an `ACCESS EXCLUSIVE`
lock for the duration — no reads, no writes, and behind a connection pooler
that means the pool fills rather than merely waiting. There is no concurrent
form of a type change; the size of the table is the size of the outage.
The primary key is cheaper here than in the migration: outside a transaction
the unique index is built `CONCURRENTLY` and then attached, so the exclusive
lock covers only the attach. That is why this task is not simply "V163 without
the limit".
## Safety
Nothing is destructive except de-duplication, which deletes rows that share a
uuid — on a table that has run without a primary key those are the same
logical row stored twice, and a duplicate makes `ADD PRIMARY KEY` impossible.
`--dry-run` prints every statement, including the delete, without executing.
Values that cannot be cast to `uuid` abort that table with the query to
inspect them; other tables still proceed.
"""
use Mix.Task
alias PhoenixKit.Install.PrefixConfig
alias PhoenixKit.Migrations.UUIDIntegrity
@requirements ["app.start"]
@switches [dry_run: :boolean, prefix: :string]
@impl Mix.Task
def run(args) do
{opts, tables} = OptionParser.parse!(args, strict: @switches)
# Same resolution the doctor and installer use: --prefix > app config > public
prefix = PrefixConfig.resolve_prefix(opts)
dry_run? = opts[:dry_run] || false
repo = PhoenixKit.RepoHelper.repo()
broken = UUIDIntegrity.broken_tables(repo, prefix)
case {filter_requested(broken, tables), tables} do
{[], []} ->
Mix.shell().info([:green, "✓ Every phoenix_kit table has a proper uuid primary key."])
# Named tables that need nothing must NOT report the global all-clear.
# V163's log tells an operator to run this task with a table name, so a
# typo — or a table already repaired, or the wrong --prefix — would
# otherwise answer "everything is fine" and change nothing.
{[], named} ->
Mix.shell().info([
:yellow,
"Nothing to do: none of the named table(s) need repair — #{Enum.join(named, ", ")}.",
:reset,
"\nRun without arguments to list every table that does (prefix: #{prefix})."
])
{found, _} ->
Mix.shell().info("Tables needing repair: #{length(found)}")
Enum.each(found, &repair(repo, prefix, &1, dry_run?))
report(dry_run?)
end
end
defp filter_requested(broken, []), do: broken
defp filter_requested(broken, requested) do
Enum.filter(broken, &(&1.name in requested))
end
defp repair(repo, prefix, %{name: name} = table, dry_run?) do
qualified = UUIDIntegrity.qualify(prefix, name)
if UUIDIntegrity.castable?(repo, qualified, table) do
rows = UUIDIntegrity.estimated_rows(repo, prefix, name)
# `:unknown` is said out loud rather than printed as a number. This task
# is the maintenance-window path the migration defers TO, so the operator
# deciding whether they have a window needs to know the size is a guess
# nobody has ever taken — not read "~0 rows" and size the window for it.
size =
case rows do
:unknown -> "size unknown — never analyzed"
n -> "~#{n} rows"
end
Mix.shell().info([
:cyan,
"\n#{name}",
:reset,
" (#{size}) #{UUIDIntegrity.describe(table)}"
])
announce_duplicates(repo, qualified, table)
# concurrent_index: this task runs outside a transaction, so the unique
# index can be built without holding the table against readers.
qualified
|> UUIDIntegrity.repair_statements(prefix, table, concurrent_index: true)
|> Enum.each(&run_statement(repo, &1, dry_run?))
else
Mix.shell().error("""
#{name}: uuid holds values that are not valid UUIDs — skipped. Inspect with:
SELECT uuid FROM #{qualified}
WHERE uuid IS NOT NULL AND uuid !~* '#{uuid_regex()}' LIMIT 20;
""")
end
end
defp run_statement(_repo, sql, true), do: Mix.shell().info([" [dry-run] ", String.trim(sql)])
defp run_statement(repo, sql, false) do
Mix.shell().info([" ", String.trim(sql)])
repo.query!(sql, [], timeout: :infinity)
end
# The DELETE is the one destructive statement in the list `--dry-run` prints,
# and how many rows it takes is not visible from the SQL itself.
defp announce_duplicates(_repo, _qualified, %{has_pk: true}), do: :ok
defp announce_duplicates(repo, qualified, _table) do
case UUIDIntegrity.duplicate_rows(repo, qualified) do
n when n > 0 ->
Mix.shell().info([:yellow, " #{n} row(s) share a uuid and will be DELETED"])
_ ->
:ok
end
end
defp report(true) do
Mix.shell().info([:yellow, "\nDry run — nothing was changed."])
end
defp report(false) do
Mix.shell().info([:green, "\n✓ Repair complete. Re-run mix phoenix_kit.doctor to confirm."])
end
defp uuid_regex, do: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
end