PhoenixKit. Migrations. ExpectedSchema. Behaviour behaviour
(phoenix_kit v2.0.1)
Copy Markdown
View Source
The manifest contract every PhoenixKit.Migrations.ExpectedSchema
implementation satisfies — this is the entry point for the whole
verify-and-repair design (spec §5.1/§6.1).
Why this module exists (and why it is not called ExpectedSchema)
PhoenixKit.Migrations.ExpectedSchema is the name of the tool-generated
manifest (dev_docs/squash/generate_baseline.exs's Emitter.render_manifest/2,
frozen from P1) — a real migrated scratch database, one incremental
per-version catalog diff at a time (spec §5.1/§8.3). It does not exist in
this repository yet; it lands with the squash PR (P3), @moduledoc false
(a manifest spanning the whole V01..@current_version chain is too large
to be a useful hexdocs page, and it is regenerated — never hand-edited — so
there is nothing there worth documenting in place).
This module — and its siblings
PhoenixKit.Migrations.ExpectedSchema.Object and
PhoenixKit.Migrations.ExpectedSchema.DataInvariant — is the public docs
surface for that contract, deliberately namespaced underneath
ExpectedSchema rather than reusing that exact name: once the real
generated module lands at lib/phoenix_kit/migrations/expected_schema.ex,
it is a single generated file that replaces its own previous contents
wholesale on every regeneration ("never hand-merged" per spec §8.3) — a
docs/behaviour module living at that same name and path would be silently
destroyed the first time the squash PR's generation step runs. Living at
expected_schema/behaviour.ex instead means both files coexist forever:
the generated module supplies the data, this one documents the shape.
The three callbacks
Every implementation — the real generated manifest, and any test fixture
such as PhoenixKit.Test.FixtureExpectedSchema — exports exactly these
three functions. objects/1 and data_invariants/1 take a
prefix because both return SQL text/specs anchored to a specific Postgres
schema (spec §5.1's "__SCHEMA__" substitution); chain_hash/0 does not,
because it hashes the v*.ex migration file set itself, which has no
notion of a runtime prefix.
A hand-written implementation should:
use Ecto.Migration-style raw SQL/data as a private "raw" list, written withPhoenixKit.Migrations.ExpectedSchema.Object.schema_token/0("__SCHEMA__") standing in for the prefix and the atom:prefixstanding in for a helper-call argument that needs it (seeObject's "Helper creates" section);- normalize the incoming
prefixonce, viaObject.normalize_prefix/1; - materialize every raw object/invariant against that prefix via
Object.materialize/2/DataInvariant.materialize/2.
PhoenixKit.Test.FixtureExpectedSchema follows exactly this shape and is a
runnable reference alongside these docs.
@behaviour PhoenixKit.Migrations.ExpectedSchema.Behaviour
@impl PhoenixKit.Migrations.ExpectedSchema.Behaviour
def objects(prefix) do
prefix = Object.normalize_prefix(prefix)
Enum.map(raw_objects(), &Object.materialize(&1, prefix))
end
@impl PhoenixKit.Migrations.ExpectedSchema.Behaviour
def data_invariants(prefix \ "public") do
prefix = Object.normalize_prefix(prefix)
Enum.map(raw_data_invariants(), &DataInvariant.materialize(&1, prefix))
end
@impl PhoenixKit.Migrations.ExpectedSchema.Behaviour
def chain_hash, do: @chain_hashFinding the concrete module
Nothing in P2 calls PhoenixKit.Migrations.ExpectedSchema (or any other
implementation) by name directly — go through
PhoenixKit.Migrations.ExpectedSchema.Resolver.resolve/0. It degrades
gracefully ({:error, :not_generated}) for the whole of P2, since the real
module does not exist until P3's scratch-DB generation step runs, and lets
tests substitute a fixture via
Application.put_env(:phoenix_kit, :expected_schema_module, ...).
The real generated module does not declare @behaviour here
The Emitter's template (Emitter.render_manifest/2) emits def objects/1,
def data_invariants/1, def chain_hash/0 directly with no
@behaviour PhoenixKit.Migrations.ExpectedSchema.Behaviour line — P1 is
frozen, so that will still be true once the real module lands in P3. The
compiler's callback-completeness warning therefore never fires for it.
Resolver compensates by checking conformance structurally at runtime
(exported-function membership) rather than relying on @behaviour
declaration — see PhoenixKit.Migrations.ExpectedSchema.Object's
moduledoc, deviation 4, for the full reasoning. Hand-written
implementations should still declare @behaviour (as shown above and in
the fixture) for the free compile-time check on top of that.
Summary
Callbacks
SHA-256 (lower-hex) over the sorted v*.ex migration file set at the time
this module was generated — the staleness detector (spec §5.1/§8.3):
mix phoenix_kit.release_check and a plain DB-free unit test both assert
this still matches a fresh hash of the on-disk chain, catching "a migration
was added but the manifest was not regenerated" before it reaches a
release. Takes no prefix — the migration file set has no per-install
variation.
Every data invariant (spec §5.1's upgrade-only-transform assertions),
materialized for prefix. See PhoenixKit.Migrations.ExpectedSchema.DataInvariant.
Every tracked schema object, materialized for prefix.
Callbacks
@callback chain_hash() :: String.t()
SHA-256 (lower-hex) over the sorted v*.ex migration file set at the time
this module was generated — the staleness detector (spec §5.1/§8.3):
mix phoenix_kit.release_check and a plain DB-free unit test both assert
this still matches a fresh hash of the on-disk chain, catching "a migration
was added but the manifest was not regenerated" before it reaches a
release. Takes no prefix — the migration file set has no per-install
variation.
@callback data_invariants(prefix :: String.t() | nil) :: [ PhoenixKit.Migrations.ExpectedSchema.DataInvariant.t() ]
Every data invariant (spec §5.1's upgrade-only-transform assertions),
materialized for prefix. See PhoenixKit.Migrations.ExpectedSchema.DataInvariant.
Same prefix contract as objects/1. Declared at arity 1 (not the spec
§5.1 pseudocode's arity 0) because :assert SQL is schema-anchored the same
way objects/1's SQL is — see Object's moduledoc, deviation 5.
@callback objects(prefix :: String.t() | nil) :: [ PhoenixKit.Migrations.ExpectedSchema.Object.t() ]
Every tracked schema object, materialized for prefix.
prefix is nil (normalizes to "public") or a validated schema name
(PhoenixKit.Migrations.Postgres.Helpers.validate_prefix!/1 — raises
ArgumentError for anything unsafe to interpolate into SQL, same as every
migration entry point).
Deterministic: two calls with the same prefix return == lists (the
generator sorts by {since, class, id} at emission time; a hand-written
implementation should preserve that or any other fixed order — callers must
not depend on a particular order, only on it being stable across calls).