What happens to a plugin's table when the plugin goes #128

Closed
opened 2026-09-09 10:15:51 +00:00 by tiagoagueda · 1 comment
Owner

Observation

a plugin, even a internal must be a self-contained as possible

Third prerequisite, and the one that only bites the kinds that hold data. A self-contained
plugin that owns a model owns a table, and a table outlives the package that made it.

What exists

Every model in Postulo lives in a Postulo app with a Postulo migration. The live example is
the newest one: phone-numbers is a plugin, and everything it governs is in core —
core.PhoneNumber, core/migrations/0007_phonenumber.py, the data migration that carried
the old single field across, and a GenericRelation on two holders in two other apps.

Make that plugin self-contained in the sense the imperative asks for and it becomes a
Django app of its own with a migration history of its own. Then three questions arrive that
do not exist today:

  1. Uninstalling. Server settings → Plugins removes a package. If the package owned the
    table, removing it leaves either a table with no model — invisible to migrate, present
    in every backup, holding somebody's telephone numbers — or a schema change that deletes
    the data. Neither is currently possible, because no plugin owns a table.
  2. Switching off, which is a different act. #90 settled that switching a plugin off
    deletes nothing, in the interface, in 39 languages. The distinction between off and
    uninstalled has never had to carry weight before and now would.
  3. A missing app at start-up. Django needs the app in INSTALLED_APPS to load the
    model. A plugin uninstalled while its rows remain means either an app that must stay
    installed after its package is gone, or a start-up that fails on a migration referring to
    a module that no longer imports.

What this asks for

A written rule for what happens to a plugin's data when the plugin goes, and a mechanism
that makes it true — before any plugin is moved out of core carrying a table with it.

Worth being careful about

Backups and exports are the safety net and they have to know. manage.py backup and the
account export walk models; a table owned by a package that is no longer installed is a
table neither can see. Somebody's numbers would be absent from their export without anything
saying so, which is the failure mode the export exists to prevent.

The honest options are few. Refuse to uninstall while rows exist, and say what holds it;
uninstall and keep the table, with a way to see what orphaned tables remain; or uninstall and
delete, behind a confirmation naming the count. Each is defensible and they are not equally
safe; picking one is the substance of this issue.

Only some kinds are affected. The two sources, the importer and the transport hold
nothing and could be moved out of core with none of this decided. That makes a staged answer
possible: move the stateless ones first, and let this issue block only the ones that own
data.

A plugin that ships inside the image cannot really be uninstalled, which is a mitigation
worth stating: the built-ins arrive with Postulo and go when it does. The risk arrives with
the third-party plugin that copies the pattern, which is precisely who the documentation is
written for.

## Observation > a plugin, even a internal must be a self-contained as possible Third prerequisite, and the one that only bites the kinds that hold data. A self-contained plugin that owns a model owns a **table**, and a table outlives the package that made it. ## What exists Every model in Postulo lives in a Postulo app with a Postulo migration. The live example is the newest one: `phone-numbers` is a plugin, and everything it governs is in core — `core.PhoneNumber`, `core/migrations/0007_phonenumber.py`, the data migration that carried the old single field across, and a `GenericRelation` on two holders in two other apps. Make that plugin self-contained in the sense the imperative asks for and it becomes a Django app of its own with a migration history of its own. Then three questions arrive that do not exist today: 1. **Uninstalling.** *Server settings → Plugins* removes a package. If the package owned the table, removing it leaves either a table with no model — invisible to `migrate`, present in every backup, holding somebody's telephone numbers — or a schema change that deletes the data. Neither is currently possible, because no plugin owns a table. 2. **Switching off, which is a different act.** #90 settled that switching a plugin off deletes nothing, in the interface, in 39 languages. The distinction between *off* and *uninstalled* has never had to carry weight before and now would. 3. **A missing app at start-up.** Django needs the app in `INSTALLED_APPS` to load the model. A plugin uninstalled while its rows remain means either an app that must stay installed after its package is gone, or a start-up that fails on a migration referring to a module that no longer imports. ## What this asks for A written rule for what happens to a plugin's data when the plugin goes, and a mechanism that makes it true — before any plugin is moved out of core carrying a table with it. ## Worth being careful about **Backups and exports are the safety net and they have to know.** `manage.py backup` and the account export walk models; a table owned by a package that is no longer installed is a table neither can see. Somebody's numbers would be absent from their export without anything saying so, which is the failure mode the export exists to prevent. **The honest options are few.** Refuse to uninstall while rows exist, and say what holds it; uninstall and keep the table, with a way to see what orphaned tables remain; or uninstall and delete, behind a confirmation naming the count. Each is defensible and they are not equally safe; picking one is the substance of this issue. **Only some kinds are affected.** The two sources, the importer and the transport hold nothing and could be moved out of core with none of this decided. That makes a staged answer possible: move the stateless ones first, and let this issue block only the ones that own data. **A plugin that ships inside the image cannot really be uninstalled**, which is a mitigation worth stating: the built-ins arrive with Postulo and go when it does. The risk arrives with the third-party plugin that copies the pattern, which is precisely who the documentation is written for.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 10:15:51 +00:00
Author
Owner

The rule, which the issue said was the substance:

A plugin that owns a table may not be uninstalled while that table holds anything.

Postulo refuses, names how many records are in the way, and leaves both the package and the
data alone.

Why not the other two. Keep the table leaves data nothing can read, export or restore —
present in every backup, absent from the export of the person whose data it is, invisible to
migrate. That is exactly the failure the issue named. Delete behind a confirmation makes
removing a package a data-destroying act, when somebody may only be swapping it for a newer
build of the same plugin — and it puts uninstall on the wrong side of a promise Postulo makes
in thirty-nine languages, that switching a plugin off deletes nothing. Nobody holds that
distinction in their head at the moment it matters.

Refusing is also the shape already used three times here: the last administrator, the mail
transport that is the last way in, the plugin that is somebody's only recovery route.

The three questions the issue raised, answered.

  1. Uninstalling — refused while rows exist, on remove() rather than only on the page, so a
    management command or a shell meets the same rule.
  2. Off versus uninstalled — now genuinely different acts, and the refusal says so: "switch
    the plugin off instead — off keeps everything."
  3. A missing app at start-up — mostly dissolved by the rule. The table is always empty when
    the app goes, so its migrations reverse cleanly and there is never a migration referring to
    a module that no longer imports. CONTRIBUTING.md states the requirement that a plugin
    owning a table ships its own migrations and is in INSTALLED_APPS.

"Backups and exports are the safety net and they have to know." The export now carries a
plugins section. A plugin that owns a person's data answers export_for; one that owns data
and cannot answer has its models named under not_carried, because a silent gap is
discovered when somebody restores and a stated one while they still have the original. A plugin
that raises while exporting is named the same way rather than taking a whole archive down.
FORMAT_VERSION is 8.

"Only some kinds are affected", and the staged answer is now available. owns_models is
absent from every plugin Postulo ships, so the sources, the importer and the transport can be
moved out of core with none of this in the way — which is what #129 and #147 will want.

The test plugin claims to own core.Tag. The mechanism deals in labels and does not care where
a model lives, so borrowing a real one exercises all of it without inventing an app that would
exist only for a test.

19 tests in tests/test_plugin_data.py, the rule in CONTRIBUTING.md for plugin authors and
in the wiki for operators, and one string in all 39 European catalogues.

Shipped in 41b5af2 on 0.3.0, with main kept level.

**The rule, which the issue said was the substance:** > A plugin that owns a table may not be uninstalled while that table holds anything. Postulo refuses, names how many records are in the way, and leaves both the package and the data alone. **Why not the other two.** *Keep the table* leaves data nothing can read, export or restore — present in every backup, absent from the export of the person whose data it is, invisible to `migrate`. That is exactly the failure the issue named. *Delete behind a confirmation* makes removing a package a data-destroying act, when somebody may only be swapping it for a newer build of the same plugin — and it puts *uninstall* on the wrong side of a promise Postulo makes in thirty-nine languages, that switching a plugin **off** deletes nothing. Nobody holds that distinction in their head at the moment it matters. Refusing is also the shape already used three times here: the last administrator, the mail transport that is the last way in, the plugin that is somebody's only recovery route. **The three questions the issue raised, answered.** 1. *Uninstalling* — refused while rows exist, on `remove()` rather than only on the page, so a management command or a shell meets the same rule. 2. *Off versus uninstalled* — now genuinely different acts, and the refusal says so: "switch the plugin off instead — off keeps everything." 3. *A missing app at start-up* — mostly dissolved by the rule. The table is always empty when the app goes, so its migrations reverse cleanly and there is never a migration referring to a module that no longer imports. `CONTRIBUTING.md` states the requirement that a plugin owning a table ships its own migrations and is in `INSTALLED_APPS`. **"Backups and exports are the safety net and they have to know."** The export now carries a `plugins` section. A plugin that owns a person's data answers `export_for`; one that owns data and cannot answer has its models **named** under `not_carried`, because a silent gap is discovered when somebody restores and a stated one while they still have the original. A plugin that raises while exporting is named the same way rather than taking a whole archive down. `FORMAT_VERSION` is 8. **"Only some kinds are affected", and the staged answer is now available.** `owns_models` is absent from every plugin Postulo ships, so the sources, the importer and the transport can be moved out of core with none of this in the way — which is what #129 and #147 will want. The test plugin claims to own `core.Tag`. The mechanism deals in labels and does not care where a model lives, so borrowing a real one exercises all of it without inventing an app that would exist only for a test. 19 tests in `tests/test_plugin_data.py`, the rule in `CONTRIBUTING.md` for plugin authors and in the wiki for operators, and one string in all 39 European catalogues. Shipped in `41b5af2` on `0.3.0`, with `main` kept level.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#128
No description provided.