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 1705814df1..93c7475d42 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 @@ -67,6 +67,7 @@ export interface ChartDataResponseResult { is_cached: boolean; query: string; rowcount: number; + sql_rowcount: number; stacktrace: string | null; status: | 'stopped' diff --git a/superset-frontend/plugins/plugin-chart-table/test/testData.ts b/superset-frontend/plugins/plugin-chart-table/test/testData.ts index 24abc3381e..6c76e865e8 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/testData.ts +++ b/superset-frontend/plugins/plugin-chart-table/test/testData.ts @@ -80,6 +80,7 @@ const basicQueryResult: ChartDataResponseResult = { is_cached: false, query: 'SELECT ...', rowcount: 100, + sql_rowcount: 100, stacktrace: null, status: 'success', from_dttm: null, diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx index b424b95ea5..bab24fdd47 100644 --- a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx @@ -44,6 +44,7 @@ export const useResultsTableView = ( {!isLoading && firstQueryResponse && ( )} diff --git a/superset-frontend/src/explore/components/DataTableControl/RowCount.test.tsx b/superset-frontend/src/explore/components/DataTableControl/RowCount.test.tsx deleted file mode 100644 index 6ebbcef77b..0000000000 --- a/superset-frontend/src/explore/components/DataTableControl/RowCount.test.tsx +++ /dev/null @@ -1,36 +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 React from 'react'; -import { render, screen } from 'spec/helpers/testing-library'; -import { RowCount } from '.'; - -test('Render a RowCount with a single row', () => { - render(); - expect(screen.getByText('1 row')).toBeInTheDocument(); -}); - -test('Render a RowCount with multiple rows', () => { - render(); - expect(screen.getByText('3 rows')).toBeInTheDocument(); -}); - -test('Render a RowCount on loading', () => { - render(); - expect(screen.getByText('Loading...')).toBeInTheDocument(); -}); diff --git a/superset-frontend/src/explore/components/DataTableControl/index.tsx b/superset-frontend/src/explore/components/DataTableControl/index.tsx index 8dc4b1d014..60ca470e4a 100644 --- a/superset-frontend/src/explore/components/DataTableControl/index.tsx +++ b/superset-frontend/src/explore/components/DataTableControl/index.tsx @@ -44,7 +44,6 @@ import Button from 'src/components/Button'; import Popover from 'src/components/Popover'; import { prepareCopyToClipboardTabularData } from 'src/utils/common'; import CopyToClipboard from 'src/components/CopyToClipboard'; -import RowCountLabel from 'src/explore/components/RowCountLabel'; import { getTimeColumns, setTimeColumns } from './utils'; export const CellNull = styled('span')` @@ -118,14 +117,6 @@ export const FilterInput = ({ ); }; -export const RowCount = ({ - data, - loading, -}: { - data?: Record[]; - loading: boolean; -}) => ; - enum FormatPickerValue { Formatted = 'formatted', Original = 'original', diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx index 7a02114a2f..b1ff73e29e 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx @@ -22,10 +22,10 @@ import { css, GenericDataType, styled } from '@superset-ui/core'; import { CopyToClipboardButton, FilterInput, - RowCount, } from 'src/explore/components/DataTableControl'; import { applyFormattingToTabularData } from 'src/utils/common'; import { getTimeColumns } from 'src/explore/components/DataTableControl/utils'; +import RowCountLabel from 'src/explore/components/RowCountLabel'; import { TableControlsProps } from '../types'; export const TableControlsWrapper = styled.div` @@ -47,6 +47,7 @@ export const TableControls = ({ onInputChange, columnNames, columnTypes, + rowcount, isLoading, }: TableControlsProps) => { const originalTimeColumns = getTimeColumns(datasourceId); @@ -74,7 +75,7 @@ export const TableControls = ({ align-items: center; `} > - + diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx index b542aad996..eaae35bd2f 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx @@ -48,6 +48,7 @@ export const SamplesPane = ({ const [colnames, setColnames] = useState([]); const [coltypes, setColtypes] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [rowcount, setRowCount] = useState(0); const [responseError, setResponseError] = useState(''); const datasourceId = useMemo( () => `${datasource.id}__${datasource.type}`, @@ -66,6 +67,7 @@ export const SamplesPane = ({ setData(ensureIsArray(response.data)); setColnames(ensureIsArray(response.colnames)); setColtypes(ensureIsArray(response.coltypes)); + setRowCount(response.rowcount); setResponseError(''); cache.add(datasource); if (queryForce && actions) { @@ -108,6 +110,7 @@ export const SamplesPane = ({ data={filteredData} columnNames={colnames} columnTypes={coltypes} + rowcount={rowcount} datasourceId={datasourceId} onInputChange={input => setFilterText(input)} isLoading={isLoading} @@ -128,6 +131,7 @@ export const SamplesPane = ({ data={filteredData} columnNames={colnames} columnTypes={coltypes} + rowcount={rowcount} datasourceId={datasourceId} onInputChange={input => setFilterText(input)} isLoading={isLoading} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx index c2614dfda6..b543167840 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx @@ -30,6 +30,7 @@ export const SingleQueryResultPane = ({ data, colnames, coltypes, + rowcount, datasourceId, dataSize = 50, isVisible, @@ -55,6 +56,7 @@ export const SingleQueryResultPane = ({ data={filteredData} columnNames={colnames} columnTypes={coltypes} + rowcount={rowcount} datasourceId={datasourceId} onInputChange={input => setFilterText(input)} isLoading={false} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx index 9b6b597c79..bae6b6fd7b 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx @@ -119,6 +119,7 @@ export const useResultsPane = ({ data={[]} columnNames={[]} columnTypes={[]} + rowcount={0} datasourceId={queryFormData.datasource} onInputChange={() => {}} isLoading={false} @@ -135,7 +136,6 @@ export const useResultsPane = ({ , ); } - return resultResp .slice(0, queryCount) .map((result, idx) => ( @@ -143,6 +143,7 @@ export const useResultsPane = ({ data={result.data} colnames={result.colnames} coltypes={result.coltypes} + rowcount={result.rowcount} dataSize={dataSize} datasourceId={queryFormData.datasource} key={idx} diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx index 74358af959..40370638e5 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx @@ -93,6 +93,8 @@ describe('DataTablesPane', () => { data: [{ __timestamp: 1230768000000, genre: 'Action' }], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 1, + sql_rowcount: 1, }, ], }, @@ -125,6 +127,8 @@ describe('DataTablesPane', () => { ], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 2, + sql_rowcount: 2, }, ], }, @@ -135,6 +139,7 @@ describe('DataTablesPane', () => { }); userEvent.click(screen.getByText('Results')); expect(await screen.findByText('2 rows')).toBeVisible(); + expect(screen.getByText('Action')).toBeVisible(); expect(screen.getByText('Horror')).toBeVisible(); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx index 839126bb62..a6f9830d2e 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -50,6 +50,8 @@ describe('ResultsPaneOnDashboard', () => { ], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 2, + sql_rowcount: 2, }, ], }, @@ -78,6 +80,8 @@ describe('ResultsPaneOnDashboard', () => { data: [{ genre: 'Action' }, { genre: 'Horror' }], colnames: ['genre'], coltypes: [1], + rowcount: 2, + sql_rowcount: 2, }, ], }, diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx index eed6e84808..02c421aeec 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx @@ -50,6 +50,8 @@ describe('SamplesPane', () => { ], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 2, + sql_rowcount: 2, }, }, ); diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts index 4e6062ba4a..4939b7d748 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/types.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts @@ -71,11 +71,13 @@ export interface TableControlsProps { columnNames: string[]; columnTypes: GenericDataType[]; isLoading: boolean; + rowcount: number; } export interface QueryResultInterface { colnames: string[]; coltypes: GenericDataType[]; + rowcount: number; data: Record[][]; } diff --git a/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx b/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx index 2e7a44c15e..8e3e3b0c1d 100644 --- a/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx +++ b/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx @@ -52,7 +52,7 @@ test('RowCountLabel renders limit with danger and tooltip', async () => { expect(screen.getByText(expectedText)).toBeInTheDocument(); userEvent.hover(screen.getByText(expectedText)); const tooltip = await screen.findByRole('tooltip'); - expect(tooltip).toHaveTextContent('Limit reached'); + expect(tooltip).toHaveTextContent('The row limit'); expect(tooltip).toHaveStyle('background: rgba(0, 0, 0, 0.902);'); }); diff --git a/superset-frontend/src/explore/components/RowCountLabel/index.tsx b/superset-frontend/src/explore/components/RowCountLabel/index.tsx index be41be0ba6..80fc362959 100644 --- a/superset-frontend/src/explore/components/RowCountLabel/index.tsx +++ b/superset-frontend/src/explore/components/RowCountLabel/index.tsx @@ -28,9 +28,13 @@ type RowCountLabelProps = { loading?: boolean; }; +const limitReachedMsg = t( + 'The row limit set for the chart was reached. The chart may show partial data.', +); + export default function RowCountLabel(props: RowCountLabelProps) { - const { rowcount = 0, limit, loading } = props; - const limitReached = rowcount === limit; + const { rowcount = 0, limit = null, loading } = props; + const limitReached = limit && rowcount >= limit; const type = limitReached || (rowcount === 0 && !loading) ? 'danger' : 'default'; const formattedRowCount = getNumberFormatter()(rowcount); @@ -46,7 +50,7 @@ export default function RowCountLabel(props: RowCountLabelProps) { ); return limitReached ? ( - {t('Limit reached')}}> + {limitReachedMsg}}> {label} ) : ( diff --git a/superset/common/query_actions.py b/superset/common/query_actions.py index d73a99d027..bdbccc78db 100644 --- a/superset/common/query_actions.py +++ b/superset/common/query_actions.py @@ -135,6 +135,8 @@ def _get_full( "data": payload.get("data"), "colnames": payload.get("colnames"), "coltypes": payload.get("coltypes"), + "rowcount": payload.get("rowcount"), + "sql_rowcount": payload.get("sql_rowcount"), } return payload diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index d8b5bea4bb..f003f8ed30 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -194,6 +194,7 @@ class QueryContextProcessor: "status": cache.status, "stacktrace": cache.stacktrace, "rowcount": len(cache.df.index), + "sql_rowcount": cache.sql_rowcount, "from_dttm": query_obj.from_dttm, "to_dttm": query_obj.to_dttm, "label_map": label_map, diff --git a/superset/common/utils/query_cache_manager.py b/superset/common/utils/query_cache_manager.py index a0fb65b20d..d2e6e07437 100644 --- a/superset/common/utils/query_cache_manager.py +++ b/superset/common/utils/query_cache_manager.py @@ -64,6 +64,7 @@ class QueryCacheManager: is_cached: bool | None = None, cache_dttm: str | None = None, cache_value: dict[str, Any] | None = None, + sql_rowcount: int | None = None, ) -> None: self.df = df self.query = query @@ -79,6 +80,7 @@ class QueryCacheManager: self.is_cached = is_cached self.cache_dttm = cache_dttm self.cache_value = cache_value + self.sql_rowcount = sql_rowcount # pylint: disable=too-many-arguments def set_query_result( @@ -102,6 +104,7 @@ class QueryCacheManager: self.rejected_filter_columns = query_result.rejected_filter_columns self.error_message = query_result.error_message self.df = query_result.df + self.sql_rowcount = query_result.sql_rowcount self.annotation_data = {} if annotation_data is None else annotation_data if self.status != QueryStatus.FAILED: @@ -117,6 +120,7 @@ class QueryCacheManager: "applied_filter_columns": self.applied_filter_columns, "rejected_filter_columns": self.rejected_filter_columns, "annotation_data": self.annotation_data, + "sql_rowcount": self.sql_rowcount, } if self.is_loaded and key and self.status != QueryStatus.FAILED: self.set( @@ -167,6 +171,7 @@ class QueryCacheManager: query_cache.status = QueryStatus.SUCCESS query_cache.is_loaded = True query_cache.is_cached = cache_value is not None + query_cache.sql_rowcount = cache_value.get("sql_rowcount", None) query_cache.cache_dttm = ( cache_value["dttm"] if cache_value is not None else None ) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 450def33b2..4b22873903 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -584,6 +584,7 @@ class QueryResult: # pylint: disable=too-few-public-methods self.errors = errors or [] self.from_dttm = from_dttm self.to_dttm = to_dttm + self.sql_rowcount = len(self.df.index) if not self.df.empty else 0 class ExtraJSONMixin: diff --git a/superset/views/core.py b/superset/views/core.py index 4faede0f34..287f71e548 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -167,6 +167,8 @@ class Superset(BaseSupersetView): "data": payload["df"].to_dict("records"), "colnames": payload.get("colnames"), "coltypes": payload.get("coltypes"), + "rowcount": payload.get("rowcount"), + "sql_rowcount": payload.get("sql_rowcount"), }, ) diff --git a/superset/viz.py b/superset/viz.py index 7c78f84228..447b36d53d 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -262,6 +262,8 @@ class BaseViz: # pylint: disable=too-many-public-methods "data": payload["df"].to_dict(orient="records"), "colnames": payload.get("colnames"), "coltypes": payload.get("coltypes"), + "rowcount": payload.get("rowcount"), + "sql_rowcount": payload.get("sql_rowcount"), } @deprecated(deprecated_in="3.0")