Packages
phoenix_kit
1.7.201
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/v106.ex
defmodule PhoenixKit.Migrations.Postgres.V106 do
@moduledoc """
V106: Split `phoenix_kit_projects.name` uniqueness across templates and
real projects.
V101 created a single global unique index
(`phoenix_kit_projects_name_index`) on `lower(name)` over the
`phoenix_kit_projects` table. Templates and real projects share that
table — distinguished only by the `is_template` boolean — so they
also shared one name namespace, which made
`Projects.create_project_from_template/2` collide whenever a real
project should reuse the source template's name (the common,
expected case).
This migration replaces that single index with two partial unique
indexes — one per `is_template` value — so a template "Onboarding"
and a real project "Onboarding" can coexist freely.
## Indexes
- `phoenix_kit_projects_name_template_index`
`UNIQUE (lower(name)) WHERE is_template = true`
- `phoenix_kit_projects_name_project_index`
`UNIQUE (lower(name)) WHERE is_template = false`
Both are idempotent (`CREATE INDEX IF NOT EXISTS` / `DROP INDEX IF
EXISTS`).
## Schema-side change only — changeset half lives downstream
Core owns the SQL. The matching `unique_constraint(...)` swap on
the `Project` schema lives in the **downstream `phoenix_kit_projects`
package**, not in this repo (no `phoenix_kit_projects` schema
exists under `lib/` here — `rg phoenix_kit_projects_name_template_index
lib/` will only return this migration).
The downstream changeset picks
`:phoenix_kit_projects_name_template_index` vs
`:phoenix_kit_projects_name_project_index` based on the
`is_template` field at validate time, so unique-name violations
surface as a clean `name has already been taken` form error
instead of a generic FK error path. Without the changeset half
shipped alongside this migration, the V106 split would be
invisible to end users — they'd still see the legacy single
constraint name in error tuples.
"""
use Ecto.Migration
def up(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_projects_name_index")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS phoenix_kit_projects_name_template_index
ON #{p}phoenix_kit_projects (lower(name))
WHERE is_template = true
""")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS phoenix_kit_projects_name_project_index
ON #{p}phoenix_kit_projects (lower(name))
WHERE is_template = false
""")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '106'")
end
@doc """
Reverts to V101's single global unique index.
**Lossy rollback:** if the post-V106 state has both a template and a
real project with the same name, recreating the single
`phoenix_kit_projects_name_index` will fail with a uniqueness
violation. Resolve duplicates before rolling back in production.
The down step pre-checks for cross-mode duplicates and raises an
actionable message (naming one offending row) BEFORE dropping the
partial indexes. Without the pre-check, operators would hit a
generic Postgres `duplicate key value violates unique constraint`
during the `CREATE UNIQUE INDEX` step — same end result but the
error message wouldn't tell them which name to resolve, and by then
the partial indexes have already been dropped, leaving the table
with no name uniqueness at all.
"""
def down(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
# Pre-check: find a `lower(name)` that exists across both
# template and project rows. The partial indexes V106 added each
# cover one bucket, so they don't collide today; the global
# index we're about to recreate would.
case repo().query!(
"SELECT lower(name) FROM #{p}phoenix_kit_projects " <>
"GROUP BY lower(name) HAVING count(*) > 1 LIMIT 1",
[],
log: false
) do
%{rows: []} ->
:ok
%{rows: [[duplicate_name]]} ->
raise """
Cannot roll back V106: name #{inspect(duplicate_name)} exists \
in both `phoenix_kit_projects` rows that V106's split allowed \
to coexist (template + real project, or two of either kind \
sharing a name). Resolve the duplicate before rolling back \
— either delete one of the rows or rename it. The down step \
recreates a single global UNIQUE index on (lower(name)) which \
these duplicates would violate.\
"""
end
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_projects_name_project_index")
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_projects_name_template_index")
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS phoenix_kit_projects_name_index
ON #{p}phoenix_kit_projects (lower(name))
""")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '105'")
end
defp prefix_str("public"), do: "public."
defp prefix_str(prefix), do: "#{prefix}."
end