Turns one PhoenixKit.Migrations.Repair.Scope.resolved() into the SQL to
run, and (in real repair mode) runs it — additive-only, statement-at-a-time,
autocommit, spec §6.2/§6.3.
Two halves, deliberately split
create_action/2 is pure — no repo argument, cannot touch a
database — and is the whole "what SQL would this be" answer:
PhoenixKit.Migrations.Repair.verify/1 (and a dry-run .repair/1) call
only this half, so --dry-run genuinely cannot write anything by
construction, not by discipline. test/phoenix_kit/migrations/repair/executor_test.exs
asserts exact statement text against it — the "SQL-generation assertions
without executing" the plan calls for. create/3 is the executing half;
it has no unit test (no database in this suite).
Additive-only rules this module enforces (spec §6.2)
Never DROP, ALTER TYPE, SET/DROP NOT NULL on a pre-existing column,
UPDATE/DELETE user data, rename, or touch anything outside
phoenix_kit* — none of those verbs appear anywhere below. The one
UPDATE this module ever issues (backfill/4) only ever targets WHERE "<col>" IS NULL on a column this same call just added in the
immediately preceding statement (spec D7) — never a pre-existing column,
never rows that already have a value.
FK NOT VALID + VALIDATE (spec §6.3)
For every foreign key (shape.type == "f"), create_action/2 always adds
NOT VALID to the ADD CONSTRAINT — regardless of whether the manifest's
own precomputed object.create would have (it never does; V113 is the
in-chain precedent for the two-step). create/3 follows a successful
ADD with a separate VALIDATE CONSTRAINT statement; a validation
failure leaves the constraint NOT VALID (per spec — never dropped, never
retried) and reports an orphan-row count via
PhoenixKit.Migrations.Repair.Probe.orphan_count/6.
Summary
Types
What happened (or, from create_action/2 alone, would be attempted).
Functions
Runs create_action/2 for real: executes a SQL string immediately
(repo.query!/3, autocommit), invokes a {:helper, mfa}, or reports
:already_present for nil. Column creates backfill their own default on
success (spec D7); FK constraint creates follow with VALIDATE CONSTRAINT
on success — unless skip_validate? is true (§6.3's --unsafe-pooled:
the constraint is added NOT VALID and left there, deliberately, without
ever attempting the VALIDATE statement against a connection that cannot
safely hold the advisory lock either).
The create action for one resolved object — nil (nothing to create;
:legacy_optional), a SQL string, or {:helper, {mod, fun, args}}. Pure.
Types
@type outcome() :: :created | :already_present | :best_effort_skipped | {:create_failed, String.t()} | {:fk_validation_failed, String.t()}
What happened (or, from create_action/2 alone, would be attempted).
Functions
@spec create( Ecto.Repo.t(), PhoenixKit.Migrations.Repair.Scope.resolved(), prefix :: String.t(), skip_validate? :: boolean() ) :: outcome()
Runs create_action/2 for real: executes a SQL string immediately
(repo.query!/3, autocommit), invokes a {:helper, mfa}, or reports
:already_present for nil. Column creates backfill their own default on
success (spec D7); FK constraint creates follow with VALIDATE CONSTRAINT
on success — unless skip_validate? is true (§6.3's --unsafe-pooled:
the constraint is added NOT VALID and left there, deliberately, without
ever attempting the VALIDATE statement against a connection that cannot
safely hold the advisory lock either).
@spec create_action( PhoenixKit.Migrations.Repair.Scope.resolved(), prefix :: String.t() ) :: nil | String.t() | {:helper, {module(), atom(), [term()]}}
The create action for one resolved object — nil (nothing to create;
:legacy_optional), a SQL string, or {:helper, {mod, fun, args}}. Pure.
Dispatches on class exactly as PhoenixKit.Migrations.Repair.ShapeSql's
moduledoc describes: :column/:index/:constraint are always rebuilt
from resolved.shape (never resolved.object.create, which is the
newest-revision precomputed field and NOT VALID-less for every FK); every
other class uses the object's own precomputed create (revision-blind by
design, matching the generator).
iex> resolved = %{
...> object: %{class: :column, check: {:catalog, %{kind: :column, table: "widgets", column: "name"}}},
...> shape: %{type: "text", not_null: false, default: nil}
...> }
iex> Executor.create_action(resolved, "public")
~s(ALTER TABLE public.widgets ADD COLUMN IF NOT EXISTS "name" text)