Files
superset2/tests/integration_tests/tasks/async_queries_tests.py
T
Claude Code ae1838814f chore(tags): stop auto-generating type:/editor:/favorited_by: tags
villebro noted on #43390 that these system-generated tags appear to be
unused. Confirmed: every tags list and filter in the frontend explicitly
excludes non-custom tags (ChartList, DashboardList, SavedQueryList, the
chart PropertiesModal, the dashboard Header), so nothing ever surfaced
them to a user. What remained was pure write-side overhead: 13
SQLAlchemy event listeners across 5 models firing on every chart/
dashboard/query/dataset save and every favorite/unfavorite, plus a whole
performance-optimization mixin (CustomTagsOptimizationMixin,
DASHBOARD_LIST_CUSTOM_TAGS_ONLY) that existed purely to strip the
resulting noise back out of dashboard-list responses.

This removes the generation:
- superset/tags/models.py: drop ObjectUpdater's editor:/type: generation
  (after_insert/after_update) and FavStarUpdater's favorited_by:
  generation entirely. Keeps after_delete (tagged_object cleanup applies
  to every tag, custom included, and nothing else removes those rows
  since tagged_object.object_id has no FK - see its column comment).
- superset/tags/core.py: only registers the delete-cleanup listeners now.
- superset/common/tags.py + the `sync_tags` CLI command: removed (the
  backfill path for the generation this removes).
- superset/views/custom_tags_api_mixin.py, DASHBOARD_LIST_CUSTOM_TAGS_ONLY,
  Dashboard.custom_tags, and the schema/API plumbing built around them:
  removed - nothing left to optimize away once implicit tags stop
  accumulating.

Kept for backward compatibility, since MCP's list_tags/get_tag_info tools
document these tag types and upgraded deployments may already have rows
of these types: the TagType enum values, the custom_tag API filter,
and bulk-delete protection for non-custom tags. Docstrings updated to
say these are legacy/no longer generated rather than actively implicit.

Also fixes a real, currently-broken import in superset/daos/tag.py
(current_user_can_modify_object doesn't live in
superset.commands.tag.utils, only in superset.commands.utils) that
otherwise blocks every test in this area from running at all. Filed and
fixed separately as #43467; this commit will collapse away on rebase
once that merges.

Follow-up to #43390.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 13:44:55 -07:00

153 lines
5.8 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.
"""Unit tests for async query celery jobs in Superset"""
from unittest import mock
from uuid import uuid4
import pytest
from celery.exceptions import SoftTimeLimitExceeded
from parameterized import parameterized
from superset.async_events.cache_backend import (
RedisCacheBackend,
RedisSentinelCacheBackend,
)
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.commands.chart.exceptions import ChartDataQueryFailedError
from superset.extensions import async_query_manager, security_manager
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices, # noqa: F401
load_birth_names_data, # noqa: F401
)
from tests.integration_tests.fixtures.query_context import get_query_context
from tests.integration_tests.test_app import app
@pytest.mark.usefixtures(
"load_birth_names_data", "load_birth_names_dashboard_with_slices"
)
class TestAsyncQueries(SupersetTestCase):
@parameterized.expand(
[
("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
]
)
@mock.patch("superset.tasks.async_queries.set_form_data")
@mock.patch.object(async_query_manager, "update_job")
def test_load_chart_data_into_cache(
self, cache_type, cache_backend, mock_update_job, mock_set_form_data
):
from superset.tasks.async_queries import load_chart_data_into_cache
app._got_first_request = False
async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
async_query_manager.init_app(app)
query_context = get_query_context("birth_names")
user = security_manager.find_user("gamma")
job_metadata = {
"channel_id": str(uuid4()),
"job_id": str(uuid4()),
"user_id": user.id,
"status": "pending",
"errors": [],
}
load_chart_data_into_cache(job_metadata, query_context)
mock_set_form_data.assert_called_once_with(query_context)
mock_update_job.assert_called_once_with(
job_metadata, "done", result_url=mock.ANY
)
@parameterized.expand(
[
("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
]
)
@mock.patch.object(
ChartDataCommand, "run", side_effect=ChartDataQueryFailedError("Error: foo")
)
@mock.patch.object(async_query_manager, "update_job")
def test_load_chart_data_into_cache_error(
self, cache_type, cache_backend, mock_update_job, mock_run_command
):
from superset.tasks.async_queries import load_chart_data_into_cache
app._got_first_request = False
async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
async_query_manager.init_app(app)
query_context = get_query_context("birth_names")
user = security_manager.find_user("gamma")
job_metadata = {
"channel_id": str(uuid4()),
"job_id": str(uuid4()),
"user_id": user.id,
"status": "pending",
"errors": [],
}
with pytest.raises(ChartDataQueryFailedError):
load_chart_data_into_cache(job_metadata, query_context)
mock_run_command.assert_called_once_with(cache=True)
errors = [{"message": "Error: foo"}]
mock_update_job.assert_called_once_with(job_metadata, "error", errors=errors)
@parameterized.expand(
[
("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
]
)
@mock.patch.object(ChartDataCommand, "run")
@mock.patch.object(async_query_manager, "update_job")
def test_soft_timeout_load_chart_data_into_cache(
self, cache_type, cache_backend, mock_update_job, mock_run_command
):
from superset.tasks.async_queries import load_chart_data_into_cache
app._got_first_request = False
async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
async_query_manager.init_app(app)
user = security_manager.find_user("gamma")
form_data = {}
job_metadata = {
"channel_id": str(uuid4()),
"job_id": str(uuid4()),
"user_id": user.id,
"status": "pending",
"errors": [],
}
errors = ["A timeout occurred while loading chart data"]
with pytest.raises(SoftTimeLimitExceeded): # noqa: PT012
with mock.patch(
"superset.tasks.async_queries.set_form_data"
) as set_form_data:
set_form_data.side_effect = SoftTimeLimitExceeded()
load_chart_data_into_cache(job_metadata, form_data)
set_form_data.assert_called_once_with(form_data, "error", errors=errors)