# `PhoenixKit.Migrations.Repair.ShapeSql`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/phoenix_kit/migrations/repair/shape_sql.ex#L1)

Rebuilds a `CREATE`/structural-probe SQL statement from an
`PhoenixKit.Migrations.ExpectedSchema.Object.shape_at/2` result, for the
classes where the manifest's own precomputed `object.create`/`object.check`
(always the **newest** revision — see `Object`'s moduledoc) is the wrong
thing to execute against an intermediate comment version.

## Why only `:column`/`:index`/`:constraint`

The frozen P1 generator (`PhoenixKit.Squash.Generate.Emitter.object_create/2`,
`dev_docs/squash/generate_baseline.exs`) itself only ever revision-scopes
these three classes — `:extension`/`:function`/`:sequence`/`:table`/`:seed`
creates are built from `newest_shape/1` **unconditionally**, even inside the
generator's own `render_baseline/4` (which slices `since <= floor` and
rebuilds from `shape_at/2` for those three classes but not the other five).
That is a deliberate property of the ground truth, not an oversight to
"fix" here:

  * `:table` creates are always the same empty-shell `CREATE TABLE ... ()`
    regardless of revision (every column is its own separate `:column`
    object — spec §5.1's "every surviving column is its own object").
  * `:extension`/`:function` creates are idempotent, additive,
    revision-blind operations (`ensure_extension!/2`,
    `ensure_uuid_v7_function/2`, or a `CREATE OR REPLACE FUNCTION`) —
    there is no "old shape" a later delta depends on the *absence* of.
  * `:sequence` creates are a one-time declaration; nothing in the chain
    alters a sequence's own increment/min/max/cache/cycle after creation.
  * `:seed` rebuild would need the emission **strategy**
    (`:conflict_key`/`:where_not_exists`/`:admin_role_select`/helper) —
    information the generator deliberately does not carry into the final
    `Object.t()` (it is folded into the precomputed `create` string at
    generation time, per table, not per revision). No real seed object in
    the current manifest actually needs revision-scoped values (the
    generator's baseline seed policy is always `DO NOTHING`/
    `WHERE NOT EXISTS`, never an upgrade-era `DO UPDATE` — spec §8.2/S2),
    so this is a documented, deliberate gap rather than a silent one: if a
    future manifest ever ships a genuinely multi-revision `:seed` object,
    `PhoenixKit.Migrations.Repair.Scope` falls back to the newest-shape
    `create`/`check` for it, exactly like it does for the other four
    revision-blind classes.

Every function takes an already-materialized `prefix` (a real schema name,
never the `"__SCHEMA__"` token) — by the time
`PhoenixKit.Migrations.Repair` has an `Object.t()` in hand, it came from
`manifest.objects(prefix)`, which already substituted every revision's
shape (`Object.materialize/2` walks `:revisions` too, not just the
top-level `:create`/`:check`).

Idioms mirrored exactly from `PhoenixKit.Squash.Generate.Emitter`
(CLAUDE.md's prefix-safety rules): bare index names on `CREATE`, a
name-anchored `pg_constraint` JOIN guard for constraints (never a
`regclass` cast — the V146 25P02 trap), `ADD COLUMN IF NOT EXISTS`.

# `column_create`

```elixir
@spec column_create(String.t(), String.t(), String.t(), map()) :: String.t()
```

`ALTER TABLE <prefix>.<table> ADD COLUMN IF NOT EXISTS "<column>" <type>
[DEFAULT <default>] [NOT NULL]` — mirrors `Emitter.column_def/3`'s call
site inside `object_create/2`'s `:column` clause exactly, including the
NOT-NULL-only-with-a-DEFAULT asymmetry (adding a `NOT NULL` column with no
default to a populated table fails outright, so a repair-created column is
never given one without a value to fall back on).

    iex> ShapeSql.column_create("public", "phoenix_kit_widgets", "name",
    ...>   %{type: "character varying(50)", not_null: true, default: nil})
    ~s|ALTER TABLE public.phoenix_kit_widgets ADD COLUMN IF NOT EXISTS "name" character varying(50)|

    iex> ShapeSql.column_create("public", "phoenix_kit_widgets", "uuid",
    ...>   %{type: "uuid", not_null: true, default: "public.uuid_generate_v7()"})
    ~s|ALTER TABLE public.phoenix_kit_widgets ADD COLUMN IF NOT EXISTS "uuid" uuid DEFAULT public.uuid_generate_v7() NOT NULL|

# `constraint_create`

```elixir
@spec constraint_create(
  String.t(),
  String.t(),
  String.t(),
  %{definition: String.t()},
  keyword()
) :: String.t()
```

Name-anchored, guarded `ADD CONSTRAINT` — mirrors
`Emitter.guarded_constraint_sql/3` exactly: a `pg_constraint` JOIN keyed on
`conname` + `t.relname` + `n.nspname`, never a `'<table>'::regclass` cast
(CLAUDE.md's documented 25P02 trap for immediate checks — this guard is
evaluated inside a queued `DO $$` block via `execute/1` in migration
context, but `PhoenixKit.Migrations.Repair.Executor` runs it as an
immediate statement too; the guard itself never casts a name to
`regclass`, so it stays safe either way).

`opts[:not_valid]` appends ` NOT VALID` — spec §6.3's rule for foreign keys
specifically (`Executor` passes this for `shape.type == "f"`, followed by a
separate `VALIDATE CONSTRAINT` statement; V113 is the in-chain precedent
for the two-step). Never set for any other constraint type — a `NOT VALID`
primary key or unique constraint is not valid Postgres syntax.

    iex> ShapeSql.constraint_create("public", "phoenix_kit_widgets", "phoenix_kit_widgets_pkey",
    ...>   %{definition: "PRIMARY KEY (uuid)"})
    ...> |> String.contains?("ADD CONSTRAINT phoenix_kit_widgets_pkey PRIMARY KEY (uuid);")
    true

# `index_create`

```elixir
@spec index_create(%{definition: String.t()}) :: String.t()
```

Inserts `IF NOT EXISTS` into a captured `pg_get_indexdef` definition —
mirrors `Emitter.index_create_sql/1` exactly (index **names** stay bare on
`CREATE`; only `DROP INDEX` ever takes a schema-qualified name).

    iex> ShapeSql.index_create(%{definition: "CREATE INDEX my_idx ON public.widgets USING btree (owner_uuid)"})
    "CREATE INDEX IF NOT EXISTS my_idx ON public.widgets USING btree (owner_uuid)"

# `validate_constraint`

```elixir
@spec validate_constraint(String.t(), String.t(), String.t()) :: String.t()
```

`ALTER TABLE <prefix>.<table> VALIDATE CONSTRAINT <name>` — the second half
of the `NOT VALID` two-step (V113 precedent: cheap on a live table, only
scans rows written after the `ADD`, not the whole table).

    iex> ShapeSql.validate_constraint("public", "phoenix_kit_widgets", "phoenix_kit_widgets_owner_uuid_fkey")
    "ALTER TABLE public.phoenix_kit_widgets VALIDATE CONSTRAINT phoenix_kit_widgets_owner_uuid_fkey"

---

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