Files
superset2/tests/unit_tests/databases/commands/delete_test.py
Mike Bridge 04f8b700d7 feat(datasets): soft-delete and restore (#40130)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:57:08 -07:00

130 lines
5.1 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
import pytest
from pytest_mock import MockerFixture
from sqlalchemy.orm.session import Session
def test_delete_database_blocked_by_soft_deleted_dataset(
mocker: MockerFixture,
session: Session,
) -> None:
"""A database whose only datasets are soft-deleted must NOT be deletable.
``Database.tables`` now hides soft-deleted datasets (``SqlaTable`` inherits
``SoftDeleteMixin``), so the validation counts datasets with the visibility
filter bypassed — otherwise the database looks empty and gets hard-deleted
while ``tables.database_id`` rows still reference it.
"""
from superset import db
from superset.commands.database.delete import DeleteDatabaseCommand
from superset.commands.database.exceptions import (
DatabaseDeleteSoftDeletedDatasetsExistFailedError,
)
from superset.connectors.sqla.models import SqlaTable
from superset.daos.database import DatabaseDAO
from superset.daos.report import ReportScheduleDAO
from superset.models.core import Database
SqlaTable.metadata.create_all(session.get_bind())
database = Database(database_name="del_db", sqlalchemy_uri="sqlite://")
soft_deleted = SqlaTable(
table_name="gone",
database=database,
deleted_at=datetime(2026, 1, 1, 12, 0, 0),
)
db.session.add_all([database, soft_deleted])
db.session.flush()
# Isolate the dataset check: the report/model lookups are unrelated to the
# soft-delete behaviour under test.
mocker.patch.object(DatabaseDAO, "find_by_id", return_value=database)
mocker.patch.object(ReportScheduleDAO, "find_by_database_id", return_value=[])
command = DeleteDatabaseCommand(database.id)
# The soft-deleted-only branch raises the *specific* subclass so the
# message tells the operator the blockers are hidden rows.
with pytest.raises(DatabaseDeleteSoftDeletedDatasetsExistFailedError):
command.validate()
def test_delete_database_blocked_by_live_dataset(
mocker: MockerFixture,
session: Session,
) -> None:
"""A database with a live (non-deleted) dataset raises the base error, not
the soft-deleted subclass — pinning that the two ``validate`` branches map
to distinct messages."""
from superset import db
from superset.commands.database.delete import DeleteDatabaseCommand
from superset.commands.database.exceptions import (
DatabaseDeleteDatasetsExistFailedError,
DatabaseDeleteSoftDeletedDatasetsExistFailedError,
)
from superset.connectors.sqla.models import SqlaTable
from superset.daos.database import DatabaseDAO
from superset.daos.report import ReportScheduleDAO
from superset.models.core import Database
SqlaTable.metadata.create_all(session.get_bind())
database = Database(database_name="live_db", sqlalchemy_uri="sqlite://")
live = SqlaTable(table_name="present", database=database)
db.session.add_all([database, live])
db.session.flush()
mocker.patch.object(DatabaseDAO, "find_by_id", return_value=database)
mocker.patch.object(ReportScheduleDAO, "find_by_database_id", return_value=[])
command = DeleteDatabaseCommand(database.id)
with pytest.raises(DatabaseDeleteDatasetsExistFailedError) as exc_info:
command.validate()
# The live branch must NOT surface the hidden-rows message.
assert not isinstance(
exc_info.value, DatabaseDeleteSoftDeletedDatasetsExistFailedError
)
def test_delete_database_allowed_when_no_datasets(
mocker: MockerFixture,
session: Session,
) -> None:
"""With no datasets at all (soft-deleted or active), validation passes."""
from superset import db
from superset.commands.database.delete import DeleteDatabaseCommand
from superset.connectors.sqla.models import SqlaTable
from superset.daos.database import DatabaseDAO
from superset.daos.report import ReportScheduleDAO
from superset.models.core import Database
SqlaTable.metadata.create_all(session.get_bind())
database = Database(database_name="empty_db", sqlalchemy_uri="sqlite://")
db.session.add(database)
db.session.flush()
mocker.patch.object(DatabaseDAO, "find_by_id", return_value=database)
mocker.patch.object(ReportScheduleDAO, "find_by_database_id", return_value=[])
command = DeleteDatabaseCommand(database.id)
# Should not raise.
command.validate()