# `PhoenixKit.Migrations.Postgres.V127`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.165/lib/phoenix_kit/migrations/postgres/v127.ex#L1)

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.

# `down`

# `up`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
