{"record":{"id":"1c9cde44d1cc2468","repo":"apache/superset","slug":"cannot-delete-a-database-whose-only-remaining-data","errorCode":null,"errorMessage":"Cannot delete a database whose only remaining datasets are soft-deleted. Restore them (POST /api/v1/dataset/<uuid>/restore) and delete them permanently once a purge capability ships, or remove the underlying rows out-of-band, before deleting the database.","messagePattern":"Cannot delete a database whose only remaining datasets are soft-deleted\\. Restore them \\(POST /api/v1/dataset/<uuid>/restore\\) and delete them permanently once a purge capability ships, or remove the underlying rows out-of-band, before deleting the database\\.","errorType":"exception","errorClass":"DatabaseDeleteSoftDeletedDatasetsExistFailedError","httpStatus":500,"severity":"error","filePath":"superset/commands/database/delete.py","lineNumber":102,"sourceCode":"                db.session.query(SqlaTable.id)\n                .filter(\n                    SqlaTable.database_id == self._model_id,\n                    SqlaTable.deleted_at.is_(None),\n                )\n                .exists()\n            ).scalar()\n            has_any = db.session.query(\n                db.session.query(SqlaTable.id)\n                .filter(SqlaTable.database_id == self._model_id)\n                .exists()\n            ).scalar()\n        # Both cases block the delete (a soft-deleted dataset still FK-references\n        # the database), but the message differs: with only hidden rows left the\n        # operator's dataset list looks empty, so say so explicitly.\n        if has_live:\n            raise DatabaseDeleteDatasetsExistFailedError()\n        if has_any:\n            raise DatabaseDeleteSoftDeletedDatasetsExistFailedError()\n","sourceCodeStart":84,"sourceCodeEnd":103,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/database/delete.py#L84-L103","documentation":"DatabaseDeleteSoftDeletedDatasetsExistFailedError raised in DeleteDatabaseCommand.validate (delete.py:102) when has_live is false but has_any is true: every remaining SqlaTable row referencing the database is soft-deleted, yet those rows still physically FK-reference the database (tables.database_id), so a hard delete would violate referential integrity. The long message tells the operator exactly that: restore the datasets via POST /api/v1/dataset/<uuid>/restore and delete them permanently, or remove the rows out-of-band, until a purge capability ships.","triggerScenarios":"DELETE /api/v1/database/<id> after all its datasets were 'deleted' in the UI (which soft-deletes them); the dataset list looks empty so the operator expects the delete to succeed.","commonSituations":"Teams that cleaned up datasets via the UI before decommissioning a database; environments on dialects where the FK would turn this into an opaque 500 instead.","solutions":["For each soft-deleted dataset: POST /api/v1/dataset/<uuid>/restore, then DELETE /api/v1/dataset/<pk> (hard delete) — repeat until none remain.","If the REST purge path is insufficient for bulk cleanup, remove the rows out-of-band with a targeted metadata-DB script (DELETE FROM tables WHERE database_id = <id> AND deleted_at IS NOT NULL), with a backup first.","Retry DELETE /api/v1/database/<id> once both counts (live and any) are zero.","Track upstream: once a dataset purge capability ships, prefer it over manual SQL."],"exampleFix":"# before: delete fails — only soft-deleted datasets remain\nDELETE /api/v1/database/5\n# -> DatabaseDeleteSoftDeletedDatasetsExistFailedError\n\n# after: purge each soft-deleted dataset, then delete the DB\nPOST /api/v1/dataset/<uuid>/restore\nDELETE /api/v1/dataset/<pk>\nDELETE /api/v1/database/5","handlingStrategy":"validation","validationCode":"from superset.connectors.sqla.models import SqlaTable\nfrom superset import db\n\ndef soft_deleted_dataset_ids(database_id: int) -> list:\n    return [\n        row[0]\n        for row in db.session.query(SqlaTable.id, SqlaTable.uuid)\n        .filter(SqlaTable.database_id == database_id)\n        .all()\n    ]  # if the live count is 0 but this is non-empty, the delete will fail","typeGuard":null,"tryCatchPattern":"from superset.commands.database.exceptions import (\n    DatabaseDeleteSoftDeletedDatasetsExistFailedError,\n)\n\ntry:\n    DeleteDatabaseCommand(model_id).run()\nexcept DatabaseDeleteSoftDeletedDatasetsExistFailedError:\n    for uuid in soft_deleted_uuids(model_id):\n        requests.post(f\"/api/v1/dataset/{uuid}/restore\")\n        requests.delete(f\"/api/v1/dataset/{pk_of(uuid)}\")\n    DeleteDatabaseCommand(model_id).run()  # retry","preventionTips":["UI dataset deletion is soft deletion — it does NOT free the database for hard delete.","Purge soft-deleted datasets (restore then delete) as part of database decommissioning runbooks.","An empty-looking dataset list can still block the delete; trust the error, not the UI."],"tags":["database","delete","dataset","soft-delete","foreign-key","purge"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}