PhoenixKit.Migrations.Repair.Executor (phoenix_kit v2.2.0)

Copy Markdown View Source

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

outcome()

@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

create(repo, resolved, prefix, skip_validate? \\ false)

@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).

create_action(map, prefix)

@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)