Deletes runtime drafts that were discarded while these resources still soft-deleted.
What went wrong
MishkaCmsCore.Runtime.PageDraft and its two siblings used AshArchival until 2026-08-01.
Discarding a draft did not remove the row — it set archived_at, and a resource-level
base_filter_sql "(archived_at IS NULL)" hid it from every read.
When the extension was removed, discard became a real delete. But the base filter went with it,
so every row still carrying an archived_at became visible again: a draft its author threw
away is back in the picker, indistinguishable from work in progress. That happened the moment the
code was pulled, before any migration ran.
Dropping the column afterwards does not resurrect anything further — the rows were already visible — but it does remove the only evidence of which ones were discarded. This module is how that evidence gets acted on while it still exists.
What it deletes
Rows matching archived_at IS NOT NULL AND applied_at IS NULL, in page_drafts, layout_drafts
and error_page_drafts.
The applied_at IS NULL half is not redundant. Nothing in the UI could archive a published draft
— every picker lists pending drafts only — but Drafts.discard/3 takes an id from the client and
does not check it against that list, so the combination is reachable in principle. A published
draft has already written its content to the live Page, and its publish is recorded there;
requiring applied_at IS NULL means this purge provably cannot destroy the record of a publish,
rather than merely being unlikely to.
Nothing else needs the same treatment. The old archive do blocks declared no archive_related,
so the removal orphaned nothing, and the resources that still archive — Page, Layout, Component,
Media, Site, and MishkaDocument's own *_drafts, which are a different thing entirely — are
untouched and must stay that way.
Running it
It is safe on a database that never archived, safe once the column is gone, and safe to run twice
— each of those is a clean no-op reporting zero. It is only USEFUL before the migration that drops
archived_at, because afterwards a discarded draft is byte-identical to live work and no query
can tell them apart.
mix mishka_cms_core.purge_draft_archival --dry-run
mix mishka_cms_core.purge_draft_archivalIn a release there is no Mix, and bin/migrate drops the column unattended, so run this first:
bin/mishka_cms eval 'MishkaCmsCore.Runtime.DraftArchivalPurge.run()'See Mix.Tasks.MishkaCmsCore.PurgeDraftArchival and MishkaCmsCore.Runtime.PageDraft.
Summary
Functions
Whether any table still carries the column — what makes this task worth running at all.
Purges the legacy rows and returns what it did, per table.
Types
@type report() :: %{ required(String.t()) => %{ optional(:deleted) => non_neg_integer(), optional(:found) => non_neg_integer(), optional(:skipped) => :no_table | :no_column, optional(:failed) => [String.t()] } }
Functions
@spec pending?() :: boolean()
Whether any table still carries the column — what makes this task worth running at all.
Purges the legacy rows and returns what it did, per table.
Options:
:dry_run— find and count, delete nothing. Defaultfalse.