Module-owned versioned migrations for phoenix_kit_posts — the
decentralized-migrations protocol that core's mix phoenix_kit.update
discovers via migration_module/0. This follows the canonical shape
documented in phoenix_kit_hello_world's README ("Versioned migrations",
"Adopting a table core already creates") and its
mix phoenix_kit_hello_world.audit_migrations task: two readers
(migrated_version/1 for migration context, migrated_version_runtime/1
for Mix-task context), up/1 re-reading the version before it changes
anything, and a namespaced COMMENT ON TABLE marker on one anchor table
for a chain that spans 13. phoenix_kit_warehouse (v1_statements/2,
8 adopted tables in one version) and phoenix_kit_billing (ten adopted
tables in one version) are the closest sibling examples of this exact
adoption situation, scaled up further here.
Ownership situation — read before touching
All 13 phoenix_kit_post*/phoenix_kit_comment_* tables are core's
baseline: V135 created all 13 in their pre-time_zone shape (with a
plain, non-unique phoenix_kit_posts_slug_index), V167 made that index
UNIQUE (repairing any existing duplicate slugs first), V168 added the
(user_uuid, slug) unique index on phoenix_kit_post_groups, and V185
added phoenix_kit_posts.time_zone. On every existing install
all 13 already have their full current shape before this chain ever
executes — this is an ADOPTION, not a create. Varchar widths are never
restated as a second number: each owning schema's own column_widths/0
(Post, PostComment, PostGroup, PostMention, PostTag, PostView
— the other seven tables have no varchar column) is the single shape
authority this chain's DDL interpolates.
Rather than stamp all 13 tables, the chain anchors its version marker on a
single table — phoenix_kit_posts itself, this module's own central,
load-bearing table. This deliberately departs from the sibling chains'
convention of anchoring on the table with no outgoing FK of its own
(phoenix_kit_post_tags has that property here — zero outgoing FKs, the
only one of the 13 — but is not otherwise central to the module and is not
the table every other adoption in this chain ultimately points back to).
phoenix_kit_posts is never at risk of being dropped independently of the
rest of the chain, which is what the anchor choice is actually protecting.
Every table with a user_uuid column carries a real FK to
phoenix_kit_users(uuid) ON DELETE CASCADE — 9 of the 13 tables
(phoenix_kit_post_tags, phoenix_kit_post_tag_assignments, and
phoenix_kit_post_group_assignments have no user_uuid column and thus
no such FK). In total this chain adopts 24 FKs: 9 to phoenix_kit_users,
1 self-referential on phoenix_kit_post_comments (parent_uuid), 1 to
phoenix_kit_post_groups, 1 to phoenix_kit_post_tags, 2 to
phoenix_kit_files (phoenix_kit_post_media.file_uuid, ON DELETE CASCADE, and phoenix_kit_post_groups.cover_image_uuid, ON DELETE SET NULL — the lone non-CASCADE FK in the whole set), 8 to phoenix_kit_posts
itself, and 2 to phoenix_kit_post_comments
(phoenix_kit_comment_likes/_dislikes).
A discrepancy between core's migration source and its ExpectedSchema manifest
The manifest's human-readable create: string for a bare (no-DEFAULT)
not-null column across all 13 tables omits NOT NULL — e.g.
phoenix_kit_posts.title. Core's actual migration source (v135.ex) and
the manifest's own structured revisions.not_null field both agree the
column IS NOT NULL. This chain's DDL follows the source and
revisions — the create: string is the buggy representation, and the
ownership test below diffs against revisions, never against create:.
Phase 0 — this V1 adopts, and changes NOTHING
CREATE TABLE IF NOT EXISTS shape-identical to core's V135/V167/V168/V185
baseline, under core's exact object names (every pkey, index, and FK),
then a namespaced marker stamp on the anchor table (pkpo_schema:1 —
an adopted table may already carry a foreign comment, so the reader must
treat prose as version 0, never crash on it, never assume it means V1).
Because the shape is unchanged, core's ExpectedSchema manifest stays
accurate: no core release is required and there is no release-ordering
hazard. This package releases alone.
Five unique constraints this module's own schemas assert via
Ecto.Changeset.unique_constraint/3 have no backing index in core's
baseline — phoenix_kit_post_likes/_dislikes/_mentions on
(post_uuid, user_uuid) and phoenix_kit_comment_likes/_dislikes on
(comment_uuid, user_uuid). V1 does NOT create these: it is a pure
adoption of core's shape exactly as it stands, and adding a constraint
core never had would be a shape CHANGE, not an adoption. They are a
documented gap for a future V2.
Phase 1 — the first real shape change (V2+) is when core must move too
Before shipping a version that changes any of the 13 tables' shape — including closing the five gaps above:
- add the objects that version alters to core's manifest generator's
@excluded_exact(dev_docs/squash/generate_baseline.exs) and regenerateExpectedSchema; - raise this package's
:phoenix_kitfloor to the release that ships that regenerated manifest.
Skipping step 1 means mix phoenix_kit.repair restores the old shape
after every run, silently undoing the new version.
A related, separate proposal (not a code change in this PR): core's
@table_owner_prefixes/@table_owner_substrings baseline-squash heuristic
(generate_baseline.exs) currently tags phoenix_kit_post_comments,
phoenix_kit_post_likes, phoenix_kit_post_dislikes,
phoenix_kit_comment_likes and phoenix_kit_comment_dislikes as
owner: :comments — a coincidental substring match on
"comment"/"like"/"dislike" (that file's own moduledoc calls this "a
best-effort hint, not a rigorous taxonomy") since the real
phoenix_kit_comments module owns entirely different tables
(phoenix_kit_comments, phoenix_kit_comments_likes,
phoenix_kit_comments_dislikes — plural comments_). A {"post_", :posts}
prefix rule ahead of the substring fallback, plus explicit entries for the
two phoenix_kit_comment_* tables, would tag all 5 as :posts instead.
Phase 2 — creation leaves core's baseline at the next squash cycle
When core cuts its next baseline, module-owned tables are simply not
included: fresh installs from then on get all 13 phoenix_kit_post*/
phoenix_kit_comment_* tables from THIS chain's V1 — which is why V1's
up/1 ensures the uuid_generate_v7() function (and its pgcrypto
extension) exist rather than assuming core's chain already provided them,
and why every CREATE TABLE must already be the full, correct definition
on its own, not merely a shape-matching no-op for an already-existing
table. Existing installs are untouched — a baseline squash only affects
fresh installs and below-floor bridging.
What must NEVER happen
No conditional core migration of the form "module absent → drop the
tables" — that is nondeterministic (depends on which packages are
compiled in) and destroys data on a host that merely removed the
package. Removing this module's data is a human, manual step — see
README.md "Removing this module" for the operator SQL. There is
deliberately no automated uninstall path, and down/1 NEVER drops any of
the 13 tables for ANY target version, including 0 — it only unstamps
(or re-stamps) the marker on the anchor table. The rows are every user's
posts, likes, tags, groups, media, mentions, views and legacy comments,
and on most installs every table is core-created; rolling back this
module's chain must not destroy any of them.
The migrated version is tracked as a pkpo_schema:<N> COMMENT on
phoenix_kit_posts. A marker-less table, or one carrying a foreign
(non-pkpo_schema:) comment, reads as version 0 — the core-baseline shape
before this chain existed.
Summary
Functions
The version this code expects the schema to be at.
Rolls back to opts[:version] (default 0). Migration-context only.
Never drops a table or a row in any of the 13, for any target — see the
moduledoc.
The SQL down/1 executes, as data (marker bookkeeping only, on the
anchor table). V1 changes no shape of its own — it is pure adoption — so
there is nothing to drop beyond the marker; all 13 tables and every row in
them are left untouched, for any target including 0.
The version a bare, freshly-created set of tables is at (Phase 2 — a future install whose core baseline no longer creates these tables).
The version currently installed, read INSIDE a migration — through
Ecto.Migration's own repo(). No rescue: inside a migration a version
that cannot be read must abort the transaction, never be guessed at.
up/1 and down/1 call this — never migrated_version_runtime/1 —
before making any change.
Runtime-safe reader — the one mix phoenix_kit.update calls, from a Mix
task with no migrator running, through PhoenixKit's configured repo
instead of Ecto.Migration's.
Applies every chain version up to opts[:version] (default
current_version/0). Migration-context only — re-reads the installed
version via migrated_version/1 before making any change, so a database
already at (or ahead of) the target does nothing.
The SQL up/1 executes, as data — the testable single source. The
ownership test suite parses these statements to prove that the object
names are core's V135/V167/V168/V185 names, that every CREATE TABLE
stays shape-identical to core's ExpectedSchema manifest, that every
varchar width is its owning schema's column_widths/0, and that nothing
here can drop a table.
The table carrying the pkpo_schema:<N> marker for the whole 13-table chain.
Functions
@spec current_version() :: pos_integer()
The version this code expects the schema to be at.
Rolls back to opts[:version] (default 0). Migration-context only.
Never drops a table or a row in any of the 13, for any target — see the
moduledoc.
@spec down_statements(String.t(), non_neg_integer()) :: [String.t()]
The SQL down/1 executes, as data (marker bookkeeping only, on the
anchor table). V1 changes no shape of its own — it is pure adoption — so
there is nothing to drop beyond the marker; all 13 tables and every row in
them are left untouched, for any target including 0.
@spec initial_version() :: pos_integer()
The version a bare, freshly-created set of tables is at (Phase 2 — a future install whose core baseline no longer creates these tables).
@spec migrated_version(keyword() | map()) :: non_neg_integer()
The version currently installed, read INSIDE a migration — through
Ecto.Migration's own repo(). No rescue: inside a migration a version
that cannot be read must abort the transaction, never be guessed at.
up/1 and down/1 call this — never migrated_version_runtime/1 —
before making any change.
@spec migrated_version_runtime(keyword() | map()) :: non_neg_integer()
Runtime-safe reader — the one mix phoenix_kit.update calls, from a Mix
task with no migrator running, through PhoenixKit's configured repo
instead of Ecto.Migration's.
An invalid prefix is re-raised, matching core's own reader: 0 means
"not installed here", so reporting it for a bad prefix would tell the
operator something false and send the updater off to install a schema
over live data. Genuine unreachability still yields 0, which is safe
only because up/1 re-reads the version in migration context before
touching anything — a wrong 0 costs a redundant migration file, never
wrong DDL.
Applies every chain version up to opts[:version] (default
current_version/0). Migration-context only — re-reads the installed
version via migrated_version/1 before making any change, so a database
already at (or ahead of) the target does nothing.
@spec up_statements(String.t(), non_neg_integer()) :: [String.t()]
The SQL up/1 executes, as data — the testable single source. The
ownership test suite parses these statements to prove that the object
names are core's V135/V167/V168/V185 names, that every CREATE TABLE
stays shape-identical to core's ExpectedSchema manifest, that every
varchar width is its owning schema's column_widths/0, and that nothing
here can drop a table.
target selects how much of the chain to emit (default
current_version/0): 0 applies nothing (not an operation — clearing
the marker is down/1's job); 1 is the pure V135/V167/V168/V185-adoption
step across all 13 tables.
@spec version_table() :: String.t()
The table carrying the pkpo_schema:<N> marker for the whole 13-table chain.
Not part of the protocol mix phoenix_kit.update calls. Exported so an
auditor (mix phoenix_kit_hello_world.audit_migrations) can verify the
marker is really a number without hard-coding this table's name.