You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jo...@apache.org on 2023/07/12 22:45:35 UTC

[superset] branch master updated: chore(command): Condense delete/bulk-delete operations (#24607)

This is an automated email from the ASF dual-hosted git repository.

johnbodley pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new a156816064 chore(command): Condense delete/bulk-delete operations (#24607)
a156816064 is described below

commit a156816064c47b9ad12f34571f44e02cfb72fe75
Author: John Bodley <45...@users.noreply.github.com>
AuthorDate: Wed Jul 12 15:45:29 2023 -0700

    chore(command): Condense delete/bulk-delete operations (#24607)
    
    Co-authored-by: Michael S. Molina <70...@users.noreply.github.com>
---
 superset/annotation_layers/annotations/api.py      | 10 +---
 .../annotations/commands/bulk_delete.py            | 51 ----------------
 .../annotations/commands/delete.py                 | 14 ++---
 .../annotations/commands/exceptions.py             |  8 +--
 superset/annotation_layers/api.py                  | 13 ++--
 superset/annotation_layers/commands/bulk_delete.py | 54 -----------------
 superset/annotation_layers/commands/delete.py      | 16 ++---
 superset/annotation_layers/commands/exceptions.py  | 12 +---
 superset/charts/api.py                             |  8 +--
 superset/charts/commands/bulk_delete.py            | 70 ----------------------
 superset/charts/commands/delete.py                 | 32 +++++-----
 superset/charts/commands/exceptions.py             | 10 +---
 superset/css_templates/api.py                      |  8 +--
 .../commands/{bulk_delete.py => delete.py}         |  6 +-
 superset/css_templates/commands/exceptions.py      |  4 +-
 superset/dashboards/api.py                         |  8 +--
 superset/dashboards/commands/bulk_delete.py        | 70 ----------------------
 superset/dashboards/commands/delete.py             | 25 ++++----
 superset/dashboards/commands/exceptions.py         | 10 +---
 superset/datasets/api.py                           |  8 +--
 superset/datasets/commands/bulk_delete.py          | 60 -------------------
 superset/datasets/commands/delete.py               | 25 ++++----
 superset/datasets/commands/exceptions.py           |  6 +-
 superset/queries/saved_queries/api.py              | 10 ++--
 .../commands/{bulk_delete.py => delete.py}         |  6 +-
 .../queries/saved_queries/commands/exceptions.py   |  2 +-
 superset/reports/api.py                            |  8 +--
 superset/reports/commands/bulk_delete.py           | 61 -------------------
 superset/reports/commands/delete.py                | 23 +++----
 superset/reports/commands/exceptions.py            |  5 --
 superset/row_level_security/api.py                 |  4 +-
 .../commands/{bulk_delete.py => delete.py}         |  6 +-
 superset/row_level_security/commands/exceptions.py |  4 +-
 tests/integration_tests/datasets/api_tests.py      |  2 +-
 .../security/row_level_security_tests.py           |  2 +-
 35 files changed, 123 insertions(+), 538 deletions(-)

diff --git a/superset/annotation_layers/annotations/api.py b/superset/annotation_layers/annotations/api.py
index 70e0a1ad02..484335b81c 100644
--- a/superset/annotation_layers/annotations/api.py
+++ b/superset/annotation_layers/annotations/api.py
@@ -24,9 +24,6 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_babel import ngettext
 from marshmallow import ValidationError
 
-from superset.annotation_layers.annotations.commands.bulk_delete import (
-    BulkDeleteAnnotationCommand,
-)
 from superset.annotation_layers.annotations.commands.create import (
     CreateAnnotationCommand,
 )
@@ -34,7 +31,6 @@ from superset.annotation_layers.annotations.commands.delete import (
     DeleteAnnotationCommand,
 )
 from superset.annotation_layers.annotations.commands.exceptions import (
-    AnnotationBulkDeleteFailedError,
     AnnotationCreateFailedError,
     AnnotationDeleteFailedError,
     AnnotationInvalidError,
@@ -438,7 +434,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
               $ref: '#/components/responses/500'
         """
         try:
-            DeleteAnnotationCommand(annotation_id).run()
+            DeleteAnnotationCommand([annotation_id]).run()
             return self.response(200, message="OK")
         except AnnotationNotFoundError:
             return self.response_404()
@@ -495,7 +491,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteAnnotationCommand(item_ids).run()
+            DeleteAnnotationCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -506,5 +502,5 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
             )
         except AnnotationNotFoundError:
             return self.response_404()
-        except AnnotationBulkDeleteFailedError as ex:
+        except AnnotationDeleteFailedError as ex:
             return self.response_422(message=str(ex))
diff --git a/superset/annotation_layers/annotations/commands/bulk_delete.py b/superset/annotation_layers/annotations/commands/bulk_delete.py
deleted file mode 100644
index 153f93aa6f..0000000000
--- a/superset/annotation_layers/annotations/commands/bulk_delete.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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.
-import logging
-from typing import Optional
-
-from superset.annotation_layers.annotations.commands.exceptions import (
-    AnnotationBulkDeleteFailedError,
-    AnnotationNotFoundError,
-)
-from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.models.annotations import Annotation
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteAnnotationCommand(BaseCommand):
-    def __init__(self, model_ids: list[int]):
-        self._model_ids = model_ids
-        self._models: Optional[list[Annotation]] = None
-
-    def run(self) -> None:
-        self.validate()
-        assert self._models
-
-        try:
-            AnnotationDAO.delete(self._models)
-        except DAODeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise AnnotationBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = AnnotationDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise AnnotationNotFoundError()
diff --git a/superset/annotation_layers/annotations/commands/delete.py b/superset/annotation_layers/annotations/commands/delete.py
index bbd7f39dd6..2850f8cb96 100644
--- a/superset/annotation_layers/annotations/commands/delete.py
+++ b/superset/annotation_layers/annotations/commands/delete.py
@@ -30,22 +30,22 @@ logger = logging.getLogger(__name__)
 
 
 class DeleteAnnotationCommand(BaseCommand):
-    def __init__(self, model_id: int):
-        self._model_id = model_id
-        self._model: Optional[Annotation] = None
+    def __init__(self, model_ids: list[int]):
+        self._model_ids = model_ids
+        self._models: Optional[list[Annotation]] = None
 
     def run(self) -> None:
         self.validate()
-        assert self._model
+        assert self._models
 
         try:
-            AnnotationDAO.delete(self._model)
+            AnnotationDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
             raise AnnotationDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
-        self._model = AnnotationDAO.find_by_id(self._model_id)
-        if not self._model:
+        self._models = AnnotationDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
             raise AnnotationNotFoundError()
diff --git a/superset/annotation_layers/annotations/commands/exceptions.py b/superset/annotation_layers/annotations/commands/exceptions.py
index e7fc93ecff..dcdf42cc85 100644
--- a/superset/annotation_layers/annotations/commands/exceptions.py
+++ b/superset/annotation_layers/annotations/commands/exceptions.py
@@ -48,10 +48,6 @@ class AnnotationUniquenessValidationError(ValidationError):
         )
 
 
-class AnnotationBulkDeleteFailedError(DeleteFailedError):
-    message = _("Annotations could not be deleted.")
-
-
 class AnnotationNotFoundError(CommandException):
     message = _("Annotation not found.")
 
@@ -68,5 +64,5 @@ class AnnotationUpdateFailedError(CreateFailedError):
     message = _("Annotation could not be updated.")
 
 
-class AnnotationDeleteFailedError(CommandException):
-    message = _("Annotation delete failed.")
+class AnnotationDeleteFailedError(DeleteFailedError):
+    message = _("Annotations could not be deleted.")
diff --git a/superset/annotation_layers/api.py b/superset/annotation_layers/api.py
index 25e995b611..2e64fec78c 100644
--- a/superset/annotation_layers/api.py
+++ b/superset/annotation_layers/api.py
@@ -23,14 +23,9 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_babel import ngettext
 from marshmallow import ValidationError
 
-from superset.annotation_layers.commands.bulk_delete import (
-    BulkDeleteAnnotationLayerCommand,
-)
 from superset.annotation_layers.commands.create import CreateAnnotationLayerCommand
 from superset.annotation_layers.commands.delete import DeleteAnnotationLayerCommand
 from superset.annotation_layers.commands.exceptions import (
-    AnnotationLayerBulkDeleteFailedError,
-    AnnotationLayerBulkDeleteIntegrityError,
     AnnotationLayerCreateFailedError,
     AnnotationLayerDeleteFailedError,
     AnnotationLayerDeleteIntegrityError,
@@ -151,7 +146,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
               $ref: '#/components/responses/500'
         """
         try:
-            DeleteAnnotationLayerCommand(pk).run()
+            DeleteAnnotationLayerCommand([pk]).run()
             return self.response(200, message="OK")
         except AnnotationLayerNotFoundError:
             return self.response_404()
@@ -346,7 +341,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteAnnotationLayerCommand(item_ids).run()
+            DeleteAnnotationLayerCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -357,7 +352,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
             )
         except AnnotationLayerNotFoundError:
             return self.response_404()
-        except AnnotationLayerBulkDeleteIntegrityError as ex:
+        except AnnotationLayerDeleteIntegrityError as ex:
             return self.response_422(message=str(ex))
-        except AnnotationLayerBulkDeleteFailedError as ex:
+        except AnnotationLayerDeleteFailedError as ex:
             return self.response_422(message=str(ex))
diff --git a/superset/annotation_layers/commands/bulk_delete.py b/superset/annotation_layers/commands/bulk_delete.py
deleted file mode 100644
index a227fc3266..0000000000
--- a/superset/annotation_layers/commands/bulk_delete.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# 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.
-import logging
-from typing import Optional
-
-from superset.annotation_layers.commands.exceptions import (
-    AnnotationLayerBulkDeleteFailedError,
-    AnnotationLayerBulkDeleteIntegrityError,
-    AnnotationLayerNotFoundError,
-)
-from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationLayerDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.models.annotations import AnnotationLayer
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteAnnotationLayerCommand(BaseCommand):
-    def __init__(self, model_ids: list[int]):
-        self._model_ids = model_ids
-        self._models: Optional[list[AnnotationLayer]] = None
-
-    def run(self) -> None:
-        self.validate()
-        assert self._models
-
-        try:
-            AnnotationLayerDAO.delete(self._models)
-        except DAODeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise AnnotationLayerBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = AnnotationLayerDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise AnnotationLayerNotFoundError()
-        if AnnotationLayerDAO.has_annotations(self._model_ids):
-            raise AnnotationLayerBulkDeleteIntegrityError()
diff --git a/superset/annotation_layers/commands/delete.py b/superset/annotation_layers/commands/delete.py
index 14f53272f1..41c727054b 100644
--- a/superset/annotation_layers/commands/delete.py
+++ b/superset/annotation_layers/commands/delete.py
@@ -31,24 +31,24 @@ logger = logging.getLogger(__name__)
 
 
 class DeleteAnnotationLayerCommand(BaseCommand):
-    def __init__(self, model_id: int):
-        self._model_id = model_id
-        self._model: Optional[AnnotationLayer] = None
+    def __init__(self, model_ids: list[int]):
+        self._model_ids = model_ids
+        self._models: Optional[list[AnnotationLayer]] = None
 
     def run(self) -> None:
         self.validate()
-        assert self._model
+        assert self._models
 
         try:
-            AnnotationLayerDAO.delete(self._model)
+            AnnotationLayerDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
             raise AnnotationLayerDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
-        self._model = AnnotationLayerDAO.find_by_id(self._model_id)
-        if not self._model:
+        self._models = AnnotationLayerDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
             raise AnnotationLayerNotFoundError()
-        if AnnotationLayerDAO.has_annotations(self._model.id):
+        if AnnotationLayerDAO.has_annotations(self._model_ids):
             raise AnnotationLayerDeleteIntegrityError()
diff --git a/superset/annotation_layers/commands/exceptions.py b/superset/annotation_layers/commands/exceptions.py
index 584962321b..a9e6e97ecf 100644
--- a/superset/annotation_layers/commands/exceptions.py
+++ b/superset/annotation_layers/commands/exceptions.py
@@ -29,10 +29,6 @@ class AnnotationLayerInvalidError(CommandInvalidError):
     message = _("Annotation layer parameters are invalid.")
 
 
-class AnnotationLayerBulkDeleteFailedError(DeleteFailedError):
-    message = _("Annotation layer could not be deleted.")
-
-
 class AnnotationLayerCreateFailedError(CreateFailedError):
     message = _("Annotation layer could not be created.")
 
@@ -45,18 +41,14 @@ class AnnotationLayerNotFoundError(CommandException):
     message = _("Annotation layer not found.")
 
 
-class AnnotationLayerDeleteFailedError(CommandException):
-    message = _("Annotation layer delete failed.")
+class AnnotationLayerDeleteFailedError(DeleteFailedError):
+    message = _("Annotation layers could not be deleted.")
 
 
 class AnnotationLayerDeleteIntegrityError(CommandException):
     message = _("Annotation layer has associated annotations.")
 
 
-class AnnotationLayerBulkDeleteIntegrityError(CommandException):
-    message = _("Annotation layer has associated annotations.")
-
-
 class AnnotationLayerNameUniquenessValidationError(ValidationError):
     """
     Marshmallow validation error for annotation layer name already exists
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 8cfe9fa4c2..a060b23b0e 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -32,11 +32,9 @@ from werkzeug.wrappers import Response as WerkzeugResponse
 from werkzeug.wsgi import FileWrapper
 
 from superset import app, is_feature_enabled, thumbnail_cache
-from superset.charts.commands.bulk_delete import BulkDeleteChartCommand
 from superset.charts.commands.create import CreateChartCommand
 from superset.charts.commands.delete import DeleteChartCommand
 from superset.charts.commands.exceptions import (
-    ChartBulkDeleteFailedError,
     ChartCreateFailedError,
     ChartDeleteFailedError,
     ChartForbiddenError,
@@ -461,7 +459,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
               $ref: '#/components/responses/500'
         """
         try:
-            DeleteChartCommand(pk).run()
+            DeleteChartCommand([pk]).run()
             return self.response(200, message="OK")
         except ChartNotFoundError:
             return self.response_404()
@@ -521,7 +519,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteChartCommand(item_ids).run()
+            DeleteChartCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -532,7 +530,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
             return self.response_404()
         except ChartForbiddenError:
             return self.response_403()
-        except ChartBulkDeleteFailedError as ex:
+        except ChartDeleteFailedError as ex:
             return self.response_422(message=str(ex))
 
     @expose("/<pk>/cache_screenshot/", methods=("GET",))
diff --git a/superset/charts/commands/bulk_delete.py b/superset/charts/commands/bulk_delete.py
deleted file mode 100644
index 0555992e24..0000000000
--- a/superset/charts/commands/bulk_delete.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# 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.
-import logging
-from typing import Optional
-
-from flask_babel import lazy_gettext as _
-
-from superset import security_manager
-from superset.charts.commands.exceptions import (
-    ChartBulkDeleteFailedError,
-    ChartBulkDeleteFailedReportsExistError,
-    ChartForbiddenError,
-    ChartNotFoundError,
-)
-from superset.commands.base import BaseCommand
-from superset.commands.exceptions import DeleteFailedError
-from superset.daos.chart import ChartDAO
-from superset.daos.report import ReportScheduleDAO
-from superset.exceptions import SupersetSecurityException
-from superset.models.slice import Slice
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteChartCommand(BaseCommand):
-    def __init__(self, model_ids: list[int]):
-        self._model_ids = model_ids
-        self._models: Optional[list[Slice]] = None
-
-    def run(self) -> None:
-        self.validate()
-        assert self._models
-
-        try:
-            ChartDAO.delete(self._models)
-        except DeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise ChartBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = ChartDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise ChartNotFoundError()
-        # Check there are no associated ReportSchedules
-        if reports := ReportScheduleDAO.find_by_chart_ids(self._model_ids):
-            report_names = [report.name for report in reports]
-            raise ChartBulkDeleteFailedReportsExistError(
-                _("There are associated alerts or reports: %s" % ",".join(report_names))
-            )
-        # Check ownership
-        for model in self._models:
-            try:
-                security_manager.raise_for_ownership(model)
-            except SupersetSecurityException as ex:
-                raise ChartForbiddenError() from ex
diff --git a/superset/charts/commands/delete.py b/superset/charts/commands/delete.py
index 72d9f9a732..1b3d93f063 100644
--- a/superset/charts/commands/delete.py
+++ b/superset/charts/commands/delete.py
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 import logging
-from typing import cast, Optional
+from typing import Optional
 
 from flask_babel import lazy_gettext as _
 
@@ -38,33 +38,37 @@ logger = logging.getLogger(__name__)
 
 
 class DeleteChartCommand(BaseCommand):
-    def __init__(self, model_id: int):
-        self._model_id = model_id
-        self._model: Optional[Slice] = None
+    def __init__(self, model_ids: list[int]):
+        self._model_ids = model_ids
+        self._models: Optional[list[Slice]] = None
 
     def run(self) -> None:
         self.validate()
-        self._model = cast(Slice, self._model)
+        assert self._models
+
+        for model_id in self._model_ids:
+            Dashboard.clear_cache_for_slice(slice_id=model_id)
+
         try:
-            Dashboard.clear_cache_for_slice(slice_id=self._model_id)
-            ChartDAO.delete(self._model)
+            ChartDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
             raise ChartDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
-        self._model = ChartDAO.find_by_id(self._model_id)
-        if not self._model:
+        self._models = ChartDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
             raise ChartNotFoundError()
         # Check there are no associated ReportSchedules
-        if reports := ReportScheduleDAO.find_by_chart_id(self._model_id):
+        if reports := ReportScheduleDAO.find_by_chart_ids(self._model_ids):
             report_names = [report.name for report in reports]
             raise ChartDeleteFailedReportsExistError(
                 _("There are associated alerts or reports: %s" % ",".join(report_names))
             )
         # Check ownership
-        try:
-            security_manager.raise_for_ownership(self._model)
-        except SupersetSecurityException as ex:
-            raise ChartForbiddenError() from ex
+        for model in self._models:
+            try:
+                security_manager.raise_for_ownership(model)
+            except SupersetSecurityException as ex:
+                raise ChartForbiddenError() from ex
diff --git a/superset/charts/commands/exceptions.py b/superset/charts/commands/exceptions.py
index 83792ae252..00877aa803 100644
--- a/superset/charts/commands/exceptions.py
+++ b/superset/charts/commands/exceptions.py
@@ -120,7 +120,7 @@ class ChartUpdateFailedError(UpdateFailedError):
 
 
 class ChartDeleteFailedError(DeleteFailedError):
-    message = _("Chart could not be deleted.")
+    message = _("Charts could not be deleted.")
 
 
 class ChartDeleteFailedReportsExistError(ChartDeleteFailedError):
@@ -135,10 +135,6 @@ class ChartForbiddenError(ForbiddenError):
     message = _("Changing this chart is forbidden")
 
 
-class ChartBulkDeleteFailedError(DeleteFailedError):
-    message = _("Charts could not be deleted.")
-
-
 class ChartDataQueryFailedError(CommandException):
     pass
 
@@ -147,10 +143,6 @@ class ChartDataCacheLoadError(CommandException):
     pass
 
 
-class ChartBulkDeleteFailedReportsExistError(ChartBulkDeleteFailedError):
-    message = _("There are associated alerts or reports")
-
-
 class ChartImportError(ImportFailedError):
     message = _("Import chart failed for an unknown reason")
 
diff --git a/superset/css_templates/api.py b/superset/css_templates/api.py
index 3f0980f2ff..f7688afe03 100644
--- a/superset/css_templates/api.py
+++ b/superset/css_templates/api.py
@@ -23,9 +23,9 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_babel import ngettext
 
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
-from superset.css_templates.commands.bulk_delete import BulkDeleteCssTemplateCommand
+from superset.css_templates.commands.delete import DeleteCssTemplateCommand
 from superset.css_templates.commands.exceptions import (
-    CssTemplateBulkDeleteFailedError,
+    CssTemplateDeleteFailedError,
     CssTemplateNotFoundError,
 )
 from superset.css_templates.filters import CssTemplateAllTextFilter
@@ -130,7 +130,7 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteCssTemplateCommand(item_ids).run()
+            DeleteCssTemplateCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -141,5 +141,5 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
             )
         except CssTemplateNotFoundError:
             return self.response_404()
-        except CssTemplateBulkDeleteFailedError as ex:
+        except CssTemplateDeleteFailedError as ex:
             return self.response_422(message=str(ex))
diff --git a/superset/css_templates/commands/bulk_delete.py b/superset/css_templates/commands/delete.py
similarity index 92%
rename from superset/css_templates/commands/bulk_delete.py
rename to superset/css_templates/commands/delete.py
index ef13a1c8e2..123658cb45 100644
--- a/superset/css_templates/commands/bulk_delete.py
+++ b/superset/css_templates/commands/delete.py
@@ -19,7 +19,7 @@ from typing import Optional
 
 from superset.commands.base import BaseCommand
 from superset.css_templates.commands.exceptions import (
-    CssTemplateBulkDeleteFailedError,
+    CssTemplateDeleteFailedError,
     CssTemplateNotFoundError,
 )
 from superset.daos.css import CssTemplateDAO
@@ -29,7 +29,7 @@ from superset.models.core import CssTemplate
 logger = logging.getLogger(__name__)
 
 
-class BulkDeleteCssTemplateCommand(BaseCommand):
+class DeleteCssTemplateCommand(BaseCommand):
     def __init__(self, model_ids: list[int]):
         self._model_ids = model_ids
         self._models: Optional[list[CssTemplate]] = None
@@ -42,7 +42,7 @@ class BulkDeleteCssTemplateCommand(BaseCommand):
             CssTemplateDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
-            raise CssTemplateBulkDeleteFailedError() from ex
+            raise CssTemplateDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
diff --git a/superset/css_templates/commands/exceptions.py b/superset/css_templates/commands/exceptions.py
index d950822cfa..9551758106 100644
--- a/superset/css_templates/commands/exceptions.py
+++ b/superset/css_templates/commands/exceptions.py
@@ -19,8 +19,8 @@ from flask_babel import lazy_gettext as _
 from superset.commands.exceptions import CommandException, DeleteFailedError
 
 
-class CssTemplateBulkDeleteFailedError(DeleteFailedError):
-    message = _("CSS template could not be deleted.")
+class CssTemplateDeleteFailedError(DeleteFailedError):
+    message = _("CSS templates could not be deleted.")
 
 
 class CssTemplateNotFoundError(CommandException):
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 781ad6f6c7..8464c87444 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -39,12 +39,10 @@ from superset.commands.importers.exceptions import NoValidFilesFoundError
 from superset.commands.importers.v1.utils import get_contents_from_bundle
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.daos.dashboard import DashboardDAO, EmbeddedDashboardDAO
-from superset.dashboards.commands.bulk_delete import BulkDeleteDashboardCommand
 from superset.dashboards.commands.create import CreateDashboardCommand
 from superset.dashboards.commands.delete import DeleteDashboardCommand
 from superset.dashboards.commands.exceptions import (
     DashboardAccessDeniedError,
-    DashboardBulkDeleteFailedError,
     DashboardCreateFailedError,
     DashboardDeleteFailedError,
     DashboardForbiddenError,
@@ -674,7 +672,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
               $ref: '#/components/responses/500'
         """
         try:
-            DeleteDashboardCommand(pk).run()
+            DeleteDashboardCommand([pk]).run()
             return self.response(200, message="OK")
         except DashboardNotFoundError:
             return self.response_404()
@@ -734,7 +732,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteDashboardCommand(item_ids).run()
+            DeleteDashboardCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -747,7 +745,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
             return self.response_404()
         except DashboardForbiddenError:
             return self.response_403()
-        except DashboardBulkDeleteFailedError as ex:
+        except DashboardDeleteFailedError as ex:
             return self.response_422(message=str(ex))
 
     @expose("/export/", methods=("GET",))
diff --git a/superset/dashboards/commands/bulk_delete.py b/superset/dashboards/commands/bulk_delete.py
deleted file mode 100644
index 707f0d722a..0000000000
--- a/superset/dashboards/commands/bulk_delete.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# 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.
-import logging
-from typing import Optional
-
-from flask_babel import lazy_gettext as _
-
-from superset import security_manager
-from superset.commands.base import BaseCommand
-from superset.commands.exceptions import DeleteFailedError
-from superset.daos.dashboard import DashboardDAO
-from superset.daos.report import ReportScheduleDAO
-from superset.dashboards.commands.exceptions import (
-    DashboardBulkDeleteFailedError,
-    DashboardBulkDeleteFailedReportsExistError,
-    DashboardForbiddenError,
-    DashboardNotFoundError,
-)
-from superset.exceptions import SupersetSecurityException
-from superset.models.dashboard import Dashboard
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteDashboardCommand(BaseCommand):
-    def __init__(self, model_ids: list[int]):
-        self._model_ids = model_ids
-        self._models: Optional[list[Dashboard]] = None
-
-    def run(self) -> None:
-        self.validate()
-        assert self._models
-
-        try:
-            DashboardDAO.delete(self._models)
-        except DeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise DashboardBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = DashboardDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise DashboardNotFoundError()
-        # Check there are no associated ReportSchedules
-        if reports := ReportScheduleDAO.find_by_dashboard_ids(self._model_ids):
-            report_names = [report.name for report in reports]
-            raise DashboardBulkDeleteFailedReportsExistError(
-                _("There are associated alerts or reports: %s" % ",".join(report_names))
-            )
-        # Check ownership
-        for model in self._models:
-            try:
-                security_manager.raise_for_ownership(model)
-            except SupersetSecurityException as ex:
-                raise DashboardForbiddenError() from ex
diff --git a/superset/dashboards/commands/delete.py b/superset/dashboards/commands/delete.py
index 13b92977df..23e38093ab 100644
--- a/superset/dashboards/commands/delete.py
+++ b/superset/dashboards/commands/delete.py
@@ -37,33 +37,34 @@ logger = logging.getLogger(__name__)
 
 
 class DeleteDashboardCommand(BaseCommand):
-    def __init__(self, model_id: int):
-        self._model_id = model_id
-        self._model: Optional[Dashboard] = None
+    def __init__(self, model_ids: list[int]):
+        self._model_ids = model_ids
+        self._models: Optional[list[Dashboard]] = None
 
     def run(self) -> None:
         self.validate()
-        assert self._model
+        assert self._models
 
         try:
-            DashboardDAO.delete(self._model)
+            DashboardDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
             raise DashboardDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
-        self._model = DashboardDAO.find_by_id(self._model_id)
-        if not self._model:
+        self._models = DashboardDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
             raise DashboardNotFoundError()
         # Check there are no associated ReportSchedules
-        if reports := ReportScheduleDAO.find_by_dashboard_id(self._model_id):
+        if reports := ReportScheduleDAO.find_by_dashboard_ids(self._model_ids):
             report_names = [report.name for report in reports]
             raise DashboardDeleteFailedReportsExistError(
                 _("There are associated alerts or reports: %s" % ",".join(report_names))
             )
         # Check ownership
-        try:
-            security_manager.raise_for_ownership(self._model)
-        except SupersetSecurityException as ex:
-            raise DashboardForbiddenError() from ex
+        for model in self._models:
+            try:
+                security_manager.raise_for_ownership(model)
+            except SupersetSecurityException as ex:
+                raise DashboardForbiddenError() from ex
diff --git a/superset/dashboards/commands/exceptions.py b/superset/dashboards/commands/exceptions.py
index 1a5bdaf789..19184b894c 100644
--- a/superset/dashboards/commands/exceptions.py
+++ b/superset/dashboards/commands/exceptions.py
@@ -51,15 +51,7 @@ class DashboardNotFoundError(ObjectNotFoundError):
 
 
 class DashboardCreateFailedError(CreateFailedError):
-    message = _("Dashboard could not be created.")
-
-
-class DashboardBulkDeleteFailedError(CreateFailedError):
-    message = _("Dashboards could not be deleted.")
-
-
-class DashboardBulkDeleteFailedReportsExistError(DashboardBulkDeleteFailedError):
-    message = _("There are associated alerts or reports")
+    message = _("Dashboards could not be created.")
 
 
 class DashboardUpdateFailedError(UpdateFailedError):
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 87e1d9e74c..d5a0478c5d 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -37,12 +37,10 @@ from superset.connectors.sqla.models import SqlaTable
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.daos.dataset import DatasetDAO
 from superset.databases.filters import DatabaseFilter
-from superset.datasets.commands.bulk_delete import BulkDeleteDatasetCommand
 from superset.datasets.commands.create import CreateDatasetCommand
 from superset.datasets.commands.delete import DeleteDatasetCommand
 from superset.datasets.commands.duplicate import DuplicateDatasetCommand
 from superset.datasets.commands.exceptions import (
-    DatasetBulkDeleteFailedError,
     DatasetCreateFailedError,
     DatasetDeleteFailedError,
     DatasetForbiddenError,
@@ -453,7 +451,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
               $ref: '#/components/responses/500'
         """
         try:
-            DeleteDatasetCommand(pk).run()
+            DeleteDatasetCommand([pk]).run()
             return self.response(200, message="OK")
         except DatasetNotFoundError:
             return self.response_404()
@@ -788,7 +786,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteDatasetCommand(item_ids).run()
+            DeleteDatasetCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -801,7 +799,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
             return self.response_404()
         except DatasetForbiddenError:
             return self.response_403()
-        except DatasetBulkDeleteFailedError as ex:
+        except DatasetDeleteFailedError as ex:
             return self.response_422(message=str(ex))
 
     @expose("/import/", methods=("POST",))
diff --git a/superset/datasets/commands/bulk_delete.py b/superset/datasets/commands/bulk_delete.py
deleted file mode 100644
index 48515bbc6a..0000000000
--- a/superset/datasets/commands/bulk_delete.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# 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.
-import logging
-from typing import Optional
-
-from superset import security_manager
-from superset.commands.base import BaseCommand
-from superset.commands.exceptions import DeleteFailedError
-from superset.connectors.sqla.models import SqlaTable
-from superset.daos.dataset import DatasetDAO
-from superset.datasets.commands.exceptions import (
-    DatasetBulkDeleteFailedError,
-    DatasetForbiddenError,
-    DatasetNotFoundError,
-)
-from superset.exceptions import SupersetSecurityException
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteDatasetCommand(BaseCommand):
-    def __init__(self, model_ids: list[int]):
-        self._model_ids = model_ids
-        self._models: Optional[list[SqlaTable]] = None
-
-    def run(self) -> None:
-        self.validate()
-        assert self._models
-
-        try:
-            DatasetDAO.delete(self._models)
-        except DeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise DatasetBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = DatasetDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise DatasetNotFoundError()
-        # Check ownership
-        for model in self._models:
-            try:
-                security_manager.raise_for_ownership(model)
-            except SupersetSecurityException as ex:
-                raise DatasetForbiddenError() from ex
diff --git a/superset/datasets/commands/delete.py b/superset/datasets/commands/delete.py
index 3e7fc7d5a3..478267d01d 100644
--- a/superset/datasets/commands/delete.py
+++ b/superset/datasets/commands/delete.py
@@ -33,27 +33,28 @@ logger = logging.getLogger(__name__)
 
 
 class DeleteDatasetCommand(BaseCommand):
-    def __init__(self, model_id: int):
-        self._model_id = model_id
-        self._model: Optional[SqlaTable] = None
+    def __init__(self, model_ids: list[int]):
+        self._model_ids = model_ids
+        self._models: Optional[list[SqlaTable]] = None
 
     def run(self) -> None:
         self.validate()
-        assert self._model
+        assert self._models
 
         try:
-            return DatasetDAO.delete(self._model)
+            DatasetDAO.delete(self._models)
         except DAODeleteFailedError as ex:
-            logger.exception(ex)
+            logger.exception(ex.exception)
             raise DatasetDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
-        self._model = DatasetDAO.find_by_id(self._model_id)
-        if not self._model:
+        self._models = DatasetDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
             raise DatasetNotFoundError()
         # Check ownership
-        try:
-            security_manager.raise_for_ownership(self._model)
-        except SupersetSecurityException as ex:
-            raise DatasetForbiddenError() from ex
+        for model in self._models:
+            try:
+                security_manager.raise_for_ownership(model)
+            except SupersetSecurityException as ex:
+                raise DatasetForbiddenError() from ex
diff --git a/superset/datasets/commands/exceptions.py b/superset/datasets/commands/exceptions.py
index 7c6ef86634..fe9fe94cc6 100644
--- a/superset/datasets/commands/exceptions.py
+++ b/superset/datasets/commands/exceptions.py
@@ -179,11 +179,7 @@ class DatasetUpdateFailedError(UpdateFailedError):
 
 
 class DatasetDeleteFailedError(DeleteFailedError):
-    message = _("Dataset could not be deleted.")
-
-
-class DatasetBulkDeleteFailedError(DeleteFailedError):
-    message = _("Dataset(s) could not be bulk deleted.")
+    message = _("Datasets could not be deleted.")
 
 
 class DatasetRefreshFailedError(UpdateFailedError):
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index c6e980c5de..327a2ac4cd 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -36,11 +36,9 @@ from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.databases.filters import DatabaseFilter
 from superset.extensions import event_logger
 from superset.models.sql_lab import SavedQuery
-from superset.queries.saved_queries.commands.bulk_delete import (
-    BulkDeleteSavedQueryCommand,
-)
+from superset.queries.saved_queries.commands.delete import DeleteSavedQueryCommand
 from superset.queries.saved_queries.commands.exceptions import (
-    SavedQueryBulkDeleteFailedError,
+    SavedQueryDeleteFailedError,
     SavedQueryNotFoundError,
 )
 from superset.queries.saved_queries.commands.export import ExportSavedQueriesCommand
@@ -213,7 +211,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteSavedQueryCommand(item_ids).run()
+            DeleteSavedQueryCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -224,7 +222,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
             )
         except SavedQueryNotFoundError:
             return self.response_404()
-        except SavedQueryBulkDeleteFailedError as ex:
+        except SavedQueryDeleteFailedError as ex:
             return self.response_422(message=str(ex))
 
     @expose("/export/", methods=("GET",))
diff --git a/superset/queries/saved_queries/commands/bulk_delete.py b/superset/queries/saved_queries/commands/delete.py
similarity index 92%
rename from superset/queries/saved_queries/commands/bulk_delete.py
rename to superset/queries/saved_queries/commands/delete.py
index 4e68f6b79d..40b73658e0 100644
--- a/superset/queries/saved_queries/commands/bulk_delete.py
+++ b/superset/queries/saved_queries/commands/delete.py
@@ -22,14 +22,14 @@ from superset.daos.exceptions import DAODeleteFailedError
 from superset.daos.query import SavedQueryDAO
 from superset.models.dashboard import Dashboard
 from superset.queries.saved_queries.commands.exceptions import (
-    SavedQueryBulkDeleteFailedError,
+    SavedQueryDeleteFailedError,
     SavedQueryNotFoundError,
 )
 
 logger = logging.getLogger(__name__)
 
 
-class BulkDeleteSavedQueryCommand(BaseCommand):
+class DeleteSavedQueryCommand(BaseCommand):
     def __init__(self, model_ids: list[int]):
         self._model_ids = model_ids
         self._models: Optional[list[Dashboard]] = None
@@ -42,7 +42,7 @@ class BulkDeleteSavedQueryCommand(BaseCommand):
             SavedQueryDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
-            raise SavedQueryBulkDeleteFailedError() from ex
+            raise SavedQueryDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
diff --git a/superset/queries/saved_queries/commands/exceptions.py b/superset/queries/saved_queries/commands/exceptions.py
index 7318573524..20797955e3 100644
--- a/superset/queries/saved_queries/commands/exceptions.py
+++ b/superset/queries/saved_queries/commands/exceptions.py
@@ -24,7 +24,7 @@ from superset.commands.exceptions import (
 )
 
 
-class SavedQueryBulkDeleteFailedError(DeleteFailedError):
+class SavedQueryDeleteFailedError(DeleteFailedError):
     message = _("Saved queries could not be deleted.")
 
 
diff --git a/superset/reports/api.py b/superset/reports/api.py
index 3686ab74bd..eb6ddcfa7f 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -30,11 +30,9 @@ from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.dashboards.filters import DashboardAccessFilter
 from superset.databases.filters import DatabaseFilter
 from superset.extensions import event_logger
-from superset.reports.commands.bulk_delete import BulkDeleteReportScheduleCommand
 from superset.reports.commands.create import CreateReportScheduleCommand
 from superset.reports.commands.delete import DeleteReportScheduleCommand
 from superset.reports.commands.exceptions import (
-    ReportScheduleBulkDeleteFailedError,
     ReportScheduleCreateFailedError,
     ReportScheduleDeleteFailedError,
     ReportScheduleForbiddenError,
@@ -278,7 +276,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
               $ref: '#/components/responses/500'
         """
         try:
-            DeleteReportScheduleCommand(pk).run()
+            DeleteReportScheduleCommand([pk]).run()
             return self.response(200, message="OK")
         except ReportScheduleNotFoundError:
             return self.response_404()
@@ -495,7 +493,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteReportScheduleCommand(item_ids).run()
+            DeleteReportScheduleCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
@@ -508,5 +506,5 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
             return self.response_404()
         except ReportScheduleForbiddenError:
             return self.response_403()
-        except ReportScheduleBulkDeleteFailedError as ex:
+        except ReportScheduleDeleteFailedError as ex:
             return self.response_422(message=str(ex))
diff --git a/superset/reports/commands/bulk_delete.py b/superset/reports/commands/bulk_delete.py
deleted file mode 100644
index 3e3df3d100..0000000000
--- a/superset/reports/commands/bulk_delete.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# 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.
-import logging
-from typing import Optional
-
-from superset import security_manager
-from superset.commands.base import BaseCommand
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.report import ReportScheduleDAO
-from superset.exceptions import SupersetSecurityException
-from superset.reports.commands.exceptions import (
-    ReportScheduleBulkDeleteFailedError,
-    ReportScheduleForbiddenError,
-    ReportScheduleNotFoundError,
-)
-from superset.reports.models import ReportSchedule
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteReportScheduleCommand(BaseCommand):
-    def __init__(self, model_ids: list[int]):
-        self._model_ids = model_ids
-        self._models: Optional[list[ReportSchedule]] = None
-
-    def run(self) -> None:
-        self.validate()
-        assert self._models
-
-        try:
-            ReportScheduleDAO.delete(self._models)
-        except DAODeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise ReportScheduleBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = ReportScheduleDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise ReportScheduleNotFoundError()
-
-        # Check ownership
-        for model in self._models:
-            try:
-                security_manager.raise_for_ownership(model)
-            except SupersetSecurityException as ex:
-                raise ReportScheduleForbiddenError() from ex
diff --git a/superset/reports/commands/delete.py b/superset/reports/commands/delete.py
index 4f9be93f8a..2cdac17c4d 100644
--- a/superset/reports/commands/delete.py
+++ b/superset/reports/commands/delete.py
@@ -33,28 +33,29 @@ logger = logging.getLogger(__name__)
 
 
 class DeleteReportScheduleCommand(BaseCommand):
-    def __init__(self, model_id: int):
-        self._model_id = model_id
-        self._model: Optional[ReportSchedule] = None
+    def __init__(self, model_ids: list[int]):
+        self._model_ids = model_ids
+        self._models: Optional[list[ReportSchedule]] = None
 
     def run(self) -> None:
         self.validate()
-        assert self._model
+        assert self._models
 
         try:
-            ReportScheduleDAO.delete(self._model)
+            ReportScheduleDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
             raise ReportScheduleDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
-        self._model = ReportScheduleDAO.find_by_id(self._model_id)
-        if not self._model:
+        self._models = ReportScheduleDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
             raise ReportScheduleNotFoundError()
 
         # Check ownership
-        try:
-            security_manager.raise_for_ownership(self._model)
-        except SupersetSecurityException as ex:
-            raise ReportScheduleForbiddenError() from ex
+        for model in self._models:
+            try:
+                security_manager.raise_for_ownership(model)
+            except SupersetSecurityException as ex:
+                raise ReportScheduleForbiddenError() from ex
diff --git a/superset/reports/commands/exceptions.py b/superset/reports/commands/exceptions.py
index cba12e0786..2d82d5c312 100644
--- a/superset/reports/commands/exceptions.py
+++ b/superset/reports/commands/exceptions.py
@@ -21,7 +21,6 @@ from superset.commands.exceptions import (
     CommandException,
     CommandInvalidError,
     CreateFailedError,
-    DeleteFailedError,
     ForbiddenError,
     ValidationError,
 )
@@ -125,10 +124,6 @@ class ReportScheduleInvalidError(CommandInvalidError):
     message = _("Report Schedule parameters are invalid.")
 
 
-class ReportScheduleBulkDeleteFailedError(DeleteFailedError):
-    message = _("Report Schedule could not be deleted.")
-
-
 class ReportScheduleCreateFailedError(CreateFailedError):
     message = _("Report Schedule could not be created.")
 
diff --git a/superset/row_level_security/api.py b/superset/row_level_security/api.py
index 43912689fb..7bf00e92f7 100644
--- a/superset/row_level_security/api.py
+++ b/superset/row_level_security/api.py
@@ -32,8 +32,8 @@ from superset.connectors.sqla.models import RowLevelSecurityFilter
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.daos.exceptions import DAOCreateFailedError, DAOUpdateFailedError
 from superset.extensions import event_logger
-from superset.row_level_security.commands.bulk_delete import BulkDeleteRLSRuleCommand
 from superset.row_level_security.commands.create import CreateRLSRuleCommand
+from superset.row_level_security.commands.delete import DeleteRLSRuleCommand
 from superset.row_level_security.commands.exceptions import RLSRuleNotFoundError
 from superset.row_level_security.commands.update import UpdateRLSRuleCommand
 from superset.row_level_security.schemas import (
@@ -340,7 +340,7 @@ class RLSRestApi(BaseSupersetModelRestApi):
         """
         item_ids = kwargs["rison"]
         try:
-            BulkDeleteRLSRuleCommand(item_ids).run()
+            DeleteRLSRuleCommand(item_ids).run()
             return self.response(
                 200,
                 message=ngettext(
diff --git a/superset/row_level_security/commands/bulk_delete.py b/superset/row_level_security/commands/delete.py
similarity index 92%
rename from superset/row_level_security/commands/bulk_delete.py
rename to superset/row_level_security/commands/delete.py
index f0c8dddabc..d669f7d90f 100644
--- a/superset/row_level_security/commands/bulk_delete.py
+++ b/superset/row_level_security/commands/delete.py
@@ -23,13 +23,13 @@ from superset.daos.security import RLSDAO
 from superset.reports.models import ReportSchedule
 from superset.row_level_security.commands.exceptions import (
     RLSRuleNotFoundError,
-    RuleBulkDeleteFailedError,
+    RuleDeleteFailedError,
 )
 
 logger = logging.getLogger(__name__)
 
 
-class BulkDeleteRLSRuleCommand(BaseCommand):
+class DeleteRLSRuleCommand(BaseCommand):
     def __init__(self, model_ids: list[int]):
         self._model_ids = model_ids
         self._models: list[ReportSchedule] = []
@@ -40,7 +40,7 @@ class BulkDeleteRLSRuleCommand(BaseCommand):
             RLSDAO.delete(self._models)
         except DAODeleteFailedError as ex:
             logger.exception(ex.exception)
-            raise RuleBulkDeleteFailedError() from ex
+            raise RuleDeleteFailedError() from ex
 
     def validate(self) -> None:
         # Validate/populate model exists
diff --git a/superset/row_level_security/commands/exceptions.py b/superset/row_level_security/commands/exceptions.py
index 40f8e4af81..5eb8e0b103 100644
--- a/superset/row_level_security/commands/exceptions.py
+++ b/superset/row_level_security/commands/exceptions.py
@@ -25,5 +25,5 @@ class RLSRuleNotFoundError(CommandException):
     message = _("RLS Rule not found.")
 
 
-class RuleBulkDeleteFailedError(DeleteFailedError):
-    message = _("RLS Rule could not be deleted.")
+class RuleDeleteFailedError(DeleteFailedError):
+    message = _("RLS rules could not be deleted.")
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index 97c4800478..027002507a 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -1530,7 +1530,7 @@ class TestDatasetApi(SupersetTestCase):
         rv = self.delete_assert_metric(uri, "delete")
         data = json.loads(rv.data.decode("utf-8"))
         assert rv.status_code == 422
-        assert data == {"message": "Dataset could not be deleted."}
+        assert data == {"message": "Datasets could not be deleted."}
         db.session.delete(dataset)
         db.session.commit()
 
diff --git a/tests/integration_tests/security/row_level_security_tests.py b/tests/integration_tests/security/row_level_security_tests.py
index 2a28089c3e..c29ebe9afe 100644
--- a/tests/integration_tests/security/row_level_security_tests.py
+++ b/tests/integration_tests/security/row_level_security_tests.py
@@ -483,7 +483,7 @@ class TestRowLevelSecurityUpdateAPI(SupersetTestCase):
         db.session.commit()
 
 
-class TestRowLevelSecurityBulkDeleteAPI(SupersetTestCase):
+class TestRowLevelSecurityDeleteAPI(SupersetTestCase):
     def test_invalid_id_failure(self):
         self.login("Admin")