This is an automated email from the ASF dual-hosted git repository.
michaelsmolina 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 51ac758b801 fix(tags): expire tag relationship after deleting all
tagged objects (#38163)
51ac758b801 is described below
commit 51ac758b801549b16d57ff053cbce244f0571463
Author: Evan Rusackas <[email protected]>
AuthorDate: Wed Mar 4 08:37:19 2026 -0500
fix(tags): expire tag relationship after deleting all tagged objects
(#38163)
Co-authored-by: Claude <[email protected]>
---
superset/daos/tag.py | 7 ++++
tests/unit_tests/tags/commands/update_test.py | 60 +++++++++++++++++++++++++++
2 files changed, 67 insertions(+)
diff --git a/superset/daos/tag.py b/superset/daos/tag.py
index 2c5cc358265..f351dc18eae 100644
--- a/superset/daos/tag.py
+++ b/superset/daos/tag.py
@@ -378,4 +378,11 @@ class TagDAO(BaseDAO[Tag]):
object_id,
tag.name,
)
+ # After deleting tagged objects, we need to expire the tag's
'objects'
+ # relationship to clear references to deleted TaggedObject
instances.
+ # This prevents SQLAlchemy errors when the tag is later added to
the
+ # session, as it would otherwise still hold references to deleted
objects.
+ if tagged_objects_to_delete:
+ db.session.expire(tag, ["objects"])
+
db.session.add_all(tagged_objects)
diff --git a/tests/unit_tests/tags/commands/update_test.py
b/tests/unit_tests/tags/commands/update_test.py
index e22fcc2be39..edd41991fce 100644
--- a/tests/unit_tests/tags/commands/update_test.py
+++ b/tests/unit_tests/tags/commands/update_test.py
@@ -204,3 +204,63 @@ def test_update_command_failed_validation(
"objects_to_tag": objects_to_tag,
},
).run()
+
+
+def test_update_command_remove_all_tagged_objects(
+ session_with_data: Session, mocker: MockerFixture
+):
+ """Test that removing all tagged objects from a tag works correctly.
+
+ This is a regression test for GitHub issue #36074 where bulk untagging
+ (removing all objects from a tag) caused a SQLAlchemy error because
+ the tag's 'objects' relationship still held references to deleted
+ TaggedObject instances.
+ """
+ from superset.commands.tag.create import
CreateCustomTagWithRelationshipsCommand
+ from superset.commands.tag.update import UpdateTagCommand
+ from superset.daos.tag import TagDAO
+ from superset.models.dashboard import Dashboard
+ from superset.models.slice import Slice
+ from superset.tags.models import ObjectType, TaggedObject
+
+ dashboard = db.session.query(Dashboard).first()
+ chart = db.session.query(Slice).first()
+
+ mocker.patch(
+ "superset.security.SupersetSecurityManager.is_admin", return_value=True
+ )
+ mocker.patch("superset.daos.chart.ChartDAO.find_by_id", return_value=chart)
+ mocker.patch(
+ "superset.daos.dashboard.DashboardDAO.find_by_id",
return_value=dashboard
+ )
+
+ # Create a tag with multiple objects
+ objects_to_tag = [
+ (ObjectType.dashboard, dashboard.id),
+ (ObjectType.chart, chart.id),
+ ]
+
+ CreateCustomTagWithRelationshipsCommand(
+ data={"name": "test_tag", "objects_to_tag": objects_to_tag}
+ ).run()
+
+ tag_to_update = TagDAO.find_by_name("test_tag")
+ assert len(tag_to_update.objects) == 2
+
+ # Remove all tagged objects by passing an empty list
+ # This should not raise a SQLAlchemy error about deleted instances
+ updated_tag = UpdateTagCommand(
+ tag_to_update.id,
+ {
+ "name": "test_tag",
+ "description": "updated description",
+ "objects_to_tag": [],
+ },
+ ).run()
+
+ assert updated_tag is not None
+ assert updated_tag.description == "updated description"
+ # Verify all tagged objects were removed
+ assert (
+
len(db.session.query(TaggedObject).filter_by(tag_id=updated_tag.id).all()) == 0
+ )