diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/db/purge.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/db/purge.py | 52 |
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))) |