apache/superset · error · DashboardSlugConflictError

Dashboard cannot be restored because its slug is now used by

Error message

Dashboard cannot be restored because its slug is now used by another active dashboard. Rename one of the dashboards and retry.

What it means

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.

Source

Thrown at superset/commands/dashboard/restore.py:63

    restore_failed_exc = DashboardRestoreFailedError

    def validate(self) -> Dashboard:  # type: ignore[override]
        """Extend ``BaseRestoreCommand.validate`` with a slug-conflict pre-check.

        Raises ``DashboardSlugConflictError`` when the dashboard has a
        ``slug`` that has been claimed by another active dashboard while
        this one was soft-deleted. Surfacing the conflict as a domain
        error here keeps callers from seeing an opaque ``IntegrityError``
        at flush time on dialects with the partial index, and a
        constraint-violation 500 on dialects without it.
        """
        model = super().validate()
        # Check ``is not None`` rather than truthiness: an empty-string slug is
        # still subject to the partial unique index, so it must be guarded too
        # (a falsy "" would otherwise skip the pre-check and fail later with an
        # opaque IntegrityError).
        if model.slug is not None and self._has_active_slug_twin(model):
            raise DashboardSlugConflictError()
        return model

    @staticmethod
    def _has_active_slug_twin(model: Dashboard) -> bool:
        """Return True iff another active dashboard already owns this slug.

        Slug uniqueness is enforced only among active rows (via the
        partial index ``ix_dashboards_active_slug``). If the slug has
        been claimed since this dashboard was soft-deleted, the restore
        would create two active rows with the same slug — caught here
        so it surfaces as a readable domain error rather than an opaque
        ``IntegrityError`` at flush time.

        Delegates to ``DashboardDAO.validate_update_slug_uniqueness`` so
        the active-slug-twin rule has exactly one implementation — the
        update path, this explicit restore, and the importer's
        restore-with-update all consult the same predicate (which relies
        on the ``SoftDeleteMixin`` listener to consider only active

View on GitHub (pinned to f4587218dd)

Solutions

  1. 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.
  2. 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.
  3. If the active twin is itself disposable, hard-delete it, then restore.
  4. After restoring, verify only one active dashboard carries the slug (the partial index only guards active rows).

Example fix

# before: restore fails with slug conflict
POST /api/v1/dashboard/42/restore
# -> DashboardSlugConflictError

# after: rename the active twin first, then restore
PUT /api/v1/dashboard/7
{"slug": "sales-old"}
POST /api/v1/dashboard/42/restore
Defensive patterns

Strategy: validation

Validate before calling

from superset.daos.dashboard import DashboardDAO
from superset.models.dashboard import Dashboard

def slug_is_free_for_restore(model: Dashboard) -> bool:
    if model.slug is None:
        return True
    twin = (
        db.session.query(Dashboard.id)
        .filter(Dashboard.slug == model.slug, Dashboard.id != model.id, ~Dashboard.deleted_at.isnot(None) is False)
        .filter(Dashboard.deleted_at.is_(None))
        .first()
    )
    return twin is None

Try / catch

from superset.commands.dashboard.exceptions import DashboardSlugConflictError

try:
    DashboardRestoreCommand(model_id).run()
except DashboardSlugConflictError:
    # rename the active twin, then retry
    ...

Prevention

When it happens

Trigger: 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='').

Common situations: 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.

Related errors


AI-assisted analysis of apache/superset@f4587218dd (2026-08-14). Data as JSON: /api/errors/60f2c2a9880e11a9. Report an issue: GitHub.