mirror of
https://github.com/apache/superset.git
synced 2026-07-12 09:45:43 +00:00
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
130 lines
5.1 KiB
Python
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()
|