{"record":{"id":"09d855b9f7824e38","repo":"apache/superset","slug":"cannot-delete-a-database-that-has-datasets-attache","errorCode":null,"errorMessage":"Cannot delete a database that has datasets attached","messagePattern":"Cannot delete a database that has datasets attached","errorType":"exception","errorClass":"DatabaseDeleteDatasetsExistFailedError","httpStatus":500,"severity":"error","filePath":"superset/commands/database/delete.py","lineNumber":100,"sourceCode":"        with skip_visibility_filter(db.session, SqlaTable):\n            has_live = db.session.query(\n                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":82,"sourceCodeEnd":103,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/database/delete.py#L82-L103","documentation":"DatabaseDeleteDatasetsExistFailedError ('Cannot delete a database that has datasets attached') raised in DeleteDatabaseCommand.validate (delete.py:100) when the visibility-filter-bypassing count finds at least one LIVE (non-soft-deleted) SqlaTable row with database_id == self._model_id. The explicit EXISTS queries are there precisely because the ORM relationship would hide soft-deleted datasets; has_live being true means ordinary, visible datasets still reference the database.","triggerScenarios":"DELETE /api/v1/database/<id> while any dataset in the UI (Datasets list) still points at this database.","commonSituations":"Decommissioning a database without first migrating or deleting its datasets; datasets created implicitly by CSV uploads; forgetting virtual/saved datasets.","solutions":["List the attached datasets (GET /api/v1/dataset/?q=(database_id:<id>)) and delete them (DELETE /api/v1/dataset/<pk>) or migrate them to another database.","If the data is moving, duplicate datasets against the new database first, then delete the old ones.","Retry the database delete only after the live dataset count is zero."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"from superset.models.sql_lab import Query  # placeholder\nfrom superset.connectors.sqla.models import SqlaTable\nfrom superset import db\n\ndef live_dataset_count(database_id: int) -> int:\n    return (\n        db.session.query(SqlaTable.id)\n        .filter(SqlaTable.database_id == database_id)\n        .count()\n    )  # non-zero blocks the delete","typeGuard":null,"tryCatchPattern":"from superset.commands.database.exceptions import DatabaseDeleteDatasetsExistFailedError\n\ntry:\n    DeleteDatabaseCommand(model_id).run()\nexcept DatabaseDeleteDatasetsExistFailedError:\n    list_and_offer_migration(database_id)  # show live datasets, delete or re-point","preventionTips":["Before deleting a database, list its datasets: GET /api/v1/dataset/?q=(database_id:<id>).","Migrate or delete all live datasets first; the count is checked with soft-delete visibility bypassed."],"tags":["database","delete","dataset","dependency","foreign-key"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}