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

betodealmeida pushed a commit to branch sl-cache
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/sl-cache by this push:
     new aaa87d79c20 Add indicator in Explore
aaa87d79c20 is described below

commit aaa87d79c2038e003c6a3b3b4c25b88f916d58ce
Author: Beto Dealmeida <[email protected]>
AuthorDate: Thu May 14 17:48:21 2026 -0400

    Add indicator in Explore
---
 .../components/CachedLabel/TooltipContent.test.tsx    |  9 +++++++++
 .../src/components/CachedLabel/TooltipContent.tsx     | 19 ++++++++++++++++---
 .../src/components/CachedLabel/index.tsx              |  8 +++++++-
 .../src/components/CachedLabel/types.ts               |  1 +
 .../superset-ui-core/src/components/Select/Select.tsx |  6 ++++--
 .../superset-ui-core/src/query/types/QueryResponse.ts |  1 +
 .../tests/dashboard/clear-all-filters.spec.ts         |  5 +----
 .../src/explore/components/ChartPills.tsx             | 12 ++++++++++--
 .../ExploreChartPanel/ExploreChartPanel.test.tsx      | 13 +++++++++++++
 superset/common/query_context_factory.py              |  1 +
 superset/common/query_context_processor.py            |  2 +-
 superset/common/query_object.py                       |  4 ++++
 superset/semantic_layers/cache.py                     |  9 +++++----
 superset/semantic_layers/mapper.py                    |  2 +-
 superset/superset_typing.py                           |  2 ++
 .../semantic_layers/cache_integration_test.py         | 16 ++++++++++++++++
 16 files changed, 92 insertions(+), 18 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
index 72629c868b2..2bc44409e44 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.test.tsx
@@ -36,3 +36,12 @@ test('Rendering TooltipContent correctly - with timestep', 
() => {
       .fromNow()}. Click to force-refresh`,
   );
 });
+
+test('Rendering TooltipContent correctly - semantic cache', () => {
+  render(<TooltipContent cacheSource="semantic" cachedTimestamp="01-01-2000" 
/>);
+  expect(screen.getByTestId('tooltip-content')?.textContent).toBe(
+    `Loaded from semantic smart cache ${extendedDayjs
+      .utc('01-01-2000')
+      .fromNow()}. Click to force-refresh`,
+  );
+});
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.tsx
index da85ff5cd06..67cead5b8d0 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/TooltipContent.tsx
@@ -23,15 +23,28 @@ import { extendedDayjs } from '../../utils/dates';
 
 interface Props {
   cachedTimestamp?: string;
+  cacheSource?: 'query' | 'semantic';
 }
-export const TooltipContent: FC<Props> = ({ cachedTimestamp }) => {
+export const TooltipContent: FC<Props> = ({
+  cachedTimestamp,
+  cacheSource = 'query',
+}) => {
+  const loadedFromText =
+    cacheSource === 'semantic'
+      ? t('Loaded from semantic smart cache')
+      : t('Loaded data cached');
+  const loadedFallbackText =
+    cacheSource === 'semantic'
+      ? t('Loaded from semantic smart cache')
+      : t('Loaded from cache');
+
   const cachedText = cachedTimestamp ? (
     <span>
-      {t('Loaded data cached')}
+      {loadedFromText}
       <b> {extendedDayjs.utc(cachedTimestamp).fromNow()}</b>
     </span>
   ) : (
-    t('Loaded from cache')
+    loadedFallbackText
   );
 
   return (
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/index.tsx
index 027526fe33f..018768d6684 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/index.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/index.tsx
@@ -29,13 +29,19 @@ export const CachedLabel: FC<CacheLabelProps> = ({
   className,
   onClick,
   cachedTimestamp,
+  cacheSource = 'query',
 }) => {
   const [hovered, setHovered] = useState(false);
 
   const labelType = hovered ? 'info' : 'default';
   return (
     <Tooltip
-      title={<TooltipContent cachedTimestamp={cachedTimestamp} />}
+      title={
+        <TooltipContent
+          cachedTimestamp={cachedTimestamp}
+          cacheSource={cacheSource}
+        />
+      }
       id="cache-desc-tooltip"
     >
       <Label
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/types.ts
 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/types.ts
index 5a8b3567405..6d54fdfd7e0 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/types.ts
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/CachedLabel/types.ts
@@ -22,5 +22,6 @@ import type { MouseEventHandler } from 'react';
 export interface CacheLabelProps {
   onClick?: MouseEventHandler<HTMLElement>;
   cachedTimestamp?: string;
+  cacheSource?: 'query' | 'semantic';
   className?: string;
 }
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx 
b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
index bab0e9a7394..971bcd45cd8 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
@@ -519,7 +519,8 @@ const Select = forwardRef(
               handleSelectAll();
             }}
           >
-            {t('Select all')} {`(${formatNumber('SMART_NUMBER', 
bulkSelectCounts.selectable)})`}
+            {t('Select all')}{' '}
+            {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
           </Button>
           <Button
             type="link"
@@ -536,7 +537,8 @@ const Select = forwardRef(
               handleDeselectAll();
             }}
           >
-            {t('Clear')} {`(${formatNumber('SMART_NUMBER', 
bulkSelectCounts.deselectable)})`}
+            {t('Clear')}{' '}
+            {`(${formatNumber('SMART_NUMBER', 
bulkSelectCounts.deselectable)})`}
           </Button>
         </StyledBulkActionsContainer>
       ),
diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts
index 210685f3480..6b5d5fb653b 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts
+++ 
b/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts
@@ -60,6 +60,7 @@ export interface ChartDataResponseResult {
   coltypes: GenericDataType[];
   error: string | null;
   is_cached: boolean;
+  semantic_cache_hit?: boolean | null;
   query: string;
   rowcount: number;
   sql_rowcount: number;
diff --git 
a/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts 
b/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts
index 4cd0c56f4b3..6bb30c3094b 100644
--- a/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts
+++ b/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts
@@ -182,10 +182,7 @@ testWithAssets(
     // Now track POST /api/v1/chart/data requests around Clear All
     const postsAfterClearAll: string[] = [];
     const handler = (req: any) => {
-      if (
-        req.url().includes('/api/v1/chart/data') &&
-        req.method() === 'POST'
-      ) {
+      if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') 
{
         postsAfterClearAll.push(req.url());
       }
     };
diff --git a/superset-frontend/src/explore/components/ChartPills.tsx 
b/superset-frontend/src/explore/components/ChartPills.tsx
index da7de6fc8b4..cc8eee4abf8 100644
--- a/superset-frontend/src/explore/components/ChartPills.tsx
+++ b/superset-frontend/src/explore/components/ChartPills.tsx
@@ -66,6 +66,9 @@ export const ChartPills = forwardRef(
   ) => {
     const isLoading = chartStatus === 'loading';
     const firstQueryResponse = queriesResponse?.[0];
+    const isQueryCached = Boolean(firstQueryResponse?.is_cached);
+    const isSemanticCached = Boolean(firstQueryResponse?.semantic_cache_hit);
+    const isAnyCacheHit = isQueryCached || isSemanticCached;
 
     // For table charts with server pagination, check second query for total 
count
     const isTableChart =
@@ -100,10 +103,15 @@ export const ChartPills = forwardRef(
               limit={Number(rowLimit ?? 0)}
             />
           )}
-          {!isLoading && firstQueryResponse?.is_cached && (
+          {!isLoading && isAnyCacheHit && (
             <CachedLabel
               onClick={refreshCachedQuery}
-              cachedTimestamp={firstQueryResponse.cached_dttm}
+              cachedTimestamp={
+                isQueryCached
+                  ? firstQueryResponse?.cached_dttm
+                  : firstQueryResponse?.queried_dttm
+              }
+              cacheSource={isSemanticCached ? 'semantic' : 'query'}
             />
           )}
           <Timer
diff --git 
a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
 
b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
index e0375f9d67f..276d2f759c8 100644
--- 
a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
+++ 
b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
@@ -168,6 +168,19 @@ describe('ChartContainer', () => {
     expect(screen.queryByText(/cached/i)).not.toBeInTheDocument();
   });
 
+  test('should show cached button for semantic smart cache hit', async () => {
+    const props = createProps({
+      chart: {
+        chartStatus: 'rendered',
+        queriesResponse: [
+          { is_cached: false, semantic_cache_hit: true, queried_dttm: 
'2026-01-01' },
+        ],
+      },
+    });
+    render(<ChartContainer {...props} />, { useRedux: true });
+    expect(await screen.findByText(/cached/i)).toBeInTheDocument();
+  });
+
   test('hides gutter when collapsing data panel', async () => {
     const props = createProps();
     setItem(LocalStorageKeys.IsDatapanelOpen, true);
diff --git a/superset/common/query_context_factory.py 
b/superset/common/query_context_factory.py
index 0d1539a9a16..889aa8ec963 100644
--- a/superset/common/query_context_factory.py
+++ b/superset/common/query_context_factory.py
@@ -82,6 +82,7 @@ class QueryContextFactory:  # pylint: 
disable=too-few-public-methods
                     result_type,
                     datasource=datasource,
                     server_pagination=server_pagination,
+                    force_query=force,
                     **query_obj,
                 ),
             )
diff --git a/superset/common/query_context_processor.py 
b/superset/common/query_context_processor.py
index bad78532051..baf32e9d03b 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -202,7 +202,7 @@ class QueryContextProcessor:
             "annotation_data": cache.annotation_data,
             "error": cache.error_message,
             "is_cached": cache.is_cached,
-            "semantic_cache_hit": cache.semantic_cache_hit,
+            "semantic_cache_hit": getattr(cache, "semantic_cache_hit", None),
             "query": cache.query,
             "status": cache.status,
             "stacktrace": cache.stacktrace,
diff --git a/superset/common/query_object.py b/superset/common/query_object.py
index e56d795ebb6..229f88b1db5 100644
--- a/superset/common/query_object.py
+++ b/superset/common/query_object.py
@@ -101,6 +101,7 @@ class QueryObject:  # pylint: 
disable=too-many-instance-attributes
     result_type: ChartDataResultType | None
     row_limit: int | None
     row_offset: int
+    force_query: bool
     series_columns: list[Column]
     series_limit: int
     series_limit_metric: Metric | None
@@ -128,6 +129,7 @@ class QueryObject:  # pylint: 
disable=too-many-instance-attributes
         post_processing: list[dict[str, Any] | None] | None = None,
         row_limit: int | None = None,
         row_offset: int | None = None,
+        force_query: bool = False,
         series_columns: list[Column] | None = None,
         series_limit: int = 0,
         series_limit_metric: Metric | None = None,
@@ -152,6 +154,7 @@ class QueryObject:  # pylint: 
disable=too-many-instance-attributes
         self._set_post_processing(post_processing)
         self.row_limit = row_limit
         self.row_offset = row_offset or 0
+        self.force_query = force_query
         self._init_series_columns(series_columns, metrics, is_timeseries)
         self.series_limit = series_limit
         self.series_limit_metric = series_limit_metric
@@ -404,6 +407,7 @@ class QueryObject:  # pylint: 
disable=too-many-instance-attributes
             "post_processing": self.post_processing,
             "row_limit": self.row_limit,
             "row_offset": self.row_offset,
+            "force_query": self.force_query,
             "series_columns": self.series_columns,
             "series_limit": self.series_limit,
             "series_limit_metric": self.series_limit_metric,
diff --git a/superset/semantic_layers/cache.py 
b/superset/semantic_layers/cache.py
index e45c4717482..f267bebfdaf 100644
--- a/superset/semantic_layers/cache.py
+++ b/superset/semantic_layers/cache.py
@@ -637,13 +637,14 @@ def _apply_post_processing(
     note_def = "Served from semantic view smart cache (post-processed locally)"
     if projection_needed:
         groupby = [d.name for d in query.dimensions]
-        aggregates = {
-            m.name: {
+        aggregates: dict[str, dict[str, str]] = {}
+        for m in query.metrics:
+            if m.aggregation is None:
+                continue
+            aggregates[m.name] = {
                 "column": m.name,
                 "operator": _AGGREGATION_TO_PANDAS[m.aggregation],
             }
-            for m in query.metrics
-        }
         df = aggregate(df, groupby=groupby, aggregates=aggregates)
         note_def = "Served from semantic view smart cache (re-aggregated 
locally)"
 
diff --git a/superset/semantic_layers/mapper.py 
b/superset/semantic_layers/mapper.py
index a4afff69d51..5f52bfe31b9 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -222,7 +222,7 @@ def _make_cached_dispatch(
     Row-count queries bypass the cache. Cache failures are logged and the
     dispatcher is called as if the cache were absent.
     """
-    if query_object.is_rowcount:
+    if query_object.is_rowcount or query_object.force_query:
         return dispatcher
 
     view = query_object.datasource
diff --git a/superset/superset_typing.py b/superset/superset_typing.py
index 39ff9ab1347..3141c9ae7b8 100644
--- a/superset/superset_typing.py
+++ b/superset/superset_typing.py
@@ -179,6 +179,7 @@ class QueryObjectDict(TypedDict, total=False):
         orderby: List of order by clauses
         row_limit: Maximum number of rows
         row_offset: Number of rows to skip
+        force_query: Whether to bypass cache when executing
         series_columns: Columns to use for series
         series_limit: Maximum number of series
         series_limit_metric: Metric to use for series limiting
@@ -215,6 +216,7 @@ class QueryObjectDict(TypedDict, total=False):
     orderby: list[OrderBy]
     row_limit: int | None
     row_offset: int
+    force_query: bool
     series_columns: list[Column]
     series_limit: int
     series_limit_metric: Metric | None
diff --git a/tests/unit_tests/semantic_layers/cache_integration_test.py 
b/tests/unit_tests/semantic_layers/cache_integration_test.py
index 432bbdfba6c..7d056e26386 100644
--- a/tests/unit_tests/semantic_layers/cache_integration_test.py
+++ b/tests/unit_tests/semantic_layers/cache_integration_test.py
@@ -106,6 +106,7 @@ def _qo(
     filter_op: str | None = None,
     filter_val: Any = None,
     limit: int | None = None,
+    force_query: bool = False,
 ) -> ValidatedQueryObject:
     qo_filters: list[dict[str, Any]] = (
         [{"col": "a", "op": filter_op, "val": filter_val}] if filter_op else []
@@ -116,6 +117,7 @@ def _qo(
         columns=["a"],
         filters=qo_filters,  # type: ignore[arg-type]
         row_limit=limit,
+        force_query=force_query,
     )
 
 
@@ -192,6 +194,20 @@ def test_changed_on_invalidates_cache(
     assert view_implementation.get_table.call_count == 2
 
 
+def test_force_query_bypasses_semantic_cache(
+    fake_cache: _InMemoryCache,
+    view_implementation: Any,
+    datasource: MagicMock,
+) -> None:
+    view_implementation.get_table = MagicMock(return_value=_result([(2, 1.0)]))
+
+    get_results(_qo(datasource, ">", 1))
+    assert view_implementation.get_table.call_count == 1
+
+    get_results(_qo(datasource, ">", 1, force_query=True))
+    assert view_implementation.get_table.call_count == 2
+
+
 # ---------------------------------------------------------------------------
 # Projection (v2) — dropping a dimension and re-aggregating
 # ---------------------------------------------------------------------------

Reply via email to