"""Purge soft-deleted applications that still reserve their names. Delete used to set isactive=False and leave the row in place. The uniqueness check on create looks at every row regardless of isactive, so each of those rows kept its name reserved permanently: creating an application with the same name failed with "already exists" against a row no screen would display, and nothing in the UI could clear it. Delete is a hard delete now, but that only fixes rows deleted from here on. This clears the ones already stranded, in every environment rather than just the one where the bug was noticed. Dependent rows are cleared the same way the API does it, and for the same reasons: appversions and computerinstalledapps are NOT NULL and meaningless without their application, while knowledgebase.appid is nullable and the article is somebody's writing, so it is unlinked rather than destroyed. Uploaded installers belonging to these rows are deliberately NOT touched. A migration that deletes files cannot be rolled back and cannot be dry-run, and a stray file is harmless next to that risk. Deletes made through the API do clean them up. Revision ID: 7d45_purge_inactive_apps Revises: 7d44_app_monitorable """ from alembic import op import sqlalchemy as sa revision = '7d45_purge_inactive_apps' down_revision = '7d44_app_monitorable' branch_labels = None depends_on = None def _table_exists(bind, name): """True when a table is present. The plugin tables are only there when the plugin is installed, so each is checked before being touched. """ return sa.inspect(bind).has_table(name) def upgrade(): bind = op.get_bind() if not _table_exists(bind, 'applications'): return doomed = [row[0] for row in bind.execute(sa.text( 'SELECT appid FROM applications WHERE isactive = false' ))] if not doomed: return for appid in doomed: params = {'appid': appid} if _table_exists(bind, 'knowledgebase'): bind.execute(sa.text( 'UPDATE knowledgebase SET appid = NULL WHERE appid = :appid' ), params) if _table_exists(bind, 'computerinstalledapps'): bind.execute(sa.text( 'DELETE FROM computerinstalledapps WHERE appid = :appid' ), params) if _table_exists(bind, 'appversions'): bind.execute(sa.text( 'DELETE FROM appversions WHERE appid = :appid' ), params) bind.execute(sa.text( 'DELETE FROM applications WHERE appid = :appid' ), params) def downgrade(): # The rows are gone and their contents were not captured, so there is # nothing to restore. Failing loudly is better than pretending otherwise. raise NotImplementedError( 'Purged applications cannot be restored; recover from a backup.')