{"record":{"id":"60f2c2a9880e11a9","repo":"apache/superset","slug":"dashboard-cannot-be-restored-because-its-slug-is-n","errorCode":null,"errorMessage":"Dashboard cannot be restored because its slug is now used by another active dashboard. Rename one of the dashboards and retry.","messagePattern":"Dashboard cannot be restored because its slug is now used by another active dashboard\\. Rename one of the dashboards and retry\\.","errorType":"exception","errorClass":"DashboardSlugConflictError","httpStatus":422,"severity":"error","filePath":"superset/commands/dashboard/restore.py","lineNumber":63,"sourceCode":"    restore_failed_exc = DashboardRestoreFailedError\n\n    def validate(self) -> Dashboard:  # type: ignore[override]\n        \"\"\"Extend ``BaseRestoreCommand.validate`` with a slug-conflict pre-check.\n\n        Raises ``DashboardSlugConflictError`` when the dashboard has a\n        ``slug`` that has been claimed by another active dashboard while\n        this one was soft-deleted. Surfacing the conflict as a domain\n        error here keeps callers from seeing an opaque ``IntegrityError``\n        at flush time on dialects with the partial index, and a\n        constraint-violation 500 on dialects without it.\n        \"\"\"\n        model = super().validate()\n        # Check ``is not None`` rather than truthiness: an empty-string slug is\n        # still subject to the partial unique index, so it must be guarded too\n        # (a falsy \"\" would otherwise skip the pre-check and fail later with an\n        # opaque IntegrityError).\n        if model.slug is not None and self._has_active_slug_twin(model):\n            raise DashboardSlugConflictError()\n        return model\n\n    @staticmethod\n    def _has_active_slug_twin(model: Dashboard) -> bool:\n        \"\"\"Return True iff another active dashboard already owns this slug.\n\n        Slug uniqueness is enforced only among active rows (via the\n        partial index ``ix_dashboards_active_slug``). If the slug has\n        been claimed since this dashboard was soft-deleted, the restore\n        would create two active rows with the same slug — caught here\n        so it surfaces as a readable domain error rather than an opaque\n        ``IntegrityError`` at flush time.\n\n        Delegates to ``DashboardDAO.validate_update_slug_uniqueness`` so\n        the active-slug-twin rule has exactly one implementation — the\n        update path, this explicit restore, and the importer's\n        restore-with-update all consult the same predicate (which relies\n        on the ``SoftDeleteMixin`` listener to consider only active","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/dashboard/restore.py#L45-L81","documentation":"DashboardSlugConflictError raised by DashboardRestoreCommand.validate (restore.py:63) when the dashboard being restored from the trash has a slug that is now owned by another ACTIVE dashboard. Slug uniqueness among active rows is enforced by the partial index ix_dashboards_active_slug, so restoring would create two active rows with the same slug; the command pre-checks and raises this readable domain error instead of an opaque IntegrityError at flush time. Note the guard checks 'is not None', so even an empty-string slug participates.","triggerScenarios":"POST /api/v1/dashboard/<id>/restore after, while this dashboard was soft-deleted, another dashboard was created or renamed with the same slug (including slug='').","commonSituations":"Delete a dashboard, recreate it with the same slug (e.g. re-importing a bundle), then try to restore the original from trash; two admins working concurrently; bulk import creating a slug twin.","solutions":["Rename or clear the slug of the currently active dashboard that owns the slug (PUT /api/v1/dashboard/<other_id> with {\"slug\": \"new-slug\"}), then retry the restore.","Alternatively remove the slug from the soft-deleted dashboard before restoring — but since it is in the trash, the practical route is renaming the active twin.","If the active twin is itself disposable, hard-delete it, then restore.","After restoring, verify only one active dashboard carries the slug (the partial index only guards active rows)."],"exampleFix":"# before: restore fails with slug conflict\nPOST /api/v1/dashboard/42/restore\n# -> DashboardSlugConflictError\n\n# after: rename the active twin first, then restore\nPUT /api/v1/dashboard/7\n{\"slug\": \"sales-old\"}\nPOST /api/v1/dashboard/42/restore","handlingStrategy":"validation","validationCode":"from superset.daos.dashboard import DashboardDAO\nfrom superset.models.dashboard import Dashboard\n\ndef slug_is_free_for_restore(model: Dashboard) -> bool:\n    if model.slug is None:\n        return True\n    twin = (\n        db.session.query(Dashboard.id)\n        .filter(Dashboard.slug == model.slug, Dashboard.id != model.id, ~Dashboard.deleted_at.isnot(None) is False)\n        .filter(Dashboard.deleted_at.is_(None))\n        .first()\n    )\n    return twin is None","typeGuard":null,"tryCatchPattern":"from superset.commands.dashboard.exceptions import DashboardSlugConflictError\n\ntry:\n    DashboardRestoreCommand(model_id).run()\nexcept DashboardSlugConflictError:\n    # rename the active twin, then retry\n    ...","preventionTips":["Before restoring from trash, check the slug is unclaimed among active dashboards.","When re-importing dashboards after a delete, prefer new slugs rather than recycling the old one.","Remember empty-string slugs conflict too — the guard uses 'is not None', not truthiness."],"tags":["dashboard","restore","slug","conflict","soft-delete"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}