MishkaCmsCore.Runtime.DraftArchivalPurge (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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_archival

In 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

report()

@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

pending?()

@spec pending?() :: boolean()

Whether any table still carries the column — what makes this task worth running at all.

run(opts \\ [])

@spec run(keyword()) :: {:ok, report()}

Purges the legacy rows and returns what it did, per table.

Options:

  • :dry_run — find and count, delete nothing. Default false.