Packages

phoenix_kit

1.7.184
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 v127.ex
Raw

lib/phoenix_kit/migrations/postgres/v127.ex

defmodule PhoenixKit.Migrations.Postgres.V127 do
@moduledoc """
V127: Sub-projects as tasks (`phoenix_kit_project_assignments.child_project_uuid`).
Lets a project be embedded inside another project as one of its task rows.
A sub-project is an `Assignment` that points at a child `Project` instead of
a reusable `Task` template — so it lives in the parent's task timeline and
gets dependencies + drag-reorder for free (both are already assignment-level
and project-scoped). The child project is the single source of truth; the
parent's linking assignment carries denormalized rollup fields (status /
progress_pct / estimated_duration / completed_at) synced by the context layer
whenever the child changes, so every existing read site (schedule math,
`recompute_project_completion`, dashboards, sorting) keeps working unchanged.
Changes to `phoenix_kit_project_assignments`:
* `child_project_uuid UUID` → FK `phoenix_kit_projects(uuid) ON DELETE RESTRICT`.
`RESTRICT` (not `CASCADE`) so a stray child-project delete fails loudly
instead of silently mutating the parent's task list — recursive teardown
is orchestrated explicitly in `PhoenixKitProjects.Projects` inside a
transaction so it can log activity and tear the subtree down in order.
* `task_uuid` loses its `NOT NULL` — a sub-project assignment has no template.
* `CHECK ((task_uuid IS NOT NULL) <> (child_project_uuid IS NOT NULL))` —
exactly one of the two is set (XOR). Existing rows (task set, child NULL)
satisfy it, so the constraint validates against current data without a
backfill.
* Partial UNIQUE index on `(child_project_uuid) WHERE child_project_uuid IS
NOT NULL` — a project is a child of at most one parent assignment. This is
also what forces template cloning to *deep-clone* child subtrees rather
than point two parents at the same child. It also serves the "find the
linking row for this child" lookups (parent breadcrumb, rollup sync): an
equality predicate `child_project_uuid = $1` implies `IS NOT NULL`, so
Postgres uses the partial index for it — no separate plain index needed.
Idempotent: re-running is a no-op once the column/constraints/indexes exist.
"""
use Ecto.Migration
def up(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
schema = schema_for(prefix)
add_child_project_uuid_column(p, schema)
add_child_project_uuid_fk(p, schema)
drop_task_uuid_not_null(p)
add_task_xor_child_check(p, schema)
create_child_project_unique_index(p, schema)
execute("COMMENT ON TABLE #{p}phoenix_kit IS '127'")
end
def down(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_project_assignments_child_project_unique")
execute("""
ALTER TABLE #{p}phoenix_kit_project_assignments
DROP CONSTRAINT IF EXISTS phoenix_kit_project_assignments_task_xor_child
""")
execute("""
ALTER TABLE #{p}phoenix_kit_project_assignments
DROP CONSTRAINT IF EXISTS phoenix_kit_project_assignments_child_project_uuid_fkey
""")
# Drop the sub-project linking rows first — they're exactly the rows that
# would violate the NOT NULL we restore below. Target them directly by
# `child_project_uuid IS NOT NULL` (the column still exists here, dropped
# just after): that's precisely the V127-created set, without leaning on
# the XOR check we dropped two statements ago. The child projects survive
# as standalone projects, but note this also cascade-deletes any
# `phoenix_kit_project_dependencies` edges touching these assignments
# (FK `ON DELETE CASCADE`), so a sub-project's dependency wiring is lost —
# rollback is feature-removal, not a reversible round-trip.
execute(
"DELETE FROM #{p}phoenix_kit_project_assignments WHERE child_project_uuid IS NOT NULL"
)
execute(
"ALTER TABLE #{p}phoenix_kit_project_assignments DROP COLUMN IF EXISTS child_project_uuid"
)
# Restore the pre-V127 NOT NULL on task_uuid. Safe now: the XOR check is
# gone and the only rows that could have had a NULL task_uuid (the child
# links) were just deleted, so every remaining row is a plain
# template-backed assignment with task_uuid set.
execute("ALTER TABLE #{p}phoenix_kit_project_assignments ALTER COLUMN task_uuid SET NOT NULL")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '126'")
end
defp add_child_project_uuid_column(p, schema) do
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_project_assignments'
AND column_name = 'child_project_uuid'
) THEN
ALTER TABLE #{p}phoenix_kit_project_assignments
ADD COLUMN child_project_uuid UUID;
END IF;
END $$;
""")
end
# FK to the embedded child project. `ON DELETE RESTRICT` keeps the parent's
# task list honest: you can't delete a project that is still embedded as a
# sub-project — the context tears the subtree down explicitly instead.
# Postgres has no `ADD CONSTRAINT IF NOT EXISTS`, so guard on the name.
defp add_child_project_uuid_fk(p, schema) do
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.table_constraints
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_project_assignments'
AND constraint_name = 'phoenix_kit_project_assignments_child_project_uuid_fkey'
) THEN
ALTER TABLE #{p}phoenix_kit_project_assignments
ADD CONSTRAINT phoenix_kit_project_assignments_child_project_uuid_fkey
FOREIGN KEY (child_project_uuid)
REFERENCES #{p}phoenix_kit_projects(uuid)
ON DELETE RESTRICT;
END IF;
END $$;
""")
end
# A sub-project assignment has no task template, so task_uuid must be
# nullable. `DROP NOT NULL` is itself idempotent (a no-op when already
# nullable), so no existence guard is needed.
defp drop_task_uuid_not_null(p) do
execute(
"ALTER TABLE #{p}phoenix_kit_project_assignments ALTER COLUMN task_uuid DROP NOT NULL"
)
end
# Exactly one of task_uuid / child_project_uuid is set. `<>` is boolean XOR
# in Postgres. Existing rows (task set, child NULL) already satisfy it.
defp add_task_xor_child_check(p, schema) do
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.table_constraints
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_project_assignments'
AND constraint_name = 'phoenix_kit_project_assignments_task_xor_child'
) THEN
ALTER TABLE #{p}phoenix_kit_project_assignments
ADD CONSTRAINT phoenix_kit_project_assignments_task_xor_child
CHECK ((task_uuid IS NOT NULL) <> (child_project_uuid IS NOT NULL));
END IF;
END $$;
""")
end
# A project can be embedded as a sub-project in at most one parent. Partial
# so the common task-backed rows (child NULL) don't collide.
defp create_child_project_unique_index(p, schema) do
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_indexes
WHERE schemaname = '#{schema}'
AND tablename = 'phoenix_kit_project_assignments'
AND indexname = 'phoenix_kit_project_assignments_child_project_unique'
) THEN
CREATE UNIQUE INDEX phoenix_kit_project_assignments_child_project_unique
ON #{p}phoenix_kit_project_assignments (child_project_uuid)
WHERE child_project_uuid IS NOT NULL;
END IF;
END $$;
""")
end
defp schema_for("public"), do: "public"
defp schema_for(prefix), do: prefix
defp prefix_str("public"), do: "public."
defp prefix_str(prefix), do: "#{prefix}."
end