{"record":{"id":"80109d9e95d1dbc0","repo":"apache/superset","slug":"dataset-existing-table-name-r-uuid-config-uui","errorCode":null,"errorMessage":"Dataset {existing.table_name!r} (uuid {config['uuid']}) was deleted and re-import requires can_write permission to restore it","messagePattern":"Dataset (.+?) \\(uuid (.+?)\\) was deleted and re-import requires can_write permission to restore it","errorType":"exception","errorClass":"ImportFailedError","httpStatus":500,"severity":"error","filePath":"superset/commands/dataset/importers/v1/utils.py","lineNumber":261,"sourceCode":"    # overwrite branches below are intentionally skipped because the caller has\n    # already established trust at the command level.\n    user = get_user()\n    # Tracks whether we entered the soft-deleted mutation path so the\n    # downstream `sync` decision (below) can reflect that an\n    # implicit-restore re-import is a clean replacement, not a merge.\n    is_soft_deleted_match = False\n\n    if existing := find_existing_for_import(SqlaTable, config[\"uuid\"]):\n        if existing.deleted_at is not None:\n            # RESTORE path — re-importing a soft-deleted UUID is an implicit\n            # restore-with-update, a distinct operation from overwriting an\n            # alive row, so it is handled in its own branch.\n            if not can_write:\n                # Case B: don't silently return a soft-deleted row to a caller\n                # without write permission — that would let the importer\n                # reattach charts/dashboards to a deleted dataset and produce\n                # broken charts.\n                raise ImportFailedError(\n                    f\"Dataset {existing.table_name!r} (uuid {config['uuid']}) \"\n                    \"was deleted and re-import requires can_write permission \"\n                    \"to restore it\"\n                )\n            # ``user`` is None on background / example-loader paths; combined\n            # with ``can_write`` (typically from ``ignore_permissions=True``)\n            # the editorship check is intentionally skipped because the caller\n            # already established trust.\n            if user and (\n                not security_manager.is_editor(existing)\n                and not security_manager.is_admin()\n            ):\n                raise ImportFailedError(\n                    f\"Dataset {existing.table_name!r} (uuid {config['uuid']}) \"\n                    \"already exists and user doesn't have permissions to \"\n                    \"restore it\"\n                )\n            # Before clearing ``deleted_at``, refuse if another active dataset","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/dataset/importers/v1/utils.py#L243-L279","documentation":"During v1 dataset import, when the uploaded UUID matches an existing soft-deleted (deleted_at set) dataset and the caller lacks can_write (dataset write permission), ImportFailedError is raised instead of silently returning the deleted row. The guard exists because returning the soft-deleted row would let the importer reattach charts/dashboards to a deleted dataset and produce broken artifacts.","triggerScenarios":"Importing a bundle whose dataset UUID matches a dataset in the trash, via a REST import by a user without can_write permission on datasets (and not ignore_permissions).","commonSituations":"A user re-imports an old export after the dataset was soft-deleted; batch import scripts running under a read-mostly role; importing a dashboard bundle that embeds the deleted dataset's UUID.","solutions":["Grant the importing user/role the can_write dataset permission (or the can_import permission set) and retry — the restore path then runs with proper editorship checks","Have an admin restore the soft-deleted dataset first, then import","Remove the dataset from the bundle if the restore is not intended"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"from superset.daos.dataset import DatasetDAO\nfrom superset.models.slice import Slice  # not needed; illustration of pre-check\n# pre-check: does the uuid match a soft-deleted row, and can we write?\nexisting = find_existing_for_import(SqlaTable, config['uuid'])\nif existing is not None and existing.deleted_at is not None:\n    assert current_user_has_dataset_write(), 're-import of a deleted dataset requires can_write'","typeGuard":null,"tryCatchPattern":"from superset.commands.exceptions import ImportFailedError\ntry:\n    import_dataset(config, overwrite=True)\nexcept ImportFailedError as ex:\n    if 'requires can_write permission to restore it' in str(ex):\n        # elevate/grant permission or drop the dataset from the bundle; do not retry unchanged\n        ...","preventionTips":["Run imports under a role with dataset write permission when restores are expected","Check the bundle's UUIDs against existing/trashed datasets before import","Clean up trash rows (restore or purge) before re-importing old bundles"],"tags":["dataset-import","permissions","soft-delete","rbac","superset"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}