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

Copy Markdown View Source

Spec §6.1's scope rule as pure functions: which manifest objects/invariants a given comment version puts "in scope", and at which revision. Everything here is DB-free.

The scope rule, restated

repair applies manifest objects with since <= comment only, each at the newest revision with as_of_version <= comment (never a future shape); objects with since > comment are pending — reported, never pre-applied.

resolve/2 gets the second half right for every class by always selecting through PhoenixKit.Migrations.ExpectedSchema.Object.shape_at(object, bound)never object.check/object.create, which are precomputed against the newest revision (see Object's moduledoc for why that distinction matters: a healthy DB whose comment sits between two revisions must verify clean against the comment-era shape, and any create for a missing object must be built from that same shape). Turning a resolved %{object:, shape:} pair into actual SQL is PhoenixKit.Migrations.Repair.Executor.create_action/2's job, not this module's — shape_at/2's result is already everything a per-class SQL builder needs (see that module's moduledoc for why every class except :column/:index/:constraint ends up using the object's own newest-shape create regardless of which revision shape selected).

Summary

Types

One in-scope object, resolved to the shape bound selects.

Functions

The additive-safety class order (spec §6.1/§6.2): extensions < functions < sequences < tables < columns < indexes < constraints < seeds.

Secondary tier within class: :constraint: foreign keys (shape.type == "f") sort AFTER every non-FK constraint (PK/UNIQUE/CHECK), regardless of since/id.

Sorts resolved objects for execution: class order first (see class_rank/1), then since, then non-FK-before-FK (see constraint_fk_rank/2), then object.id — a stable, deterministic order independent of the manifest's own emission order (which sorts {since, class, fk_rank, id} for diff-friendliness; this sorts for execution safety — class always dominates, since a column must never be attempted before its table regardless of which version introduced each).

Selects the data invariants in scope for bound (invariant.since <= bound — the same half of the scope rule as partition/2, for PhoenixKit.Migrations.ExpectedSchema.DataInvariant.t() instead of Object.t()).

Partitions objects by object.since <= bound — the scope rule's first half. Order within each list is preserved from the input.

Resolves one in-scope object to the shape bound selects (the scope rule's second half — see moduledoc). nil for an object whose since > bound — callers are expected to have already filtered via partition/2; this guard exists so a misuse fails as "nothing to resolve" rather than resolving against a shape that does not exist yet.

Types

resolved()

One in-scope object, resolved to the shape bound selects.

Functions

class_rank(class)

The additive-safety class order (spec §6.1/§6.2): extensions < functions < sequences < tables < columns < indexes < constraints < seeds.

constraint_fk_rank(arg1, arg2)

Secondary tier within class: :constraint: foreign keys (shape.type == "f") sort AFTER every non-FK constraint (PK/UNIQUE/CHECK), regardless of since/id.

class_rank/1 alone guarantees a table's own columns exist before any constraint on it, but says nothing about constraints ordered against EACH OTHER — a FK must never be attempted before the PK/UNIQUE it references. In practice since(FK) >= since(PK) always holds for a real historical chain (Postgres itself refuses to create a FK before the referenced column has a unique constraint, so the original chain could never have recorded it the other way round), so since already orders most pairs correctly — the tier below only has to arbitrate the case since cannot: two constraints tied at the same since (routine post- squash, where most pre-floor objects share the floor's since), where the fallback id tiebreak is pure alphabetical and has no notion of "PK before the FK that needs it" (this exact class of bug reproduced live: phoenix_kit_file_instances_..._fkey sorts before phoenix_kit_files_pkey alphabetically, which is backwards — see dev_docs/squash/generate_baseline.exs's Differ.constraint_fk_rank/2, the sibling fix for the manifest generator's own emission order).

execution_order(resolved_objects)

@spec execution_order([resolved()]) :: [resolved()]

Sorts resolved objects for execution: class order first (see class_rank/1), then since, then non-FK-before-FK (see constraint_fk_rank/2), then object.id — a stable, deterministic order independent of the manifest's own emission order (which sorts {since, class, fk_rank, id} for diff-friendliness; this sorts for execution safety — class always dominates, since a column must never be attempted before its table regardless of which version introduced each).

in_scope_invariants(invariants, bound)

@spec in_scope_invariants([map()], bound :: pos_integer()) :: [map()]

Selects the data invariants in scope for bound (invariant.since <= bound — the same half of the scope rule as partition/2, for PhoenixKit.Migrations.ExpectedSchema.DataInvariant.t() instead of Object.t()).

partition(objects, bound)

Partitions objects by object.since <= bound — the scope rule's first half. Order within each list is preserved from the input.

resolve(object, bound)

@spec resolve(PhoenixKit.Migrations.ExpectedSchema.Object.t(), bound :: pos_integer()) ::
  resolved() | nil

Resolves one in-scope object to the shape bound selects (the scope rule's second half — see moduledoc). nil for an object whose since > bound — callers are expected to have already filtered via partition/2; this guard exists so a misuse fails as "nothing to resolve" rather than resolving against a shape that does not exist yet.