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_indexUNIQUE (lower(name)) WHERE is_template = truephoenix_kit_projects_name_project_indexUNIQUE (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.
Summary
Functions
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.