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
# ---------------------------------------------------------------------------