aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/db/purge.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/db/purge.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/purge.py52
1 files changed, 52 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/purge.py b/packages/meshbay-hub/src/meshbay_hub/db/purge.py
new file mode 100644
index 0000000..02d2914
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/db/purge.py
@@ -0,0 +1,52 @@
+"""Deleting a group from the hub, completely.
+
+One implementation for every caller: the owner deleting a group, an
+administrator erasing the account that owns it, and the cleanup of groups no
+node ever hosted. Each used to carry its own partial cascade, and a row still
+pointing at a deleted group is not an orphan on PostgreSQL but a foreign-key
+error — the deletion fails. SQLite, which the tests run on, does not enforce
+foreign keys by default, so nothing showed it.
+
+The tables are found from the schema rather than listed: every table with a
+foreign key to `groups.id` is emptied of the group's rows, including one added
+after this was written. A table whose rows must outlive the group goes in
+`_DETACHED`, and has its reference set to null instead.
+
+Nothing on a node is touched. The hub does not command those machines; the
+administrator's route pushes a signed revocation to the connected ones.
+"""
+
+from sqlalchemy import delete, update
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from meshbay_hub.db.models import Base, ContentReport, Group
+
+# Rows that outlive their group, detached rather than deleted: a report is
+# evidence about content, and it is still needed once the group is gone.
+_DETACHED = {ContentReport.__tablename__}
+
+
+def _referencing() -> list[tuple]:
+ """(table, column) for every foreign key onto `groups.id`."""
+ refs = []
+ for table in Base.metadata.sorted_tables:
+ for fk in table.foreign_keys:
+ if fk.column.table.name == Group.__tablename__ and fk.column.name == "id":
+ refs.append((table, fk.parent))
+ return refs
+
+
+async def purge_groups(db: AsyncSession, group_ids: list[str]) -> None:
+ """Delete these groups and everything on the hub that points at them.
+
+ The caller commits: this is one step of a larger transaction (an account
+ erasure deletes its groups and then empties the account row).
+ """
+ if not group_ids:
+ return
+ for table, column in _referencing():
+ if table.name in _DETACHED:
+ await db.execute(update(table).where(column.in_(group_ids)).values({column.name: None}))
+ else:
+ await db.execute(delete(table).where(column.in_(group_ids)))
+ await db.execute(delete(Group).where(Group.id.in_(group_ids)))