From 076134b4454adda6fd3e3ceda92293d41e04da79 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 30 Jul 2026 10:16:26 +0200 Subject: [PATCH] migration: add missing deleted_at columns to 9 TenantMixin tables (0083) --- .../0083_add_missing_deleted_at_columns.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 alembic/versions/0083_add_missing_deleted_at_columns.py diff --git a/alembic/versions/0083_add_missing_deleted_at_columns.py b/alembic/versions/0083_add_missing_deleted_at_columns.py new file mode 100644 index 0000000..3eba4f7 --- /dev/null +++ b/alembic/versions/0083_add_missing_deleted_at_columns.py @@ -0,0 +1,53 @@ +"""Add missing deleted_at columns to TenantMixin tables. + +Several models inherit TenantMixin (which includes SoftDeleteMixin) +but their DB tables were never migrated to include the deleted_at column. +This causes 500 errors when SQLAlchemy tries to SELECT deleted_at. + +Revision ID: 0083 +Revises: 0082 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0083" +down_revision = "0082" +branch_labels = None +depends_on = None + +# Tables that use TenantMixin (and therefore SoftDeleteMixin) in their models +# but are missing the deleted_at column in the database. +TABLES_NEEDING_DELETED_AT = [ + "contact_folder_permissions", + "permission_delegations", + "guest_users", + "entity_policies", + "notification_types", + "password_reset_tokens", + "api_tokens", + "permission_templates", + "user_groups", +] + + +def upgrade() -> None: + conn = op.get_bind() + for table_name in TABLES_NEEDING_DELETED_AT: + # Check if column already exists before adding + result = conn.execute(sa.text( + "SELECT 1 FROM information_schema.columns " + "WHERE table_name = :t AND column_name = 'deleted_at'" + ), {"t": table_name}) + if result.scalar() is None: + op.add_column( + table_name, + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + ) + print(f" Added deleted_at to {table_name}") + else: + print(f" Skipped {table_name} (already has deleted_at)") + + +def downgrade() -> None: + for table_name in reversed(TABLES_NEEDING_DELETED_AT): + op.drop_column(table_name, "deleted_at")