# `PhoenixKitCatalogue.Migrations`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.44.2/lib/phoenix_kit_catalogue/migrations.ex#L1)

Module-owned versioned migrations for `phoenix_kit_catalogue` — the
decentralized-migrations protocol that core's `mix phoenix_kit.update`
discovers via `migration_module/0`: `current_version/0` +
`migrated_version_runtime/1` + idempotent `up/1` + version-aware
`down/1`. `phoenix_kit_crm` (ten adopted tables) is the closest sibling
example of this exact situation, scaled up further to eighteen tables.

## What V01 is

V01 is an ADOPTION step for all eighteen `phoenix_kit_cat_*` tables that
core already creates (V135: catalogues, categories, items,
manufacturers, manufacturer_suppliers, suppliers, folders,
item_catalogue_rules, pdfs, pdf_pages, pdf_page_contents,
pdf_extractions; V149: item_supplier_info; V173: attribute_groups,
attributes, attribute_values, item_attribute_groups; V177:
item_attribute_sets).

Those four versions only CREATE the tables; five more reshape them, and
the DDL below is the sum of all nine — check every one of them when
auditing a table's shape, not just its creator:

  * V146 adds `items.primary_supplier_uuid` (+ its partial index and FK);
  * V151 adds `item_supplier_info.supplier_source` / `is_primary`
    (+ the source CHECK and the one-primary-per-item partial unique);
  * V178 adds `manufacturers.crm_company_uuid` (+ the partial unique
    indexes on both directories' `crm_company_uuid`);
  * V179 adds `items.manufacturer_source` /
    `manufacturer_name_snapshot` (+ the source CHECK) and **DROPS**
    `phoenix_kit_cat_items_manufacturer_uuid_fkey`;
  * V180 adds `manufacturer_suppliers.manufacturer_source` /
    `supplier_source` (+ both source CHECKs and the current-pair unique
    on `item_supplier_info`) and **DROPS** both
    `phoenix_kit_cat_manufacturer_suppliers_*_uuid_fkey` constraints.

The three dropped foreign keys are the reason `foreign_keys/2` does not
simply mirror V135: core carries them as `:legacy_optional` in
`ExpectedSchema` — present only on installs that stopped before V179/
V180 — so re-adding one here would pin an item's manufacturer and both
sides of the M:N graph back to a local row, undoing the CRM federation.
`test/phoenix_kit_catalogue/migrations_test.exs` pins that against the
manifest in both directions.

So, concretely:

  * on an install already at core's V180+ shape (crm_company_uuid,
    manufacturer_source/supplier_source, the item/manufacturer_suppliers
    FKs V179–V180 drop) every table is already there, the `CREATE TABLE
    IF NOT EXISTS` / guarded `DO $$ ... pg_constraint ... $$` blocks all
    find their targets already in place and are no-ops — the only new
    object is the `pkc_schema:1` marker. From then on this chain owns
    every adopted table's future shape;
  * on a hypothetical fresh install whose core baseline no longer
    creates these tables, the same statements create them —
    shape-identical to core's live V182 schema (authority: a
    `pg_dump --schema-only` of the live database), with core's exact
    table/constraint/index names.

`CREATE TABLE IF NOT EXISTS` adoption is a presence check only — it
does not repair a table whose columns have drifted from core's current
shape on a host stuck below core V180 (the guarded `ADD CONSTRAINT`
blocks below DO repair a missing constraint on an existing table, but
they cannot add a missing *column* like `crm_company_uuid` or
`manufacturer_source`). That risk is contained, not eliminated: core's
own migration chain always runs ahead of this one (`mix
phoenix_kit.update` applies core's chain first), so by the time V01
runs, every adopted table is already at core's current shape on any
host this chain actually executes against — the `pk_dep(:phoenix_kit, …)`
floor in `mix.exs` (never below 2.13.11) exists precisely so a host can
always reach that shape (see the comment there for why 2.13.4–2.13.10, which
ship the shape, are excluded: V180 itself crashes on those releases).

Because V01 changes no shape, core's `ExpectedSchema` manifest (which
still audits these tables' shapes) stays accurate and no core release
is required for this version. A version that DOES change shape (V2+)
must follow the excluded-object protocol described in the Legal
chain's extraction report before it ships.

## What V2 is

V2 adds shape on top of two core-known tables (`phoenix_kit_cat_items`,
`phoenix_kit_cat_categories`): a `slug jsonb NOT NULL DEFAULT '{}'`
column on each. Core's `ExpectedSchema` resolver only ever iterates
the manifest's *declared* objects — it never enumerates a table's
actual columns to notice one that isn't manifested, so an extra
column is not inspected at all (not classified as an `:info`
finding; simply outside what the resolver looks at) — so this is
safe without a core release. The rest of V2 (the two
`phoenix_kit_cat_item_slugs` / `phoenix_kit_cat_category_slugs`
projection tables, their sync triggers, and the attribute-set GIN
index) creates objects core's manifest never names at all, which is
outside the manifest's reach by construction. Contrast the case that
WOULD require a core release first: altering the shape of a table
column core's manifest actually declares — see
`phoenix_kit_legal`'s `dev_docs/reports/2026-08-10-consent-logs-extraction.md`
for that scenario.

The two projection tables exist so a slug can be looked up (and its
per-language uniqueness enforced) without scanning every item/category
row's `slug` jsonb — the same shape core's own `ShopSlugProjection`
used before catalogue absorbed the shop tables. A trigger
(`AFTER INSERT OR UPDATE OF slug`) keeps each projection in sync with
its owning table; the projection's own `DELETE FROM <projection>` at
the top of the sync function is the one DELETE this chain is allowed
to emit (it deletes only projection rows it is about to
re-insert — never a base table row).

## What `down/1` is NOT

`down/1` unstamps the version marker; it NEVER drops any of the
eighteen tables — catalogue, category, item, and supplier data is not
this chain's to destroy, and most of it is core-created. The ownership
test pins this by asserting no statement this module can emit matches
`DROP`, `TRUNCATE`, or `DELETE`.

The migrated version is tracked as a `pkc_schema:<N>` `COMMENT ON
TABLE` marker on `phoenix_kit_cat_catalogues` (the marker convention
from the projects/Legal/CRM chains). A marker-less table reads as
version 0 — the core-baseline shape before this chain existed.

# `current_version`

```elixir
@spec current_version() :: pos_integer()
```

# `down`

Rolls back to `target` (`:version` in `opts`). Never drops a table — see the moduledoc.

# `down_statements`

```elixir
@spec down_statements(String.t(), non_neg_integer()) :: [String.t()]
```

The SQL `down/1` executes, as data (marker bookkeeping only).

# `migrated_version`

The chain version read from INSIDE a migration (migration repo).

# `migrated_version_runtime`

The chain version currently applied in the database, read OUTSIDE a
migration (the protocol shape core's update task calls — `opts` with
`:prefix`): the `pkc_schema:<N>` marker when present; a marker-less or
foreign-comment table reads as `0` (core-baseline shape — V1 is purely
adoptive, there is no pre-chain content to defend).

# `up`

Applies every chain version up to `target` (`:version` in `opts`, default `current_version/0`); idempotent.

# `up_statements`

```elixir
@spec up_statements(String.t(), pos_integer()) :: [String.t()]
```

The SQL `up/1` executes, as data — the testable single source. Every
statement is idempotent (`IF NOT EXISTS` / guarded `DO $$` block /
`CREATE OR REPLACE FUNCTION` / `DROP TRIGGER IF EXISTS` + `CREATE
TRIGGER` / `COMMENT`) so it is safe to replay on an install where
core already created some or all of these tables, and on a fresh
install with none of them. V1 order: every `CREATE TABLE`, then every
primary-key guard, then every foreign-key guard, then every
CHECK-constraint guard, then every index (table declaration order
only matters for self-documentation — FK targets are guarded, not
required to pre-exist at CREATE TABLE time; the 13 CHECK constraints
also ride inline inside their `CREATE TABLE IF NOT EXISTS`, harmless
since Postgres no-ops the whole statement on an existing table — the
guards are what actually repair a pre-existing table missing one).
V2 statements follow, then the version marker last.

`target` selects how much of the chain to emit (default
`current_version/0`): `1` is the pure V1 adoption step (the owned
tables/keys/checks/indexes below); `2` additionally adds V2's
per-language `slug` column, its trigger projections, and the
attribute-set GIN index. Mirrors `phoenix_kit_billing`'s version-aware
`up_statements/2` — the wrapper migration core's update task generates
calls this with an explicit `:version`, and a stale wrapper asking for
`1` must not receive `2`'s objects.

# `version_table`

```elixir
@spec version_table() :: String.t()
```

The table carrying the `pkc_schema:<N>` marker (auditor contract).

---

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