Repository: apache/superset
Pull request #33357: feat(Table Chart): Row limit Increase , Backend Sorting , Backend Search , Excel/CSV Improvements
Author: login: amaannawab923
Labels: name: change:backend
description: Requires changing the backend
name: size/XXL
name: viz:charts:table
description: Related to the Table chart
name: cares:preset
description: Preset cares about this issue
name: plugins
name: packages
Created: 2025-05-05T05:32:12Z | Merged: 2025-05-09T17:27:31Z

<!---
Please write the PR title following the conventions at https://www.conventionalcommits.org/en/v1.0.0/
Example:
fix(dashboard): load charts correctly
-->

### SUMMARY
<!--- Describe the change below, including rationale and design decisions -->

###  🚀 Row Limit Increase
This PR introduces a more flexible and user-aware row limit configuration for the Table visualization in Apache Superset. Previously, the row limit was globally enforced at 100,000 rows via the SQL_MAX_ROW setting. This behavior has now been refined to allow significantly larger datasets when server-side pagination is enabled.

###  🔧  New Config Variables:
Two new configuration options have been introduced in config.py to distinguish row limits based on whether server-side pagination is enabled:

### TABLE_VIZ_MAX_ROW_CLIENT = 100000
→ Maximum number of rows allowed without server-side pagination (frontend capped).

### TABLE_VIZ_MAX_ROW_SERVER = 500000
→ Maximum number of rows allowed with server-side pagination enabled.

### 🎯  Behavior Changes:
If server pagination is disabled, users will continue to be capped at 100,000 rows.

<img width="1728" alt="Screenshot 2025-05-06 at 12 10 46 AM" src="https://github.com/user-attachments/assets/82af150c-e41b-417d-ba5d-f196e917fe28" />

<img width="1728" alt="Screenshot 2025-05-06 at 12 10 58 AM" src="https://github.com/user-attachments/assets/ea22fa2f-873b-4171-b021-2af0adfe896b" />


If server pagination is enabled, the row limit increases to 500,000 rows, allowing users to query and paginate through much larger datasets.

<img width="1728" alt="Screenshot 2025-05-06 at 12 11 20 AM" src="https://github.com/user-attachments/assets/71ee15a6-e6b4-4a1b-86e4-f39f39b63daa" />


### ⚠️  Frontend UX Improvements:
A new warning tooltip is shown on the row limit control when a user attempts to fetch more than 100k rows without enabling server pagination. It advises:

“Server pagination must be enabled for row limits over 100,000.”

If a user attempts to exceed 500,000 rows, the tooltip now shows:

“Value cannot exceed 500,000 rows.”

Below are the test cases & expectations of this functionality : 

https://www.notion.so/Table-Performance-Improvements-Test-Cases-1ea89b69c3c48038aec6ddb7be2d7b98

### ↕️ Backend Sorting for Table Charts
This PR introduces backend-driven sorting for Table charts, improving performance and scalability when handling large datasets.

### 📊 Affected Scope:
Applies only to Table visualizations

Sorting is pushed to the database layer when server-side pagination is enabled

<img width="1728" alt="Screenshot 2025-05-06 at 12 14 30 AM" src="https://github.com/user-attachments/assets/5bda030d-2368-4ca4-950c-6ced1c682473" />


### ⚙️ Conditional Behavior:
✅ With server pagination enabled:
Sorting is now performed at the backend by injecting an ORDER BY clause in the SQL query. This ensures accurate, efficient sorting even with large datasets (up to 500k rows), and avoids excessive browser-side memory usage.

❌ With server pagination disabled:
Sorting behavior remains frontend-based as before. This is suitable for smaller datasets (under 100k rows), where client-side sorting is performant and simple.


### 📁 Excel/CSV Export Improvements
This PR significantly enhances the export functionality for Excel and CSV downloads from Table charts, making it more intuitive and aligned with user expectations.

### 🛠️ Key Changes:
Simplified Export Format:
Previously, exports were bundled as a .zip file containing two separate Excel files — one for metadata and another for actual data. This extra step caused confusion and was unnecessary. Now, the export returns a single .csv file (or .xlsx if needed) with the complete dataset.

Full Dataset Export (Not Just Current Page):

Before: Only the records shown on the current page (e.g., 10 rows) were exported.

Now: The export includes all records up to the row limit — ensuring that users get the entire dataset, whether that's 100k rows (client mode) or up to 500k (with server pagination enabled).

### ✅ Affects:
Both Excel and CSV export options under the Table visualization.


### 🔍 Backend Search Support for Table Charts
This PR introduces backend search for Table visualizations, enabling efficient, scalable text filtering directly within the database query layer — a major step forward for handling large datasets and improving search accuracy.

### 🧠 Search Logic & SQL Integration:
A new operator, TEXT_SEARCH, has been added to the SQLAlchemy filter operators.

This operator constructs a WHERE clause using a smart pattern with ILIKE for case-insensitive partial matching:

column ILIKE '{pattern}%' OR column ILIKE '% {pattern}%'

This allows matching across both first and last words, e.g., searching “Anderson” will find both John Anderson and Mary Anderson.

### 🧪 Technical Behavior:
Applies only when server-side pagination is enabled

Targets only string-based columns (non-string columns are cast to string when necessary)

Appends the search logic directly into the SQL query using SQLAlchemy

### 🖥️ UI Enhancements:
A new search column dropdown has been added next to the search input

This dropdown lists all string columns of the dataset

When the user types into the search box:

The selected column (from the dropdown) and the input value are combined to generate the appropriate TEXT_SEARCH SQL clause

The dropdown allows users to dynamically change which column the backend search applies to

<img width="1728" alt="Screenshot 2025-05-06 at 12 16 44 AM" src="https://github.com/user-attachments/assets/cf8c478f-43ab-4d5a-b3e8-71a83e52706b" />
<img width="1718" alt="Screenshot 2025-05-06 at 12 17 30 AM" src="https://github.com/user-attachments/assets/974c3dde-96b5-4e57-90a9-64a14431f38d" />


### 🛠️  Fixes the issue

https://github.com/apache/superset/issues/31626

This issue bascially described that the page doesn't respect the entries per page even when the sql query returns correct number of rows , this was because the transformProps didnt get the correct Page Size , it always used server page length even when it was outdated

Before fix :

[screen-capture (13).webm](https://github.com/user-attachments/assets/e558dc6a-0bf3-4778-932d-183d3c84988a)


After fix: 

[screen-capture (14).webm](https://github.com/user-attachments/assets/a92cf686-5bcc-4b49-80cd-2df2ffce9aa3)




### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
<!--- Skip this if not applicable -->

### TESTING INSTRUCTIONS
<!--- Required! What steps can be taken to manually verify the changes? -->

### ADDITIONAL INFORMATION
<!--- Check any relevant boxes with "x" -->
<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->
- [ ] Has associated issue:
- [ ] Required feature flags:
- [x] Changes UI
- [ ] Includes DB Migration (follow approval process in [SIP-59](https://github.com/apache/superset/issues/13351))
  - [ ] Migration is atomic, supports rollback & is backwards-compatible
  - [ ] Confirm DB migration upgrade and downgrade tested
  - [ ] Runtime estimates and downtime expectations provided
- [x] Introduces new feature or API
- [ ] Removes existing feature or API



Diff:
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/table.test.ts b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/table.test.ts
index 603cc7f2c8d7..319938b95e71 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/table.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/table.test.ts
@@ -252,4 +252,215 @@ describe('Visualization > Table', () => {
     });
     cy.get('td').contains(/\d*%/);
   });
+
+  it('Test row limit with server pagination toggle', () => {
+    cy.visitChartByParams({
+      ...VIZ_DEFAULTS,
+      metrics: ['count'],
+      row_limit: 100,
+    });
+
+    // Enable server pagination
+    cy.get('[data-test="server_pagination-header"] div.pull-left').click();
+
+    // Click row limit control and select high value (200k)
+    cy.get('div[aria-label="Row limit"]').click();
+
+    // Type 200000 and press enter to select the option
+    cy.get('div[aria-label="Row limit"]')
+      .find('.ant-select-selection-search-input:visible')
+      .type('200000{enter}');
+
+    // Verify that there is no error tooltip when server pagination is enabled
+    cy.get('[data-test="error-tooltip"]').should('not.exist');
+
+    // Disable server pagination
+    cy.get('[data-test="server_pagination-header"] div.pull-left').click();
+
+    // Verify error tooltip appears
+    cy.get('[data-test="error-tooltip"]').should('be.visible');
+
+    // Trigger mouseover and verify tooltip text
+    cy.get('[data-test="error-tooltip"]').trigger('mouseover');
+
+    // Verify tooltip content
+    cy.get('.antd5-tooltip-inner').should('be.visible');
+    cy.get('.antd5-tooltip-inner').should(
+      'contain',
+      'Server pagination needs to be enabled for values over',
+    );
+
+    // Hide the tooltip by adding display:none style
+    cy.get('.antd5-tooltip').invoke('attr', 'style', 'display: none');
+
+    // Enable server pagination again
+    cy.get('[data-test="server_pagination-header"] div.pull-left').click();
+
+    cy.get('[data-test="error-tooltip"]').should('not.exist');
+
+    cy.get('div[aria-label="Row limit"]').click();
+
+    // Type 1000000
+    cy.get('div[aria-label="Row limit"]')
+      .find('.ant-select-selection-search-input:visible')
+      .type('1000000');
+
+    // Wait for 1 second
+    cy.wait(1000);
+
+    // Press enter
+    cy.get('div[aria-label="Row limit"]')
+      .find('.ant-select-selection-search-input:visible')
+      .type('{enter}');
+
+    // Wait for error tooltip to appear and verify its content
+    cy.get('[data-test="error-tooltip"]')
+      .should('be.visible')
+      .trigger('mouseover');
+
+    // Wait for tooltip content and verify
+    cy.get('.antd5-tooltip-inner').should('exist');
+    cy.get('.antd5-tooltip-inner').should('be.visible');
+
+    // Verify tooltip content separately
+    cy.get('.antd5-tooltip-inner').should('contain', 'Value cannot exceed');
+  });
+
+  it('Test sorting with server pagination enabled', () => {
+    cy.visitChartByParams({
+      ...VIZ_DEFAULTS,
+      metrics: ['count'],
+      groupby: ['name'],
+      row_limit: 100000,
+      server_pagination: true, // Enable server pagination
+    });
+
+    // Wait for the initial data load
+    cy.wait('@chartData');
+
+    // Get the first column header (name)
+    cy.get('.chart-container th').contains('name').as('nameHeader');
+
+    // Click to sort ascending
+    cy.get('@nameHeader').click();
+    cy.wait('@chartData');
+
+    // Verify first row starts with 'A'
+    cy.get('.chart-container td:first').invoke('text').should('match', /^[Aa]/);
+
+    // Click again to sort descending
+    cy.get('@nameHeader').click();
+    cy.wait('@chartData');
+
+    // Verify first row starts with 'Z'
+    cy.get('.chart-container td:first').invoke('text').should('match', /^[Zz]/);
+
+    // Test numeric sorting
+    cy.get('.chart-container th').contains('COUNT').as('countHeader');
+
+    // Click to sort ascending by count
+    cy.get('@countHeader').click();
+    cy.wait('@chartData');
+
+    // Get first two count values and verify ascending order
+    cy.get('.chart-container td:nth-child(2)').then($cells => {
+      const first = parseFloat($cells[0].textContent || '0');
+      const second = parseFloat($cells[1].textContent || '0');
+      expect(first).to.be.at.most(second);
+    });
+
+    // Click again to sort descending
+    cy.get('@countHeader').click();
+    cy.wait('@chartData');
+
+    // Get first two count values and verify descending order
+    cy.get('.chart-container td:nth-child(2)').then($cells => {
+      const first = parseFloat($cells[0].textContent || '0');
+      const second = parseFloat($cells[1].textContent || '0');
+      expect(first).to.be.at.least(second);
+    });
+  });
+
+  it('Test search with server pagination enabled', () => {
+    cy.visitChartByParams({
+      ...VIZ_DEFAULTS,
+      metrics: ['count'],
+      groupby: ['name', 'state'],
+      row_limit: 100000,
+      server_pagination: true,
+      include_search: true,
+    });
+
+    cy.wait('@chartData');
+
+    // Basic search test
+    cy.get('span.dt-global-filter input.form-control.input-sm').should(
+      'be.visible',
+    );
+
+    cy.get('span.dt-global-filter input.form-control.input-sm').type('John');
+
+    cy.wait('@chartData');
+
+    cy.get('.chart-container tbody tr').each($row => {
+      cy.wrap($row).contains(/John/i);
+    });
+
+    // Clear and test case-insensitive search
+    cy.get('span.dt-global-filter input.form-control.input-sm').clear();
+
+    cy.wait('@chartData');
+
+    cy.get('span.dt-global-filter input.form-control.input-sm').type('mary');
+
+    cy.wait('@chartData');
+
+    cy.get('.chart-container tbody tr').each($row => {
+      cy.wrap($row).contains(/Mary/i);
+    });
+
+    // Test special characters
+    cy.get('span.dt-global-filter input.form-control.input-sm').clear();
+
+    cy.get('span.dt-global-filter input.form-control.input-sm').type('Nicole');
+
+    cy.wait('@chartData');
+
+    cy.get('.chart-container tbody tr').each($row => {
+      cy.wrap($row).contains(/Nicole/i);
+    });
+
+    // Test no results
+    cy.get('span.dt-global-filter input.form-control.input-sm').clear();
+
+    cy.get('span.dt-global-filter input.form-control.input-sm').type('XYZ123');
+
+    cy.wait('@chartData');
+
+    cy.get('.chart-container').contains('No records found');
+
+    // Test column-specific search
+    cy.get('.search-select').should('be.visible');
+
+    cy.get('.search-select').click();
+
+    cy.get('.ant-select-dropdown').should('be.visible');
+
+    cy.get('.ant-select-item-option').contains('state').should('be.visible');
+
+    cy.get('.ant-select-item-option').contains('state').click();
+
+    cy.get('span.dt-global-filter input.form-control.input-sm').clear();
+
+    cy.get('span.dt-global-filter input.form-control.input-sm').type('CA');
+
+    cy.wait('@chartData');
+    cy.wait(1000);
+
+    cy.get('td[aria-labelledby="header-state"]').should('be.visible');
+
+    cy.get('td[aria-labelledby="header-state"]')
+      .first()
+      .should('contain', 'CA');
+  });
 });
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
index c9e8bf9db7fd..394c2c7f309c 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
@@ -26,6 +26,7 @@ import {
 import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
 
 export const DEFAULT_MAX_ROW = 100000;
+export const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
 
 // eslint-disable-next-line import/prefer-default-export
 export const TIME_FILTER_LABELS = {
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/index.ts b/superset-frontend/packages/superset-ui-core/src/validator/index.ts
index 1198c4e0a5b4..df8dc10a70f9 100644
--- a/superset-frontend/packages/superset-ui-core/src/validator/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/validator/index.ts
@@ -25,3 +25,4 @@ export { default as validateNonEmpty } from './validateNonEmpty';
 export { default as validateMaxValue } from './validateMaxValue';
 export { default as validateMapboxStylesUrl } from './validateMapboxStylesUrl';
 export { default as validateTimeComparisonRangeValues } from './validateTimeComparisonRangeValues';
+export { default as validateServerPagination } from './validateServerPagination';
diff --git a/superset-frontend/packages/superset-ui-core/src/validator/validateServerPagination.ts b/superset-frontend/packages/superset-ui-core/src/validator/validateServerPagination.ts
new file mode 100644
index 000000000000..02db3585a1ac
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/validator/validateServerPagination.ts
@@ -0,0 +1,30 @@
+/**
+ * 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 { t } from '../translation';
+
+export default function validateServerPagination(
+  v: unknown,
+  serverPagination: boolean,
+  max: number,
+) {
+  if (Number(v) > +max && !serverPagination) {
+    return t('Server pagination needs to be enabled for values over %s', max);
+  }
+  return false;
+}
diff --git a/superset-frontend/packages/superset-ui-core/test/validator/validateServerPagination.test.ts b/superset-frontend/packages/superset-ui-core/test/validator/validateServerPagination.test.ts
new file mode 100644
index 000000000000..5a638e5142f4
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/validator/validateServerPagination.test.ts
@@ -0,0 +1,46 @@
+/**
+ * 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 { validateServerPagination } from '@superset-ui/core';
+import './setup';
+
+test('validateServerPagination returns warning message when server pagination is disabled and value exceeds max', () => {
+  expect(validateServerPagination(100001, false, 100000)).toBeTruthy();
+  expect(validateServerPagination('150000', false, 100000)).toBeTruthy();
+  expect(validateServerPagination(200000, false, 100000)).toBeTruthy();
+});
+
+test('validateServerPagination returns false when server pagination is enabled', () => {
+  expect(validateServerPagination(100001, true, 100000)).toBeFalsy();
+  expect(validateServerPagination(150000, true, 100000)).toBeFalsy();
+  expect(validateServerPagination('200000', true, 100000)).toBeFalsy();
+});
+
+test('validateServerPagination returns false when value is below max', () => {
+  expect(validateServerPagination(50000, false, 100000)).toBeFalsy();
+  expect(validateServerPagination('75000', false, 100000)).toBeFalsy();
+  expect(validateServerPagination(99999, false, 100000)).toBeFalsy();
+});
+
+test('validateServerPagination handles edge cases', () => {
+  expect(validateServerPagination(undefined, false, 100000)).toBeFalsy();
+  expect(validateServerPagination(null, false, 100000)).toBeFalsy();
+  expect(validateServerPagination(NaN, false, 100000)).toBeFalsy();
+  expect(validateServerPagination('invalid', false, 100000)).toBeFalsy();
+});
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
index 21ce6541cf2f..3aacefb2f300 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+/* eslint-disable import/no-extraneous-dependencies */
 import {
   useCallback,
   useRef,
@@ -24,6 +25,7 @@ import {
   MutableRefObject,
   CSSProperties,
   DragEvent,
+  useEffect,
 } from 'react';
 
 import {
@@ -39,8 +41,9 @@ import {
   Row,
 } from 'react-table';
 import { matchSorter, rankings } from 'match-sorter';
-import { typedMemo, usePrevious } from '@superset-ui/core';
+import { styled, typedMemo, usePrevious } from '@superset-ui/core';
 import { isEqual } from 'lodash';
+import { Space } from 'antd';
 import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
 import SelectPageSize, {
   SelectPageSizeProps,
@@ -50,6 +53,8 @@ import SimplePagination from './components/Pagination';
 import useSticky from './hooks/useSticky';
 import { PAGE_SIZE_OPTIONS } from '../consts';
 import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
+import { SearchOption, SortByItem } from '../types';
+import SearchSelectDropdown from './components/SearchSelectDropdown';
 
 export interface DataTableProps<D extends object> extends TableOptions<D> {
   tableClassName?: string;
@@ -62,7 +67,12 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
   height?: string | number;
   serverPagination?: boolean;
   onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
-  serverPaginationData: { pageSize?: number; currentPage?: number };
+  serverPaginationData: {
+    pageSize?: number;
+    currentPage?: number;
+    sortBy?: SortByItem[];
+    searchColumn?: string;
+  };
   pageSize?: number;
   noResults?: string | ((filterString: string) => ReactNode);
   sticky?: boolean;
@@ -71,6 +81,14 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
   onColumnOrderChange: () => void;
   renderGroupingHeaders?: () => JSX.Element;
   renderTimeComparisonDropdown?: () => JSX.Element;
+  handleSortByChange: (sortBy: SortByItem[]) => void;
+  sortByFromParent: SortByItem[];
+  manualSearch?: boolean;
+  onSearchChange?: (searchText: string) => void;
+  initialSearchText?: string;
+  searchInputId?: string;
+  onSearchColChange: (searchCol: string) => void;
+  searchOptions: SearchOption[];
 }
 
 export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
@@ -81,6 +99,20 @@ const sortTypes = {
   alphanumeric: sortAlphanumericCaseInsensitive,
 };
 
+const StyledSpace = styled(Space)`
+  display: flex;
+  justify-content: flex-end;
+
+  .search-select-container {
+    display: flex;
+  }
+
+  .search-by-label {
+    align-self: center;
+    margin-right: 4px;
+  }
+`;
+
 // Be sure to pass our updateMyData and the skipReset option
 export default typedMemo(function DataTable<D extends object>({
   tableClassName,
@@ -105,6 +137,14 @@ export default typedMemo(function DataTable<D extends object>({
   onColumnOrderChange,
   renderGroupingHeaders,
   renderTimeComparisonDropdown,
+  handleSortByChange,
+  sortByFromParent = [],
+  manualSearch = false,
+  onSearchChange,
+  initialSearchText,
+  searchInputId,
+  onSearchColChange,
+  searchOptions,
   ...moreUseTableOptions
 }: DataTableProps<D>): JSX.Element {
   const tableHooks: PluginHook<D>[] = [
@@ -115,6 +155,7 @@ export default typedMemo(function DataTable<D extends object>({
     doSticky ? useSticky : [],
     hooks || [],
   ].flat();
+
   const columnNames = Object.keys(data?.[0] || {});
   const previousColumnNames = usePrevious(columnNames);
   const resultsSize = serverPagination ? rowCount : data.length;
@@ -127,7 +168,8 @@ export default typedMemo(function DataTable<D extends object>({
     ...initialState_,
     // zero length means all pages, the `usePagination` plugin does not
     // understand pageSize = 0
-    sortBy: sortByRef.current,
+    // sortBy: sortByRef.current,
+    sortBy: serverPagination ? sortByFromParent : sortByRef.current,
     pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
   };
   const defaultWrapperRef = useRef<HTMLDivElement>(null);
@@ -188,7 +230,13 @@ export default typedMemo(function DataTable<D extends object>({
     wrapStickyTable,
     setColumnOrder,
     allColumns,
-    state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
+    state: {
+      pageIndex,
+      pageSize,
+      globalFilter: filterValue,
+      sticky = {},
+      sortBy,
+    },
   } = useTable<D>(
     {
       columns,
@@ -198,10 +246,46 @@ export default typedMemo(function DataTable<D extends object>({
       globalFilter: defaultGlobalFilter,
       sortTypes,
       autoResetSortBy: !isEqual(columnNames, previousColumnNames),
+      manualSortBy: !!serverPagination,
       ...moreUseTableOptions,
     },
     ...tableHooks,
   );
+
+  const handleSearchChange = useCallback(
+    (query: string) => {
+      if (manualSearch && onSearchChange) {
+        onSearchChange(query);
+      } else {
+        setGlobalFilter(query);
+      }
+    },
+    [manualSearch, onSearchChange, setGlobalFilter],
+  );
+
+  // updating the sort by to the own State of table viz
+  useEffect(() => {
+    const serverSortBy = serverPaginationData?.sortBy || [];
+
+    if (serverPagination && !isEqual(sortBy, serverSortBy)) {
+      if (Array.isArray(sortBy) && sortBy.length > 0) {
+        const [sortByItem] = sortBy;
+        const matchingColumn = columns.find(col => col?.id === sortByItem?.id);
+
+        if (matchingColumn && 'columnKey' in matchingColumn) {
+          const sortByWithColumnKey: SortByItem = {
+            ...sortByItem,
+            key: (matchingColumn as { columnKey: string }).columnKey,
+          };
+
+          handleSortByChange([sortByWithColumnKey]);
+        }
+      } else {
+        handleSortByChange([]);
+      }
+    }
+  }, [sortBy]);
+
   // make setPageSize accept 0
   const setPageSize = (size: number) => {
     if (serverPagination) {
@@ -355,6 +439,7 @@ export default typedMemo(function DataTable<D extends object>({
     resultOnPageChange = (pageNumber: number) =>
       onServerPaginationChange(pageNumber, serverPageSize);
   }
+
   return (
     <div
       ref={wrapperRef}
@@ -381,16 +466,31 @@ export default typedMemo(function DataTable<D extends object>({
               ) : null}
             </div>
             {searchInput ? (
-              <div className="col-sm-6">
+              <StyledSpace className="col-sm-6">
+                {serverPagination && (
+                  <div className="search-select-container">
+                    <span className="search-by-label">Search by: </span>
+                    <SearchSelectDropdown
+                      searchOptions={searchOptions}
+                      value={serverPaginationData?.searchColumn || ''}
+                      onChange={onSearchColChange}
+                    />
+                  </div>
+                )}
                 <GlobalFilter<D>
                   searchInput={
                     typeof searchInput === 'boolean' ? undefined : searchInput
                   }
                   preGlobalFilteredRows={preGlobalFilteredRows}
-                  setGlobalFilter={setGlobalFilter}
-                  filterValue={filterValue}
+                  setGlobalFilter={
+                    manualSearch ? handleSearchChange : setGlobalFilter
+                  }
+                  filterValue={manualSearch ? initialSearchText : filterValue}
+                  id={searchInputId}
+                  serverPagination={!!serverPagination}
+                  rowCount={rowCount}
                 />
-              </div>
+              </StyledSpace>
             ) : null}
             {renderTimeComparisonDropdown ? (
               <div
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
index 89e18140f22e..75d6594cbcd4 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
@@ -16,7 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { memo, ComponentType, ChangeEventHandler } from 'react';
+import {
+  memo,
+  ComponentType,
+  ChangeEventHandler,
+  useRef,
+  useEffect,
+} from 'react';
 import { Row, FilterValue } from 'react-table';
 import useAsyncState from '../utils/useAsyncState';
 
@@ -24,8 +30,12 @@ export interface SearchInputProps {
   count: number;
   value: string;
   onChange: ChangeEventHandler<HTMLInputElement>;
+  onBlur?: () => void;
+  inputRef?: React.RefObject<HTMLInputElement>;
 }
 
+const isSearchFocused = new Map();
+
 export interface GlobalFilterProps<D extends object> {
   preGlobalFilteredRows: Row<D>[];
   // filter value cannot be `undefined` otherwise React will report component
@@ -33,17 +43,28 @@ export interface GlobalFilterProps<D extends object> {
   filterValue: string;
   setGlobalFilter: (filterValue: FilterValue) => void;
   searchInput?: ComponentType<SearchInputProps>;
+  id?: string;
+  serverPagination: boolean;
+  rowCount: number;
 }
 
-function DefaultSearchInput({ count, value, onChange }: SearchInputProps) {
+function DefaultSearchInput({
+  count,
+  value,
+  onChange,
+  onBlur,
+  inputRef,
+}: SearchInputProps) {
   return (
     <span className="dt-global-filter">
       Search{' '}
       <input
+        ref={inputRef}
         className="form-control input-sm"
         placeholder={`${count} records...`}
         value={value}
         onChange={onChange}
+        onBlur={onBlur}
       />
     </span>
   );
@@ -56,8 +77,13 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
   filterValue = '',
   searchInput,
   setGlobalFilter,
+  id = '',
+  serverPagination,
+  rowCount,
 }: GlobalFilterProps<D>) {
-  const count = preGlobalFilteredRows.length;
+  const count = serverPagination ? rowCount : preGlobalFilteredRows.length;
+  const inputRef = useRef<HTMLInputElement>(null);
+
   const [value, setValue] = useAsyncState(
     filterValue,
     (newValue: string) => {
@@ -66,17 +92,37 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
     200,
   );
 
+  // Preserve focus during server-side filtering to maintain a better user experience
+  useEffect(() => {
+    if (
+      serverPagination &&
+      isSearchFocused.get(id) &&
+      document.activeElement !== inputRef.current
+    ) {
+      inputRef.current?.focus();
+    }
+  }, [value, serverPagination]);
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const target = e.target as HTMLInputElement;
+    e.preventDefault();
+    isSearchFocused.set(id, true);
+    setValue(target.value);
+  };
+
+  const handleBlur = () => {
+    isSearchFocused.set(id, false);
+  };
+
   const SearchInput = searchInput || DefaultSearchInput;
 
   return (
     <SearchInput
       count={count}
       value={value}
-      onChange={e => {
-        const target = e.target as HTMLInputElement;
-        e.preventDefault();
-        setValue(target.value);
-      }}
+      inputRef={inputRef}
+      onChange={handleChange}
+      onBlur={handleBlur}
     />
   );
 });
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx
new file mode 100644
index 000000000000..b58cf2e8a225
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx
@@ -0,0 +1,53 @@
+/**
+ * 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.
+ */
+/* eslint-disable import/no-extraneous-dependencies */
+import { styled } from '@superset-ui/core';
+import { Select } from 'antd';
+import { SearchOption } from '../../types';
+
+const StyledSelect = styled(Select)`
+  width: 120px;
+  margin-right: 8px;
+`;
+
+interface SearchSelectDropdownProps {
+  /** The currently selected search column value */
+  value?: string;
+  /** Callback triggered when a new search column is selected */
+  onChange: (searchCol: string) => void;
+  /** Available search column options to populate the dropdown */
+  searchOptions: SearchOption[];
+}
+
+function SearchSelectDropdown({
+  value,
+  onChange,
+  searchOptions,
+}: SearchSelectDropdownProps) {
+  return (
+    <StyledSelect
+      className="search-select"
+      value={value || (searchOptions?.[0]?.value ?? '')}
+      options={searchOptions}
+      onChange={onChange}
+    />
+  );
+}
+
+export default SearchSelectDropdown;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts b/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts
index 6c319f768453..9d591a081826 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts
@@ -115,3 +115,11 @@ declare module 'react-table' {
     extends UseTableHooks<D>,
       UseSortByHooks<D> {}
 }
+
+interface TableOwnState {
+  currentPage?: number;
+  pageSize?: number;
+  sortColumn?: string;
+  sortOrder?: 'asc' | 'desc';
+  searchText?: string;
+}
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts b/superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts
index b703d66d9715..8a5d20ed5bca 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts
@@ -18,6 +18,7 @@
  */
 
 import { SetDataMaskHook } from '@superset-ui/core';
+import { TableOwnState } from '../types/react-table';
 
 export const updateExternalFormData = (
   setDataMask: SetDataMaskHook = () => {},
@@ -30,3 +31,11 @@ export const updateExternalFormData = (
       pageSize,
     },
   });
+
+export const updateTableOwnState = (
+  setDataMask: SetDataMaskHook = () => {},
+  modifiedOwnState: TableOwnState,
+) =>
+  setDataMask({
+    ownState: modifiedOwnState,
+  });
diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index 88a326544e6c..475056c75034 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -24,6 +24,7 @@ import {
   useState,
   MouseEvent,
   KeyboardEvent as ReactKeyboardEvent,
+  useEffect,
 } from 'react';
 
 import {
@@ -61,10 +62,12 @@ import {
   PlusCircleOutlined,
   TableOutlined,
 } from '@ant-design/icons';
-import { isEmpty } from 'lodash';
+import { debounce, isEmpty, isEqual } from 'lodash';
 import {
   ColorSchemeEnum,
   DataColumnMeta,
+  SearchOption,
+  SortByItem,
   TableChartTransformedProps,
 } from './types';
 import DataTable, {
@@ -77,7 +80,7 @@ import DataTable, {
 import Styles from './Styles';
 import { formatColumnValue } from './utils/formatValue';
 import { PAGE_SIZE_OPTIONS } from './consts';
-import { updateExternalFormData } from './DataTable/utils/externalAPIs';
+import { updateTableOwnState } from './DataTable/utils/externalAPIs';
 import getScrollBarSize from './DataTable/utils/getScrollBarSize';
 
 type ValueRange = [number, number];
@@ -176,20 +179,26 @@ function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
   return sortIcon;
 }
 
-function SearchInput({ count, value, onChange }: SearchInputProps) {
-  return (
-    <span className="dt-global-filter">
-      {t('Search')}{' '}
-      <input
-        aria-label={t('Search %s records', count)}
-        className="form-control input-sm"
-        placeholder={tn('search.num_records', count)}
-        value={value}
-        onChange={onChange}
-      />
-    </span>
-  );
-}
+const SearchInput = ({
+  count,
+  value,
+  onChange,
+  onBlur,
+  inputRef,
+}: SearchInputProps) => (
+  <span className="dt-global-filter">
+    {t('Search')}{' '}
+    <input
+      ref={inputRef}
+      aria-label={t('Search %s records', count)}
+      className="form-control input-sm"
+      placeholder={tn('search.num_records', count)}
+      value={value}
+      onChange={onChange}
+      onBlur={onBlur}
+    />
+  </span>
+);
 
 function SelectPageSize({
   options,
@@ -267,6 +276,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
     isUsingTimeComparison,
     basicColorFormatters,
     basicColorColumnFormatters,
+    hasServerPageLengthChanged,
+    serverPageLength,
+    slice_id,
   } = props;
   const comparisonColumns = [
     { key: 'all', label: t('Display all') },
@@ -679,7 +691,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
   );
 
   const getColumnConfigs = useCallback(
-    (column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
+    (
+      column: DataColumnMeta,
+      i: number,
+    ): ColumnWithLooseAccessor<D> & {
+      columnKey: string;
+    } => {
       const {
         key,
         label: originalLabel,
@@ -766,6 +783,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         // must use custom accessor to allow `.` in column names
         // typing is incorrect in current version of `@types/react-table`
         // so we ask TS not to check.
+        columnKey: key,
         accessor: ((datum: D) => datum[key]) as never,
         Cell: ({ value, row }: { value: DataRecordValue; row: Row<D> }) => {
           const [isHtml, text] = formatColumnValue(column, value);
@@ -1058,13 +1076,50 @@ export default function TableChart<D extends DataRecord = DataRecord>(
     [visibleColumnsMeta, getColumnConfigs],
   );
 
+  const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);
+
+  useEffect(() => {
+    const options = (
+      columns as unknown as ColumnWithLooseAccessor &
+        {
+          columnKey: string;
+          sortType?: string;
+        }[]
+    )
+      .filter(col => col?.sortType === 'alphanumeric')
+      .map(column => ({
+        value: column.columnKey,
+        label: column.columnKey,
+      }));
+
+    if (!isEqual(options, searchOptions)) {
+      setSearchOptions(options || []);
+    }
+  }, [columns]);
+
   const handleServerPaginationChange = useCallback(
     (pageNumber: number, pageSize: number) => {
-      updateExternalFormData(setDataMask, pageNumber, pageSize);
+      const modifiedOwnState = {
+        ...serverPaginationData,
+        currentPage: pageNumber,
+        pageSize,
+      };
+      updateTableOwnState(setDataMask, modifiedOwnState);
     },
     [setDataMask],
   );
 
+  useEffect(() => {
+    if (hasServerPageLengthChanged) {
+      const modifiedOwnState = {
+        ...serverPaginationData,
+        currentPage: 0,
+        pageSize: serverPageLength,
+      };
+      updateTableOwnState(setDataMask, modifiedOwnState);
+    }
+  }, []);
+
   const handleSizeChange = useCallback(
     ({ width, height }: { width: number; height: number }) => {
       setTableSize({ width, height });
@@ -1100,6 +1155,42 @@ export default function TableChart<D extends DataRecord = DataRecord>(
 
   const { width: widthFromState, height: heightFromState } = tableSize;
 
+  const handleSortByChange = useCallback(
+    (sortBy: SortByItem[]) => {
+      if (!serverPagination) return;
+      const modifiedOwnState = {
+        ...serverPaginationData,
+        sortBy,
+      };
+      updateTableOwnState(setDataMask, modifiedOwnState);
+    },
+    [setDataMask, serverPagination],
+  );
+
+  const handleSearch = (searchText: string) => {
+    const modifiedOwnState = {
+      ...(serverPaginationData || {}),
+      searchColumn:
+        serverPaginationData?.searchColumn || searchOptions[0]?.value,
+      searchText,
+      currentPage: 0, // Reset to first page when searching
+    };
+    updateTableOwnState(setDataMask, modifiedOwnState);
+  };
+
+  const debouncedSearch = debounce(handleSearch, 800);
+
+  const handleChangeSearchCol = (searchCol: string) => {
+    if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
+      const modifiedOwnState = {
+        ...(serverPaginationData || {}),
+        searchColumn: searchCol,
+        searchText: '',
+      };
+      updateTableOwnState(setDataMask, modifiedOwnState);
+    }
+  };
+
   return (
     <Styles>
       <DataTable<D>
@@ -1115,6 +1206,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         serverPagination={serverPagination}
         onServerPaginationChange={handleServerPaginationChange}
         onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}
+        initialSearchText={serverPaginationData?.searchText || ''}
+        sortByFromParent={serverPaginationData?.sortBy || []}
+        searchInputId={`${slice_id}-search`}
         // 9 page items in > 340px works well even for 100+ pages
         maxPageItemCount={width > 340 ? 9 : 7}
         noResults={getNoResultsMessage}
@@ -1128,6 +1222,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         renderTimeComparisonDropdown={
           isUsingTimeComparison ? renderTimeComparisonDropdown : undefined
         }
+        handleSortByChange={handleSortByChange}
+        onSearchColChange={handleChangeSearchCol}
+        manualSearch={serverPagination}
+        onSearchChange={debouncedSearch}
+        searchOptions={searchOptions}
       />
     </Styles>
   );
diff --git a/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts
index 5b9cb684ed4a..439df369f5a8 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts
@@ -22,6 +22,7 @@ import {
   ensureIsArray,
   getMetricLabel,
   isPhysicalColumn,
+  QueryFormOrderBy,
   QueryMode,
   QueryObject,
   removeDuplicates,
@@ -34,7 +35,7 @@ import {
 } from '@superset-ui/chart-controls';
 import { isEmpty } from 'lodash';
 import { TableChartFormData } from './types';
-import { updateExternalFormData } from './DataTable/utils/externalAPIs';
+import { updateTableOwnState } from './DataTable/utils/externalAPIs';
 
 /**
  * Infer query mode from form data. If `all_columns` is set, then raw records mode,
@@ -191,18 +192,40 @@ const buildQuery: BuildQuery<TableChartFormData> = (
 
     const moreProps: Partial<QueryObject> = {};
     const ownState = options?.ownState ?? {};
-    if (formDataCopy.server_pagination) {
-      moreProps.row_limit =
-        ownState.pageSize ?? formDataCopy.server_page_length;
-      moreProps.row_offset =
-        (ownState.currentPage ?? 0) * (ownState.pageSize ?? 0);
+    // Build Query flag to check if its for either download as csv, excel or json
+    const isDownloadQuery =
+      ['csv', 'xlsx'].includes(formData?.result_format || '') ||
+      (formData?.result_format === 'json' &&
+        formData?.result_type === 'results');
+
+    if (isDownloadQuery) {
+      moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
+      moreProps.row_offset = 0;
+    }
+
+    if (!isDownloadQuery && formDataCopy.server_pagination) {
+      const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
+      const currentPage = ownState.currentPage ?? 0;
+
+      moreProps.row_limit = pageSize;
+      moreProps.row_offset = currentPage * pageSize;
+    }
+
+    // getting sort by in case of server pagination from own state
+    let sortByFromOwnState: QueryFormOrderBy[] | undefined;
+    if (Array.isArray(ownState?.sortBy) && ownState?.sortBy.length > 0) {
+      const sortByItem = ownState?.sortBy[0];
+      sortByFromOwnState = [[sortByItem?.key, !sortByItem?.desc]];
     }
 
     let queryObject = {
       ...baseQueryObject,
       columns,
       extras,
-      orderby,
+      orderby:
+        formData.server_pagination && sortByFromOwnState
+          ? sortByFromOwnState
+          : orderby,
       metrics,
       post_processing: postProcessing,
       time_offsets: timeOffsets,
@@ -216,11 +239,12 @@ const buildQuery: BuildQuery<TableChartFormData> = (
         JSON.stringify(queryObject.filters)
     ) {
       queryObject = { ...queryObject, row_offset: 0 };
-      updateExternalFormData(
-        options?.hooks?.setDataMask,
-        0,
-        queryObject.row_limit ?? 0,
-      );
+      const modifiedOwnState = {
+        ...(options?.ownState || {}),
+        currentPage: 0,
+        pageSize: queryObject.row_limit ?? 0,
+      };
+      updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
     }
     // Because we use same buildQuery for all table on the page we need split them by id
     options?.hooks?.setCachedChanges({
@@ -252,12 +276,32 @@ const buildQuery: BuildQuery<TableChartFormData> = (
     }
 
     if (formData.server_pagination) {
+      // Add search filter if search text exists
+      if (ownState.searchText && ownState?.searchColumn) {
+        queryObject = {
+          ...queryObject,
+          filters: [
+            ...(queryObject.filters || []),
+            {
+              col: ownState?.searchColumn,
+              op: 'ILIKE',
+              val: `${ownState.searchText}%`,
+            },
+          ],
+        };
+      }
+    }
+
+    // Now since row limit control is always visible even
+    // in case of server pagination
+    // we must use row limit from form data
+    if (formData.server_pagination && !isDownloadQuery) {
       return [
         { ...queryObject },
         {
           ...queryObject,
           time_offsets: [],
-          row_limit: 0,
+          row_limit: Number(formData?.row_limit) ?? 0,
           row_offset: 0,
           post_processing: [],
           is_rowcount: true,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index f6798b798483..aab66f800ddb 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -28,7 +28,10 @@ import {
   ControlStateMapping,
   D3_TIME_FORMAT_OPTIONS,
   Dataset,
+  DEFAULT_MAX_ROW,
+  DEFAULT_MAX_ROW_TABLE_SERVER,
   defineSavedMetrics,
+  formatSelectOptions,
   getStandardizedControls,
   QueryModeLabel,
   sections,
@@ -40,11 +43,14 @@ import {
   getMetricLabel,
   isAdhocColumn,
   isPhysicalColumn,
+  legacyValidateInteger,
   QueryFormColumn,
   QueryFormMetric,
   QueryMode,
   SMART_DATE_ID,
   t,
+  validateMaxValue,
+  validateServerPagination,
 } from '@superset-ui/core';
 
 import { isEmpty, last } from 'lodash';
@@ -188,6 +194,15 @@ const processComparisonColumns = (columns: any[], suffix: string) =>
     })
     .flat();
 
+/*
+Options for row limit control
+*/
+
+export const ROW_LIMIT_OPTIONS_TABLE = [
+  10, 50, 100, 250, 500, 1000, 5000, 10000, 50000, 100000, 150000, 200000,
+  250000, 300000, 350000, 400000, 450000, 500000,
+];
+
 const config: ControlPanelConfig = {
   controlPanelSections: [
     {
@@ -342,14 +357,6 @@ const config: ControlPanelConfig = {
           },
         ],
         [
-          {
-            name: 'row_limit',
-            override: {
-              default: 1000,
-              visibility: ({ controls }: ControlPanelsContainerProps) =>
-                !controls?.server_pagination?.value,
-            },
-          },
           {
             name: 'server_page_length',
             config: {
@@ -364,6 +371,47 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [
+          {
+            name: 'row_limit',
+            config: {
+              type: 'SelectControl',
+              freeForm: true,
+              label: t('Row limit'),
+              clearable: false,
+              mapStateToProps: state => ({
+                maxValue: state?.common?.conf?.TABLE_VIZ_MAX_ROW_SERVER,
+                server_pagination: state?.form_data?.server_pagination,
+                maxValueWithoutServerPagination:
+                  state?.common?.conf?.SQL_MAX_ROW,
+              }),
+              validators: [
+                legacyValidateInteger,
+                (v, state) =>
+                  validateMaxValue(
+                    v,
+                    state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
+                  ),
+                (v, state) =>
+                  validateServerPagination(
+                    v,
+                    state?.server_pagination,
+                    state?.maxValueWithoutServerPagination || DEFAULT_MAX_ROW,
+                  ),
+              ],
+              // Re run the validations when this control value
+              validationDependancies: ['server_pagination'],
+              default: 10000,
+              choices: formatSelectOptions(ROW_LIMIT_OPTIONS_TABLE),
+              description: t(
+                'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
+              ),
+            },
+            override: {
+              default: 1000,
+            },
+          },
+        ],
         [
           {
             name: 'order_desc',
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index d62d9cb92c95..86d4efed8f4e 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -90,6 +90,15 @@ const processDataRecords = memoizeOne(function processDataRecords(
   return data;
 });
 
+// Create a map to store cached values per slice
+const sliceCache = new Map<
+  number,
+  {
+    cachedServerLength: number;
+    passedColumns?: DataColumnMeta[];
+  }
+>();
+
 const calculateDifferences = (
   originalValue: number,
   comparisonValue: number,
@@ -480,6 +489,7 @@ const transformProps = (
     comparison_color_enabled: comparisonColorEnabled = false,
     comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
     comparison_type,
+    slice_id,
   } = formData;
   const isUsingTimeComparison =
     !isEmpty(time_compare) &&
@@ -675,6 +685,26 @@ const transformProps = (
     conditionalFormatting,
   );
 
+  // Get cached values for this slice
+  const cachedValues = sliceCache.get(slice_id);
+  let hasServerPageLengthChanged = false;
+
+  if (
+    cachedValues?.cachedServerLength !== undefined &&
+    cachedValues.cachedServerLength !== serverPageLength
+  ) {
+    hasServerPageLengthChanged = true;
+  }
+
+  // Update cache with new values
+  sliceCache.set(slice_id, {
+    cachedServerLength: serverPageLength,
+    passedColumns:
+      Array.isArray(passedColumns) && passedColumns?.length > 0
+        ? passedColumns
+        : cachedValues?.passedColumns,
+  });
+
   const startDateOffset = chartProps.rawFormData?.start_date_offset;
   return {
     height,
@@ -682,7 +712,10 @@ const transformProps = (
     isRawRecords: queryMode === QueryMode.Raw,
     data: passedData,
     totals,
-    columns: passedColumns,
+    columns:
+      Array.isArray(passedColumns) && passedColumns?.length > 0
+        ? passedColumns
+        : cachedValues?.passedColumns || [],
     serverPagination,
     metrics,
     percentMetrics,
@@ -697,7 +730,9 @@ const transformProps = (
     includeSearch,
     rowCount,
     pageSize: serverPagination
-      ? serverPageLength
+      ? serverPaginationData?.pageSize
+        ? serverPaginationData?.pageSize
+        : serverPageLength
       : getPageSize(pageLength, data.length, columns.length),
     filters: filterState.filters,
     emitCrossFilters,
@@ -711,6 +746,9 @@ const transformProps = (
     basicColorFormatters,
     startDateOffset,
     basicColorColumnFormatters,
+    hasServerPageLengthChanged,
+    serverPageLength,
+    slice_id,
   };
 };
 
diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts
index 7460a27c4618..1b7cfd45904c 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts
@@ -114,13 +114,32 @@ export type BasicColorFormatterType = {
   mainArrow: string;
 };
 
+export type SortByItem = {
+  id: string;
+  key: string;
+  desc?: boolean;
+};
+
+export type SearchOption = {
+  value: string;
+  label: string;
+};
+
+export interface ServerPaginationData {
+  pageSize?: number;
+  currentPage?: number;
+  sortBy?: SortByItem[];
+  searchText?: string;
+  searchColumn?: string;
+}
+
 export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
   timeGrain?: TimeGranularity;
   height: number;
   width: number;
   rowCount?: number;
   serverPagination: boolean;
-  serverPaginationData: { pageSize?: number; currentPage?: number };
+  serverPaginationData: ServerPaginationData;
   setDataMask: SetDataMaskHook;
   isRawRecords?: boolean;
   data: D[];
@@ -152,6 +171,11 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
   basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
   basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
   startDateOffset?: string;
+  // For explore page to reset the server Pagination data
+  // if server page length is changed from control panel
+  hasServerPageLengthChanged: boolean;
+  serverPageLength: number;
+  slice_id: number;
 }
 
 export enum ColorSchemeEnum {
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx
index 25fc045d2105..7b641d0eaecc 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.jsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx
@@ -327,6 +327,10 @@ class ChartRenderer extends Component {
       ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
       ? { inContextMenu: this.state.inContextMenu }
       : {};
+    // By pass no result component when server pagination is enabled & the table has a backend search query
+    const bypassNoResult = !(
+      formData?.server_pagination && (ownState?.searchText?.length || 0) > 0
+    );
 
     return (
       <>
@@ -367,6 +371,7 @@ class ChartRenderer extends Component {
             postTransformProps={postTransformProps}
             emitCrossFilters={emitCrossFilters}
             legendState={this.state.legendState}
+            enableNoResults={bypassNoResult}
             {...drillToDetailProps}
           />
         </div>
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js b/superset-frontend/src/explore/reducers/exploreReducer.js
index 2677c1e3ff55..156459353e82 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -214,6 +214,52 @@ export default function exploreReducer(state = {}, action) {
         currentControlsState = transformed.controlsState;
       }
 
+      const dependantControls = Object.entries(state.controls)
+        .filter(
+          ([, item]) =>
+            Array.isArray(item?.validationDependancies) &&
+            item.validationDependancies.includes(controlName),
+        )
+        .map(([key, item]) => ({
+          controlState: item,
+          dependantControlName: key,
+        }));
+
+      let updatedControlStates = {};
+      if (dependantControls.length > 0) {
+        const updatedControls = dependantControls.map(
+          ({ controlState, dependantControlName }) => {
+            // overwrite state form data with current control value as the redux state will not
+            // have latest action value
+            const overWrittenState = {
+              ...state,
+              form_data: {
+                ...state.form_data,
+                [controlName]: action.value,
+              },
+            };
+
+            return {
+              // Re run validation for dependant controls
+              controlState: getControlStateFromControlConfig(
+                controlState,
+                overWrittenState,
+                controlState?.value,
+              ),
+              dependantControlName,
+            };
+          },
+        );
+
+        updatedControlStates = updatedControls.reduce(
+          (acc, { controlState, dependantControlName }) => {
+            acc[dependantControlName] = { ...controlState };
+            return acc;
+          },
+          {},
+        );
+      }
+
       return {
         ...state,
         form_data: new_form_data,
@@ -227,6 +273,7 @@ export default function exploreReducer(state = {}, action) {
             },
           }),
           ...rerenderedControls,
+          ...updatedControlStates,
         },
       };
     },
diff --git a/superset/common/query_context_factory.py b/superset/common/query_context_factory.py
index 3e3fe2913946..7a9e9e150283 100644
--- a/superset/common/query_context_factory.py
+++ b/superset/common/query_context_factory.py
@@ -65,12 +65,23 @@ def create(  # pylint: disable=too-many-arguments
 
         result_type = result_type or ChartDataResultType.FULL
         result_format = result_format or ChartDataResultFormat.JSON
+
+        # The server pagination var is extracted from form data as the
+        # row limit for server pagination is more
+        # This particular flag server_pagination only exists for table viz type
+        server_pagination = (
+            bool(form_data.get("server_pagination")) if form_data else False
+        )
+
         queries_ = [
             self._process_query_object(
                 datasource_model_instance,
                 form_data,
                 self._query_object_factory.create(
-                    result_type, datasource=datasource, **query_obj
+                    result_type,
+                    datasource=datasource,
+                    server_pagination=server_pagination,
+                    **query_obj,
                 ),
             )
             for query_obj in queries
diff --git a/superset/common/query_object_factory.py b/superset/common/query_object_factory.py
index 63d1ef09665c..063ba023481e 100644
--- a/superset/common/query_object_factory.py
+++ b/superset/common/query_object_factory.py
@@ -57,6 +57,7 @@ def create(  # pylint: disable=too-many-arguments
         row_limit: int | None = None,
         time_range: str | None = None,
         time_shift: str | None = None,
+        server_pagination: bool | None = None,
         **kwargs: Any,
     ) -> QueryObject:
         datasource_model_instance = None
@@ -64,7 +65,12 @@ def create(  # pylint: disable=too-many-arguments
             datasource_model_instance = self._convert_to_model(datasource)
         processed_extras = self._process_extras(extras)
         result_type = kwargs.setdefault("result_type", parent_result_type)
-        row_limit = self._process_row_limit(row_limit, result_type)
+
+        # Process row limit taking server pagination into account
+        row_limit = self._process_row_limit(
+            row_limit, result_type, server_pagination=server_pagination
+        )
+
         processed_time_range = self._process_time_range(
             time_range, kwargs.get("filters"), kwargs.get("columns")
         )
@@ -96,14 +102,27 @@ def _process_extras(
         return extras
 
     def _process_row_limit(
-        self, row_limit: int | None, result_type: ChartDataResultType
+        self,
+        row_limit: int | None,
+        result_type: ChartDataResultType,
+        server_pagination: bool | None = None,
     ) -> int:
+        """Process row limit taking into account server pagination.
+
+        :param row_limit: The requested row limit
+        :param result_type: The type of result being processed
+        :param server_pagination: Whether server-side pagination is enabled
+        :return: The processed row limit
+        """
         default_row_limit = (
             self._config["SAMPLES_ROW_LIMIT"]
             if result_type == ChartDataResultType.SAMPLES
             else self._config["ROW_LIMIT"]
         )
-        return apply_max_row_limit(row_limit or default_row_limit)
+        return apply_max_row_limit(
+            row_limit or default_row_limit,
+            server_pagination=server_pagination,
+        )
 
     @staticmethod
     def _process_time_range(
diff --git a/superset/config.py b/superset/config.py
index a82c080b4299..a01c380a7e98 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -969,6 +969,10 @@ class D3TimeFormat(TypedDict, total=False):
 # Maximum number of rows returned for any analytical database query
 SQL_MAX_ROW = 100000
 
+# Maximum number of rows for any query with Server Pagination in Table Viz type
+TABLE_VIZ_MAX_ROW_SERVER = 500000
+
+
 # Maximum number of rows displayed in SQL Lab UI
 # Is set to avoid out of memory/localstorage issues in browsers. Does not affect
 # exported CSVs
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 2b80c89f6126..f1792ac47a32 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -1741,24 +1741,30 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool | None = None,
 ) -> int:
     """
-    Override row limit if max global limit is defined
+    Override row limit based on server pagination setting
 
     :param limit: requested row limit
-    :param max_limit: Maximum allowed row limit
+    :param server_pagination: whether server-side pagination
+    is enabled, defaults to None
     :return: Capped row limit
 
-    >>> apply_max_row_limit(100000, 10)
-    10
-    >>> apply_max_row_limit(10, 100000)
-    10
-    >>> apply_max_row_limit(0, 10000)
-    10000
-    """
-    if max_limit is None:
-        max_limit = current_app.config["SQL_MAX_ROW"]
+    >>> apply_max_row_limit(600000, server_pagination=True)  # Server pagination
+    500000
+    >>> apply_max_row_limit(600000, server_pagination=False)  # No pagination
+    50000
+    >>> apply_max_row_limit(5000)  # No server_pagination specified
+    5000
+    >>> apply_max_row_limit(0)  # Zero returns default max limit
+    50000
+    """
+    max_limit = (
+        current_app.config["TABLE_VIZ_MAX_ROW_SERVER"]
+        if server_pagination
+        else current_app.config["SQL_MAX_ROW"]
+    )
     if limit != 0:
         return min(max_limit, limit)
     return max_limit
diff --git a/superset/views/base.py b/superset/views/base.py
index a598994f19f5..43b4f7767181 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -109,6 +109,7 @@
     "JWT_ACCESS_CSRF_COOKIE_NAME",
     "SQLLAB_QUERY_RESULT_TIMEOUT",
     "SYNC_DB_PERMISSIONS_IN_ASYNC_MODE",
+    "TABLE_VIZ_MAX_ROW_SERVER",
 )
 
 logger = logging.getLogger(__name__)
diff --git a/tests/unit_tests/common/test_query_object_factory.py b/tests/unit_tests/common/test_query_object_factory.py
index 8ff362dec362..379137a34c4f 100644
--- a/tests/unit_tests/common/test_query_object_factory.py
+++ b/tests/unit_tests/common/test_query_object_factory.py
@@ -14,7 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from typing import Any, Optional
+from typing import Any
 from unittest.mock import Mock
 
 from pytest import fixture  # noqa: PT013
@@ -45,9 +45,15 @@ def connector_registry() -> Mock:
     return mock
 
 
-def apply_max_row_limit(limit: int, max_limit: Optional[int] = None) -> int:
-    if max_limit is None:
-        max_limit = create_app_config()["SQL_MAX_ROW"]
+def apply_max_row_limit(
+    limit: int,
+    server_pagination: bool | None = None,
+) -> int:
+    max_limit = (
+        create_app_config()["TABLE_VIZ_MAX_ROW_SERVER"]
+        if server_pagination
+        else create_app_config()["SQL_MAX_ROW"]
+    )
     if limit != 0:
         return min(max_limit, limit)
     return max_limit


Reviews:
user: login: korbit-ai[bot]
body: ### Review by Korbit AI

###### Korbit automatically attempts to detect when you fix issues in new commits.

| Category | Issue | Status |
|----------|-------|----------|
| <sub>![Readability](https://img.shields.io/badge/Readability-0284c7)</sub> | Ambiguous constant names ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007021 ) | ✅ Fix detected |
| <sub>![Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub> | Incomplete useEffect documentation ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007051 ) | ✅ Fix detected |
| <sub>![Performance](https://img.shields.io/badge/Performance-4f46e5)</sub> | Global State Map Memory Leak ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007028 ) | 🧠 Incorrect |
| <sub>![Readability](https://img.shields.io/badge/Readability-0284c7)</sub> | Hard-coded style values ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074006992 ) | 🧠 Not in standard |
| <sub>![Readability](https://img.shields.io/badge/Readability-0284c7)</sub> | Unnecessarily verbose event handler ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007010 ) | ✅ Fix detected |
| <sub>![Performance](https://img.shields.io/badge/Performance-4f46e5)</sub> | Inefficient Focus Effect Dependencies ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007047 ) | ✅ Fix detected |
| <sub>![Readability](https://img.shields.io/badge/Readability-0284c7)</sub> | Unclear default function implementation ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007064 ) | 🧠 Not in standard |
| <sub>![Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub> | Unsafe Fallback Value Logic ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007003 ) | ✅ Fix detected |
| <sub>![Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub> | Overly permissive object type for table state ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007083 ) | ✅ Fix detected |
| <sub>![Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub> | Missing Interface Property Documentation ▹ [view]( https://github.com/apache/superset/pull/33357#discussion_r2074007000 ) | ✅ Fix detected |






<details>
  <summary>Files scanned</summary>
&#8203;



| File Path | Reviewed |
| ------------------ | -------------- |
| superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts | ✅ |
| superset-frontend/packages/superset-ui-core/src/validator/index.ts | ✅ |
| superset-frontend/packages/superset-ui-core/src/validator/validateServerPagination.ts | ✅ |
| superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts | ✅ |
| superset-frontend/packages/superset-ui-chart-controls/src/constants.ts | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/types.ts | ✅ |
| superset/common/query_object_factory.py | ✅ |
| superset/common/query_context_factory.py | ✅ |
| superset-frontend/src/explore/reducers/exploreReducer.js | ✅ |
| superset-frontend/src/components/Chart/ChartRenderer.jsx | ✅ |
| superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts | ✅ |
| superset/views/base.py | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/transformProps.ts | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx | ✅ |
| superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx | ✅ |
| superset/utils/core.py | ✅ |
| superset/config.py | ✅ |
| superset/models/helpers.py | ✅ |


> Explore our [documentation](https://docs.korbit.ai) to understand the languages and file types [we support](https://docs.korbit.ai/configuration/supported-languages) and the [files we ignore](https://docs.korbit.ai/configuration/ignoring-files).

</details>





> Check out our [docs](https://docs.korbit.ai) on how you can make Korbit work best for you and your team.
>
> Loving Korbit!? Share us on [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://www.korbit.ai&text=Korbit%20AI-powered%20Code%20Reviews%20are%20awesome...) [Reddit](https://www.reddit.com/submit?url=https://www.korbit.ai&title=Korbit%20AI-powered%20Code%20Reviews%20are%20awesome...) and [X](https://x.com/intent/tweet?text=Korbit%20AI-powered%20Code%20Reviews%20are%20awesome...%0A%0Ahttps%3A%2F%2Fwww.korbit.ai%0A%0A@KorbitAi)


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T19:00:25Z
user: login: Antonio-RiveroMartnez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T14:09:50Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:12:24Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:20:25Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:21:14Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:22:29Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:22:42Z
user: login: Antonio-RiveroMartnez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T14:27:25Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:41:24Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:33:29Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:34:00Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:37:08Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:38:31Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:41:32Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:42:28Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:27:28Z
user: login: Antonio-RiveroMartnez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T08:23:03Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:27:48Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:32:35Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:32:45Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:32:54Z
user: login: Antonio-RiveroMartnez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:56:50Z
user: login: Antonio-RiveroMartnez
body: LGTM, I have seen the functionality and the comments were addressed
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:58:14Z
user: login: betodealmeida
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T14:26:57Z
user: login: betodealmeida
body: Looks great! Left a minor correction of a typo.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T15:16:55Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:29:53Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:30:59Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:31:31Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:32:51Z
user: login: amaannawab923
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:33:19Z

Review comments:
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+/* eslint-disable import/no-extraneous-dependencies */
+import { styled } from '@superset-ui/core';
+import { Select } from 'antd';
+import { SearchOption } from '../../types';
+
+const StyledSelect = styled(Select)`
+  width: 120px;
+  margin-right: 8px;
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx
user: login: korbit-ai[bot]
body: ### Hard-coded style values <sub>![category Readability](https://img.shields.io/badge/Readability-0284c7)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
Hard-coded numeric values in the styled component without clear meaning or explanation.

###### Why this matters
Magic numbers make the code less maintainable and harder to understand the purpose of specific values.

###### Suggested change ∙ *Feature Preview*
```typescript
const SELECT_WIDTH = 120;
const SELECT_MARGIN = 8;

const StyledSelect = styled(Select)`
  width: ${SELECT_WIDTH}px;
  margin-right: ${SELECT_MARGIN}px;
`;
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/404c79b1-9ee4-4e82-b9be-567ca1decf07/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/404c79b1-9ee4-4e82-b9be-567ca1decf07?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/404c79b1-9ee4-4e82-b9be-567ca1decf07?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/404c79b1-9ee4-4e82-b9be-567ca1decf07?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/404c79b1-9ee4-4e82-b9be-567ca1decf07)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:0ad272ca-db93-437b-aa20-e52b2eb59f15 -->


[](0ad272ca-db93-437b-aa20-e52b2eb59f15)
created_at: 2025-05-05T19:00:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+/* eslint-disable import/no-extraneous-dependencies */
+import { styled } from '@superset-ui/core';
+import { Select } from 'antd';
+import { SearchOption } from '../../types';
+
+const StyledSelect = styled(Select)`
+  width: 120px;
+  margin-right: 8px;
+`;
+
+interface SearchSelectDropdownProps {
+  value?: string;
+  onChange: (searchCol: string) => void;
+  searchOptions: SearchOption[];
+}
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx
user: login: korbit-ai[bot]
body: ### Missing Interface Property Documentation <sub>![category Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The interface lacks documentation explaining the purpose of its properties and their constraints.

###### Why this matters
Without clear property descriptions, other developers may misuse the component or need to investigate the implementation to understand the expected values and behaviors.

###### Suggested change ∙ *Feature Preview*
```typescript
interface SearchSelectDropdownProps {
  /** The currently selected search column value */
  value?: string;
  /** Callback triggered when a new search column is selected */
  onChange: (searchCol: string) => void;
  /** Available search column options to populate the dropdown */
  searchOptions: SearchOption[];
}
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/85b419a7-5e76-4925-8bad-1bbbf50e5cab/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/85b419a7-5e76-4925-8bad-1bbbf50e5cab?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/85b419a7-5e76-4925-8bad-1bbbf50e5cab?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/85b419a7-5e76-4925-8bad-1bbbf50e5cab?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/85b419a7-5e76-4925-8bad-1bbbf50e5cab)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:617f046b-dc7c-43f3-bb9b-d4c7a6e05aaf -->


[](617f046b-dc7c-43f3-bb9b-d4c7a6e05aaf)
created_at: 2025-05-05T19:00:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+/* eslint-disable import/no-extraneous-dependencies */
+import { styled } from '@superset-ui/core';
+import { Select } from 'antd';
+import { SearchOption } from '../../types';
+
+const StyledSelect = styled(Select)`
+  width: 120px;
+  margin-right: 8px;
+`;
+
+interface SearchSelectDropdownProps {
+  value?: string;
+  onChange: (searchCol: string) => void;
+  searchOptions: SearchOption[];
+}
+
+function SearchSelectDropdown({
+  value,
+  onChange,
+  searchOptions,
+}: SearchSelectDropdownProps) {
+  return (
+    <StyledSelect
+      value={value || searchOptions?.[0]?.value}
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx
user: login: korbit-ai[bot]
body: ### Unsafe Fallback Value Logic <sub>![category Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The fallback value logic may fail if searchOptions is an empty array, leading to a potential undefined value being assigned.

###### Why this matters
If searchOptions is empty and no value is provided, the component will attempt to use undefined as a value for the Select component, which could cause unexpected behavior or errors.

###### Suggested change ∙ *Feature Preview*
Add a safer fallback value check:
```typescript
value={value || (searchOptions?.[0]?.value ?? '')}
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/5eb1b1bc-eafd-4b5c-9f51-a24e1748da37/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/5eb1b1bc-eafd-4b5c-9f51-a24e1748da37?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/5eb1b1bc-eafd-4b5c-9f51-a24e1748da37?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/5eb1b1bc-eafd-4b5c-9f51-a24e1748da37?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/5eb1b1bc-eafd-4b5c-9f51-a24e1748da37)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:8ed47c1e-12ff-4272-a4fc-b7bda99f6207 -->


[](8ed47c1e-12ff-4272-a4fc-b7bda99f6207)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+/* eslint-disable import/no-extraneous-dependencies */
+import { styled } from '@superset-ui/core';
+import { Select } from 'antd';
+import { SearchOption } from '../../types';
+
+const StyledSelect = styled(Select)`
+  width: 120px;
+  margin-right: 8px;
+`;
+
+interface SearchSelectDropdownProps {
+  value?: string;
+  onChange: (searchCol: string) => void;
+  searchOptions: SearchOption[];
+}
+
+function SearchSelectDropdown({
+  value,
+  onChange,
+  searchOptions,
+}: SearchSelectDropdownProps) {
+  return (
+    <StyledSelect
+      value={value || searchOptions?.[0]?.value}
+      options={searchOptions}
+      onChange={(value: string) => {
+        onChange(value);
+      }}
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/SearchSelectDropdown.tsx
user: login: korbit-ai[bot]
body: ### Unnecessarily verbose event handler <sub>![category Readability](https://img.shields.io/badge/Readability-0284c7)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The onChange handler is unnecessarily verbose with a wrapper arrow function that only passes the value through.

###### Why this matters
Adding unnecessary layers of function wrapping makes the code more difficult to read at a glance and adds cognitive overhead.

###### Suggested change ∙ *Feature Preview*
```typescript
onChange={onChange}
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f095dcc3-7020-4d7d-b97e-f9719d924ac5/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f095dcc3-7020-4d7d-b97e-f9719d924ac5?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f095dcc3-7020-4d7d-b97e-f9719d924ac5?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f095dcc3-7020-4d7d-b97e-f9719d924ac5?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/f095dcc3-7020-4d7d-b97e-f9719d924ac5)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:ba5796d2-7a55-4918-89ab-b3d106703933 -->


[](ba5796d2-7a55-4918-89ab-b3d106703933)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -109,6 +109,8 @@
     "JWT_ACCESS_CSRF_COOKIE_NAME",
     "SQLLAB_QUERY_RESULT_TIMEOUT",
     "SYNC_DB_PERMISSIONS_IN_ASYNC_MODE",
+    "TABLE_VIZ_MAX_ROW_CLIENT",
+    "TABLE_VIZ_MAX_ROW_SERVER",
path: superset/views/base.py
user: login: korbit-ai[bot]
body: ### Ambiguous constant names <sub>![category Readability](https://img.shields.io/badge/Readability-0284c7)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The newly added constants have ambiguous names that don't clearly convey their purpose - whether they represent a limit, threshold, or other value.

###### Why this matters
Developers will need to look elsewhere in the codebase to understand what these constants control, reducing code readability and maintainability.

###### Suggested change ∙ *Feature Preview*
Rename to more descriptive names like:
```python
"TABLE_VIZ_CLIENT_SIDE_ROW_LIMIT",
"TABLE_VIZ_SERVER_SIDE_ROW_LIMIT",
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/7ee0cc10-0f5f-4b8b-8ad1-20e7e722855b/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/7ee0cc10-0f5f-4b8b-8ad1-20e7e722855b?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/7ee0cc10-0f5f-4b8b-8ad1-20e7e722855b?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/7ee0cc10-0f5f-4b8b-8ad1-20e7e722855b?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/7ee0cc10-0f5f-4b8b-8ad1-20e7e722855b)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:9164ddf8-9ad5-4269-80b8-cdc27daff3c3 -->


[](9164ddf8-9ad5-4269-80b8-cdc27daff3c3)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -16,34 +16,55 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { memo, ComponentType, ChangeEventHandler } from 'react';
+import {
+  memo,
+  ComponentType,
+  ChangeEventHandler,
+  useRef,
+  useEffect,
+} from 'react';
 import { Row, FilterValue } from 'react-table';
 import useAsyncState from '../utils/useAsyncState';
 
 export interface SearchInputProps {
   count: number;
   value: string;
   onChange: ChangeEventHandler<HTMLInputElement>;
+  onBlur?: () => void;
+  inputRef?: React.RefObject<HTMLInputElement>;
 }
 
+const isSearchFocused = new Map();
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
user: login: korbit-ai[bot]
body: ### Global State Map Memory Leak <sub>![category Performance](https://img.shields.io/badge/Performance-4f46e5)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
Using a global Map to track focus state across multiple instances can lead to memory leaks and state persistence issues

###### Why this matters
When multiple table instances are created and destroyed, the focus states in the Map will persist, potentially causing incorrect focus behavior and memory leaks as the Map retains references to destroyed components

###### Suggested change ∙ *Feature Preview*
Move the focus state into component state using useState or into a React context if sharing between components is needed:
```typescript
function GlobalFilter<D extends object>({ ... }) {
  const [isSearchFocused, setIsSearchFocused] = useState(false);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const target = e.target as HTMLInputElement;
    e.preventDefault();
    setIsSearchFocused(true);
    setValue(target.value);
  };

  const handleBlur = () => {
    setIsSearchFocused(false);
  };
  
  useEffect(() => {
    if (
      serverPagination &&
      isSearchFocused &&
      document.activeElement !== inputRef.current
    ) {
      inputRef.current?.focus();
    }
  }, [value]);
}
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/dcf8c8fe-e2d1-4163-b221-851ca94458cc/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/dcf8c8fe-e2d1-4163-b221-851ca94458cc?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/dcf8c8fe-e2d1-4163-b221-851ca94458cc?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/dcf8c8fe-e2d1-4163-b221-851ca94458cc?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/dcf8c8fe-e2d1-4163-b221-851ca94458cc)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:62d6fd32-9f9f-42db-8322-01722025e799 -->


[](62d6fd32-9f9f-42db-8322-01722025e799)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -66,17 +92,37 @@
     200,
   );
 
+  // Only preserve focus if search is focused for this instance
+  useEffect(() => {
+    if (
+      serverPagination &&
+      isSearchFocused.get(id) &&
+      document.activeElement !== inputRef.current
+    ) {
+      inputRef.current?.focus();
+    }
+  }, [value, id]);
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
user: login: korbit-ai[bot]
body: ### Inefficient Focus Effect Dependencies <sub>![category Performance](https://img.shields.io/badge/Performance-4f46e5)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The useEffect hook runs on every value change, potentially causing unnecessary focus checks and DOM manipulations.

###### Why this matters
Frequent re-renders and DOM operations can impact performance, especially with rapid value changes during typing.

###### Suggested change ∙ *Feature Preview*
Optimize the effect dependencies to only run when necessary:
```typescript
useEffect(() => {
  if (serverPagination && isSearchFocused && document.activeElement !== inputRef.current) {
    inputRef.current?.focus();
  }
}, [serverPagination, isSearchFocused]);
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b5279421-9afe-447a-bfec-26922e56a880/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b5279421-9afe-447a-bfec-26922e56a880?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b5279421-9afe-447a-bfec-26922e56a880?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b5279421-9afe-447a-bfec-26922e56a880?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/b5279421-9afe-447a-bfec-26922e56a880)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:4d8fbec2-b6d8-4279-99d1-39d8dbbc5576 -->


[](4d8fbec2-b6d8-4279-99d1-39d8dbbc5576)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -66,17 +92,37 @@
     200,
   );
 
+  // Only preserve focus if search is focused for this instance
+  useEffect(() => {
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
user: login: korbit-ai[bot]
body: ### Incomplete useEffect documentation <sub>![category Documentation](https://img.shields.io/badge/Documentation-7c3aed)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The useEffect comment doesn't explain why focus preservation is needed.

###### Why this matters
Without understanding the rationale, developers might remove this seemingly unnecessary focus management code.

###### Suggested change ∙ *Feature Preview*
// Preserve focus during server-side filtering to maintain a better user experience
useEffect(() => {


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/3f5b9090-40d5-4350-955a-47aa26c88960/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/3f5b9090-40d5-4350-955a-47aa26c88960?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/3f5b9090-40d5-4350-955a-47aa26c88960?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/3f5b9090-40d5-4350-955a-47aa26c88960?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/3f5b9090-40d5-4350-955a-47aa26c88960)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:42dd487b-0bff-4195-a7f4-46a5a48c0f1e -->


[](42dd487b-0bff-4195-a7f4-46a5a48c0f1e)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -30,3 +30,11 @@ export const updateExternalFormData = (
       pageSize,
     },
   });
+
+export const updateTableOwnState = (
+  setDataMask: SetDataMaskHook = () => {},
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts
user: login: korbit-ai[bot]
body: ### Unclear default function implementation <sub>![category Readability](https://img.shields.io/badge/Readability-0284c7)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
The default empty function implementation obscures the purpose of the setDataMask parameter and its expected behavior.

###### Why this matters
While providing defaults can be helpful, in this case it makes it less clear that this is a required callback for updating the data mask state.

###### Suggested change ∙ *Feature Preview*
Remove the default implementation and make the parameter required:
```typescript
export const updateTableOwnState = (
  setDataMask: SetDataMaskHook,
  modifiedOwnState: TableOwnState,
)
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/9685ffa4-8a01-4f9c-92e7-15a58a9b434d/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/9685ffa4-8a01-4f9c-92e7-15a58a9b434d?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/9685ffa4-8a01-4f9c-92e7-15a58a9b434d?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/9685ffa4-8a01-4f9c-92e7-15a58a9b434d?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/9685ffa4-8a01-4f9c-92e7-15a58a9b434d)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:2f0e5a6f-85b7-4ae9-89e4-e15cc15b8277 -->


[](2f0e5a6f-85b7-4ae9-89e4-e15cc15b8277)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -30,3 +30,11 @@
       pageSize,
     },
   });
+
+export const updateTableOwnState = (
+  setDataMask: SetDataMaskHook = () => {},
+  modifiedOwnState: object,
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/utils/externalAPIs.ts
user: login: korbit-ai[bot]
body: ### Overly permissive object type for table state <sub>![category Functionality](https://img.shields.io/badge/Functionality-0284c7)</sub>

<details>
  <summary>Tell me more</summary>

###### What is the issue?
Using the generic 'object' type is too permissive and could allow invalid state updates to pass through type checking.

###### Why this matters
Without proper type constraints, the function might accept invalid state properties that could cause runtime errors or unexpected behavior in the table component.

###### Suggested change ∙ *Feature Preview*
Define a specific interface for the table state and use it to type the parameter:
```typescript
interface TableOwnState {
  currentPage?: number;
  pageSize?: number;
  sortColumn?: string;
  sortOrder?: 'asc' | 'desc';
  searchText?: string;
}

export const updateTableOwnState = (
  setDataMask: SetDataMaskHook = () => {},
  modifiedOwnState: Partial<TableOwnState>,
) =>
  setDataMask({
    ownState: modifiedOwnState,
  });
```


###### Provide feedback to improve future suggestions
[![Nice Catch](https://img.shields.io/badge/👍%20Nice%20Catch-71BC78)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/55601b5f-a270-49de-93cd-f2dd20bf9742/upvote) [![Incorrect](https://img.shields.io/badge/👎%20Incorrect-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/55601b5f-a270-49de-93cd-f2dd20bf9742?what_not_true=true)  [![Not in Scope](https://img.shields.io/badge/👎%20Out%20of%20PR%20scope-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/55601b5f-a270-49de-93cd-f2dd20bf9742?what_out_of_scope=true) [![Not in coding standard](https://img.shields.io/badge/👎%20Not%20in%20our%20standards-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/55601b5f-a270-49de-93cd-f2dd20bf9742?what_not_in_standard=true) [![Other](https://img.shields.io/badge/👎%20Other-white)](https://app.korbit.ai/feedback/aa91ff46-6083-4491-9416-b83dd1994b51/55601b5f-a270-49de-93cd-f2dd20bf9742)
</details>

<sub>

💬 Looking for more details? Reply to this comment to chat with Korbit.
</sub>

<!--- korbi internal id:51455ccf-29a9-4d8a-aa9a-4bbddf8dcd2c -->


[](51455ccf-29a9-4d8a-aa9a-4bbddf8dcd2c)
created_at: 2025-05-05T19:00:25Z
author_association: NONE
diff_hunk: @@ -969,6 +969,13 @@ class D3TimeFormat(TypedDict, total=False):
 # Maximum number of rows returned for any analytical database query
 SQL_MAX_ROW = 100000
 
+# Maximum number of rows that can be returned for table without Server pagination
+TABLE_VIZ_MAX_ROW_CLIENT = 100000
path: superset/config.py
user: login: Antonio-RiveroMartnez
body: I know the use case rn is for tables, but should the config be more abstract? i.e, `SQL_MAX_ROW_CLIENT` and `SQL_MAX_ROW_SERVER` or even just use the same `SQL_MAX_ROW ` as above (which might be confusing to keep 3 MAX configs) and include just one `SQL_MAX_ROW_SERVER`?
created_at: 2025-05-06T13:54:28Z
author_association: MEMBER
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: Antonio-RiveroMartnez
body: Why not just using the ILIKE? and why adding the extra leading space?
created_at: 2025-05-06T13:59:00Z
author_association: MEMBER
diff_hunk: @@ -1764,6 +1765,31 @@ def apply_max_row_limit(
     return max_limit
 
 
+def apply_max_row_limit_table(
+    limit: int,
+    form_data: dict[str, Any] | None = None,
+) -> int:
+    """
+    Override row limit for table viz type based on server pagination setting
+
+    :param limit: requested row limit
+    :param max_limit: Maximum allowed row limit for table viz
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: This is not an param of the method
created_at: 2025-05-06T14:00:13Z
author_association: MEMBER
diff_hunk: @@ -1764,6 +1765,31 @@ def apply_max_row_limit(
     return max_limit
 
 
+def apply_max_row_limit_table(
+    limit: int,
+    form_data: dict[str, Any] | None = None,
+) -> int:
+    """
+    Override row limit for table viz type based on server pagination setting
+
+    :param limit: requested row limit
+    :param max_limit: Maximum allowed row limit for table viz
+    :param form_data: Form data containing server_pagination setting
+    :return: Capped row limit
+    """
+    max_limit = None
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: nit: If you initialize `max_limit = current_app.config["TABLE_VIZ_MAX_ROW_CLIENT"]` then you won't need the else below 
created_at: 2025-05-06T14:03:09Z
author_association: MEMBER
diff_hunk: @@ -105,6 +113,22 @@ def _process_row_limit(
         )
         return apply_max_row_limit(row_limit or default_row_limit)
 
+    def _process_row_limit_table(
path: superset/common/query_object_factory.py
user: login: Antonio-RiveroMartnez
body: Why not just extending the existing _process_row_limit so it can receive the optional form_data and pass it along instead of creating this specific method for tables?
created_at: 2025-05-06T14:06:39Z
author_association: MEMBER
diff_hunk: @@ -1764,6 +1765,31 @@ def apply_max_row_limit(
     return max_limit
 
 
+def apply_max_row_limit_table(
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: Same as with `_process_row_limit`, why not extending the current one instead of creating a table specific to handle optional param?
created_at: 2025-05-06T14:09:38Z
author_association: MEMBER
diff_hunk: @@ -969,6 +969,13 @@ class D3TimeFormat(TypedDict, total=False):
 # Maximum number of rows returned for any analytical database query
 SQL_MAX_ROW = 100000
 
+# Maximum number of rows that can be returned for table without Server pagination
+TABLE_VIZ_MAX_ROW_CLIENT = 100000
path: superset/config.py
user: login: amaannawab923
body: Makes sense , since both of them have same values... We can eliminate one
created_at: 2025-05-06T14:12:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: amaannawab923
body: I did try using ilike but ilike fails in scenarios with strings have more than one substring 

For example:
In case of fullname which is usually used by most orgs 
If someone wants to search by first name & last name they expect the same result 
Let's take the name 
"John Anderson"

You want to type both john & anderson & expect the same result 
then  ilike(" And"%) or ilike("Joh%")
will return the same full name and infact it goes to an unlimited number of substrings

For example: 
Searching for James Carter Wilson

 ilike(" Car"%) 
 ilike(" Wil%")
 will still return James Cater wilson since the rest of the substrings also have a leading space


These test cases will fail when just using ilike
Below is a screenshot of it working in action 
Where it returns both "Ahmad Andra" & "Andra Alsup" where andra is last name of the first while last name of the second respectively 

<img width="1718" alt="Screenshot 2025-05-06 at 12 17 30 AM" src="https://github.com/user-attachments/assets/4cacad90-6a76-496d-bf25-1095efe2368f" />


created_at: 2025-05-06T14:20:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1764,6 +1765,31 @@ def apply_max_row_limit(
     return max_limit
 
 
+def apply_max_row_limit_table(
+    limit: int,
+    form_data: dict[str, Any] | None = None,
+) -> int:
+    """
+    Override row limit for table viz type based on server pagination setting
+
+    :param limit: requested row limit
+    :param max_limit: Maximum allowed row limit for table viz
path: superset/utils/core.py
user: login: amaannawab923
body: Thanks for pointing out , forgot to remove it while refactoring the method
created_at: 2025-05-06T14:21:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +113,22 @@ def _process_row_limit(
         )
         return apply_max_row_limit(row_limit or default_row_limit)
 
+    def _process_row_limit_table(
path: superset/common/query_object_factory.py
user: login: amaannawab923
body: Makes sense , i was just too afraid to break some existing unit test cases that might be using _process_row_limit ... Let me do that
created_at: 2025-05-06T14:22:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1764,6 +1765,31 @@ def apply_max_row_limit(
     return max_limit
 
 
+def apply_max_row_limit_table(
+    limit: int,
+    form_data: dict[str, Any] | None = None,
+) -> int:
+    """
+    Override row limit for table viz type based on server pagination setting
+
+    :param limit: requested row limit
+    :param max_limit: Maximum allowed row limit for table viz
+    :param form_data: Form data containing server_pagination setting
+    :return: Capped row limit
+    """
+    max_limit = None
path: superset/utils/core.py
user: login: amaannawab923
body: Right
created_at: 2025-05-06T14:22:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: Antonio-RiveroMartnez
body: I see, in that case, extensions like https://www.postgresql.org/docs/current/pgtrgm.html should be preferred which would have to leverage some new indexes for those fields you want to search. That's probably out of the scope of this PR itself, but using hardcoded leading spaces IMO is not something we should do, not sure what others think tho, so I'll leave that open
created_at: 2025-05-06T14:27:25Z
author_association: MEMBER
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: amaannawab923
body: Checked out pgtrgm ... but it wont suffice our case because we want a prefix match for strings & substrings & pgtrgm uses fuzzy search that means 
Wilson carter will be returned for Wil & car
but it will also be returned for strings like "il" , "art" , "er" 

Which isnt expected & will return wrong results 
Will try to look for better approach though ... This approach i have implemented for a pvt fork which worked out greatly hence migrated it here
created_at: 2025-05-06T14:41:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: betodealmeida
body: There also the question of UX here. I would expect that searching for `son` would match `Anderson`, since that's the behavior we have in other input fields in Superset:

<img width="490" alt="Screenshot 2025-05-06 at 11 32 37 AM" src="https://github.com/user-attachments/assets/38747ff4-682e-4a61-9b61-9c8039746325" />

So instead of adding a new operator here I think it's better to just use `ILIKE`.

created_at: 2025-05-06T15:33:29Z
author_association: MEMBER
diff_hunk: @@ -33,6 +33,7 @@ const BINARY_OPERATORS = [
   'NOT LIKE',
   'REGEX',
   'TEMPORAL_RANGE',
+  'TEXT_SEARCH',
path: superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts
user: login: betodealmeida
body: Let's just use `ILIKE` for consistency with how search works across the app, see my first comment.
created_at: 2025-05-06T15:34:00Z
author_association: MEMBER
diff_hunk: @@ -1764,6 +1765,31 @@ def apply_max_row_limit(
     return max_limit
 
 
+def apply_max_row_limit_table(
+    limit: int,
+    form_data: dict[str, Any] | None = None,
+) -> int:
+    """
+    Override row limit for table viz type based on server pagination setting
+
+    :param limit: requested row limit
+    :param max_limit: Maximum allowed row limit for table viz
+    :param form_data: Form data containing server_pagination setting
+    :return: Capped row limit
+    """
+    max_limit = None
path: superset/utils/core.py
user: login: betodealmeida
body: Or just do a single statement:

```python
max_limit = (
    current_app.config["TABLE_VIZ_MAX_ROW_SERVER"]
    if form_data and form_data.get("server_pagination")
    else current_app.config["TABLE_VIZ_MAX_ROW_CLIENT"]
)
```
created_at: 2025-05-06T15:37:07Z
author_association: MEMBER
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: betodealmeida
body: @Antonio-RiveroMartnez keep in mind we're fitering the analytics database here, so most of them won't have indexes that can be easily leveraged.
created_at: 2025-05-06T15:38:31Z
author_association: MEMBER
diff_hunk: @@ -105,6 +113,22 @@ def _process_row_limit(
         )
         return apply_max_row_limit(row_limit or default_row_limit)
 
+    def _process_row_limit_table(
path: superset/common/query_object_factory.py
user: login: betodealmeida
body: Also, instead of passing the whole `form_data` we can just pass `server_pagination`
created_at: 2025-05-06T15:41:32Z
author_association: MEMBER
diff_hunk: @@ -70,7 +70,7 @@ def create(  # pylint: disable=too-many-arguments
                 datasource_model_instance,
                 form_data,
                 self._query_object_factory.create(
-                    result_type, datasource=datasource, **query_obj
+                    result_type, datasource=datasource, **query_obj, form_data=form_data
path: superset/common/query_context_factory.py
user: login: betodealmeida
body: ```suggestion
                    result_type, datasource=datasource, form_data=form_data, **query_obj
```
created_at: 2025-05-06T15:42:27Z
author_association: MEMBER
diff_hunk: @@ -1888,12 +1888,26 @@ def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-ma
                     elif op in {
                         utils.FilterOperator.ILIKE.value,
                         utils.FilterOperator.LIKE.value,
+                        utils.FilterOperator.TEXT_SEARCH.value,
                     }:
                         if target_generic_type != GenericDataType.STRING:
                             sqla_col = sa.cast(sqla_col, sa.String)
 
                         if op == utils.FilterOperator.LIKE.value:
                             where_clause_and.append(sqla_col.like(eq))
+                        elif op == utils.FilterOperator.TEXT_SEARCH.value:
+                            # Convert to string and handle None case
+                            search_value = str(eq) if eq is not None else ""
+                            if (
+                                search_value
+                            ):  # Only add clause if search value is not empty
+                                pattern = search_value.lower()
+                                where_clause_and.append(
+                                    sa.or_(
+                                        sqla_col.ilike(f"{pattern}%"),
+                                        sqla_col.ilike(f"% {pattern}%"),
path: superset/models/helpers.py
user: login: amaannawab923
body: @betodealmeida  i agree , that we should maintain consistency from a ux point of view but filtering results on every possible substring of the search value absolutely breaks the table in case of analytics tables &  gives random results.. 
Here i am searching for all real_name that includes ad 

<img width="1138" alt="Screenshot 2025-05-06 at 9 53 25 PM" src="https://github.com/user-attachments/assets/8d2f1f79-a7ba-41af-92d5-da3f8c17aaed" />

& the search operation begins to feel random  since Searches are supposed to be based on prefixes
An example would be linkedin search below since it has the search by name case 

<img width="564" alt="Screenshot 2025-05-06 at 10 34 05 PM" src="https://github.com/user-attachments/assets/ed43fc2c-e742-4b88-9c52-fa3e948d4b8c" />



created_at: 2025-05-06T16:27:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: Does this mean this method is now ignoring a custom `max_limit` and instead using the config max limits? because that's it not what it was doing before, meaning, this could be a braking change
created_at: 2025-05-08T08:20:29Z
author_association: MEMBER
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool = False,
 ) -> int:
     """
-    Override row limit if max global limit is defined
+    Override row limit based on server pagination setting
 
     :param limit: requested row limit
-    :param max_limit: Maximum allowed row limit
+    :param server_pagination: whether server-side pagination is enabled
     :return: Capped row limit
 
-    >>> apply_max_row_limit(100000, 10)
-    10
-    >>> apply_max_row_limit(10, 100000)
-    10
-    >>> apply_max_row_limit(0, 10000)
-    10000
-    """
-    if max_limit is None:
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: We used to use the config values only if no max_limit was provided, why the change to always using the configs now?
created_at: 2025-05-08T08:21:03Z
author_association: MEMBER
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool = False,
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: could this allow None too? so we're not forced to provide it
created_at: 2025-05-08T08:21:56Z
author_association: MEMBER
diff_hunk: @@ -96,14 +102,27 @@ def _process_extras(
         return extras
 
     def _process_row_limit(
-        self, row_limit: int | None, result_type: ChartDataResultType
+        self,
+        row_limit: int | None,
+        result_type: ChartDataResultType,
+        server_pagination: bool = False,
path: superset/common/query_object_factory.py
user: login: Antonio-RiveroMartnez
body: Could this be None too?
created_at: 2025-05-08T08:22:25Z
author_association: MEMBER
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
path: superset/utils/core.py
user: login: amaannawab923
body: The have checked the function usage, The function is not using this custom max_limit anywhere in the code ... across all of its uage .... Hence i ended up removing it since it was not being used ....
created_at: 2025-05-08T08:27:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool = False,
 ) -> int:
     """
-    Override row limit if max global limit is defined
+    Override row limit based on server pagination setting
 
     :param limit: requested row limit
-    :param max_limit: Maximum allowed row limit
+    :param server_pagination: whether server-side pagination is enabled
     :return: Capped row limit
 
-    >>> apply_max_row_limit(100000, 10)
-    10
-    >>> apply_max_row_limit(10, 100000)
-    10
-    >>> apply_max_row_limit(0, 10000)
-    10000
-    """
-    if max_limit is None:
path: superset/utils/core.py
user: login: amaannawab923
body: Because the function is no where using the max_limit from anywhere... I can add it again ... i only removed it because the argument wasn't getting used

Usages 

superset/viz.py
row_limit = apply_max_row_limit(row_limit). # No second arg for max_limit

superset/datasource/api.py
row_limit = apply_max_row_limit(app.config["FILTER_SELECT_ROW_LIMIT"]) # No second arg for max_limit

superset/sqllab/sqllab_execution_context.py
limit = apply_max_row_limit(query_params.get("queryLimit") or 0) # No second arg for max_limit

created_at: 2025-05-08T08:32:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool = False,
path: superset/utils/core.py
user: login: amaannawab923
body: Sure
created_at: 2025-05-08T08:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,14 +102,27 @@ def _process_extras(
         return extras
 
     def _process_row_limit(
-        self, row_limit: int | None, result_type: ChartDataResultType
+        self,
+        row_limit: int | None,
+        result_type: ChartDataResultType,
+        server_pagination: bool = False,
path: superset/common/query_object_factory.py
user: login: amaannawab923
body: Sure
created_at: 2025-05-08T08:32:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool = False,
 ) -> int:
     """
-    Override row limit if max global limit is defined
+    Override row limit based on server pagination setting
 
     :param limit: requested row limit
-    :param max_limit: Maximum allowed row limit
+    :param server_pagination: whether server-side pagination is enabled
     :return: Capped row limit
 
-    >>> apply_max_row_limit(100000, 10)
-    10
-    >>> apply_max_row_limit(10, 100000)
-    10
-    >>> apply_max_row_limit(0, 10000)
-    10000
-    """
-    if max_limit is None:
path: superset/utils/core.py
user: login: Antonio-RiveroMartnez
body: Oh ok ok, yeah, I'm OK with deleting if not being used
created_at: 2025-05-08T13:56:50Z
author_association: MEMBER
diff_hunk: @@ -1741,24 +1741,29 @@ def parse_boolean_string(bool_str: str | None) -> bool:
 
 def apply_max_row_limit(
     limit: int,
-    max_limit: int | None = None,
+    server_pagination: bool = False,
 ) -> int:
     """
-    Override row limit if max global limit is defined
+    Override row limit based on server pagination setting
 
     :param limit: requested row limit
-    :param max_limit: Maximum allowed row limit
+    :param server_pagination: whether server-side pagination is enabled
     :return: Capped row limit
 
-    >>> apply_max_row_limit(100000, 10)
-    10
-    >>> apply_max_row_limit(10, 100000)
-    10
-    >>> apply_max_row_limit(0, 10000)
-    10000
-    """
-    if max_limit is None:
path: superset/utils/core.py
user: login: betodealmeida
body: Nice, if it's not being used let's get rid of it.
created_at: 2025-05-08T14:26:57Z
author_association: MEMBER
diff_hunk: @@ -969,6 +969,10 @@ class D3TimeFormat(TypedDict, total=False):
 # Maximum number of rows returned for any analytical database query
 SQL_MAX_ROW = 100000
 
+# Maximum number of rows for any query without Server Pagination in Table Viz type
path: superset/config.py
user: login: betodealmeida
body: ```suggestion
# Maximum number of rows for any query with Server Pagination in Table Viz type
```
created_at: 2025-05-08T15:13:46Z
author_association: MEMBER
diff_hunk: @@ -96,14 +102,27 @@ def _process_extras(
         return extras
 
     def _process_row_limit(
-        self, row_limit: int | None, result_type: ChartDataResultType
+        self,
+        row_limit: int | None,
+        result_type: ChartDataResultType,
+        server_pagination: bool | None = None,
path: superset/common/query_object_factory.py
user: login: betodealmeida
body: ❤️
created_at: 2025-05-08T15:14:29Z
author_association: MEMBER
diff_hunk: @@ -127,7 +168,8 @@ export default typedMemo(function DataTable<D extends object>({
     ...initialState_,
     // zero length means all pages, the `usePagination` plugin does not
     // understand pageSize = 0
-    sortBy: sortByRef.current,
+    // sortBy: sortByRef.current,
+    sortBy: serverPagination ? sortByFromParent : sortByRef.current,
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
user: login: amaannawab923
body: sortByFromParent is the sortBy state from the persistent filterState of the table chart which we need on initial Render as the table loses all of its variables on complete re render... So getting the sortBy state from redux helps maintain consistency
created_at: 2025-05-09T14:29:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -198,10 +246,46 @@ export default typedMemo(function DataTable<D extends object>({
       globalFilter: defaultGlobalFilter,
       sortTypes,
       autoResetSortBy: !isEqual(columnNames, previousColumnNames),
+      manualSortBy: !!serverPagination,
       ...moreUseTableOptions,
     },
     ...tableHooks,
   );
+
+  const handleSearchChange = useCallback(
+    (query: string) => {
+      if (manualSearch && onSearchChange) {
+        onSearchChange(query);
+      } else {
+        setGlobalFilter(query);
+      }
+    },
+    [manualSearch, onSearchChange, setGlobalFilter],
+  );
+
+  // updating the sort by to the own State of table viz
+  useEffect(() => {
+    const serverSortBy = serverPaginationData?.sortBy || [];
+
+    if (serverPagination && !isEqual(sortBy, serverSortBy)) {
+      if (Array.isArray(sortBy) && sortBy.length > 0) {
+        const [sortByItem] = sortBy;
+        const matchingColumn = columns.find(col => col?.id === sortByItem?.id);
+
+        if (matchingColumn && 'columnKey' in matchingColumn) {
+          const sortByWithColumnKey: SortByItem = {
+            ...sortByItem,
+            key: (matchingColumn as { columnKey: string }).columnKey,
+          };
+
+          handleSortByChange([sortByWithColumnKey]);
+        }
+      } else {
+        handleSortByChange([]);
+      }
+    }
+  }, [sortBy]);
+
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
user: login: amaannawab923
body: useEffect for updating the sortBy state in redux of the table viz & triggering query to get the latest data based on sortBy
created_at: 2025-05-09T14:30:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -381,16 +466,31 @@ export default typedMemo(function DataTable<D extends object>({
               ) : null}
             </div>
             {searchInput ? (
-              <div className="col-sm-6">
+              <StyledSpace className="col-sm-6">
+                {serverPagination && (
+                  <div className="search-select-container">
+                    <span className="search-by-label">Search by: </span>
+                    <SearchSelectDropdown
+                      searchOptions={searchOptions}
+                      value={serverPaginationData?.searchColumn || ''}
+                      onChange={onSearchColChange}
+                    />
+                  </div>
+                )}
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
user: login: amaannawab923
body: The search select dropdown component to have dynamic search column
created_at: 2025-05-09T14:31:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,17 +92,37 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
     200,
   );
 
+  // Preserve focus during server-side filtering to maintain a better user experience
+  useEffect(() => {
+    if (
+      serverPagination &&
+      isSearchFocused.get(id) &&
+      document.activeElement !== inputRef.current
+    ) {
+      inputRef.current?.focus();
+    }
+  }, [value, serverPagination]);
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
user: login: amaannawab923
body: There was a problem that i faced with losing the focus in the search field ... as the user types in search field it triggers a complete re render & the search field focus is lost... Hence we are preserving the focus with the help of a map 
created_at: 2025-05-09T14:32:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,17 +92,37 @@ export default (memo as <T>(fn: T) => T)(function GlobalFilter<
     200,
   );
 
+  // Preserve focus during server-side filtering to maintain a better user experience
+  useEffect(() => {
+    if (
+      serverPagination &&
+      isSearchFocused.get(id) &&
+      document.activeElement !== inputRef.current
+    ) {
+      inputRef.current?.focus();
+    }
+  }, [value, serverPagination]);
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const target = e.target as HTMLInputElement;
+    e.preventDefault();
+    isSearchFocused.set(id, true);
+    setValue(target.value);
+  };
+
+  const handleBlur = () => {
+    isSearchFocused.set(id, false);
+  };
path: superset-frontend/plugins/plugin-chart-table/src/DataTable/components/GlobalFilter.tsx
user: login: amaannawab923
body: Updating the map to inform that the field has lost focus ... and there is no need to focus it on initial render
created_at: 2025-05-09T14:33:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: korbit-ai[bot]
created_at: 2025-05-05T05:32:16Z
body: Based on your review schedule, I'll hold off on reviewing this PR until it's marked as ready for review. If you'd like me to take a look now, comment `/korbit-review`.

> Your admin can change your review schedule in the [Korbit Console](https://app.korbit.ai/settings)



[](b50da9b0-6a3d-4800-80bf-c99b77580192)
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-05T05:44:25Z
body: ## [Codecov](https://app.codecov.io/gh/apache/superset/pull/33357?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.18%. Comparing base [(`76d897e`)](https://app.codecov.io/gh/apache/superset/commit/76d897eaa2f9e137102bc194c2e3109c29d0348f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to head [(`7d05b61`)](https://app.codecov.io/gh/apache/superset/commit/7d05b61e7f98ab6053ee0bfaaf726b048585b00a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
> Report is 1851 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master   #33357       +/-   ##
===========================================
+ Coverage   60.48%   83.18%   +22.69%     
===========================================
  Files        1931      553     -1378     
  Lines       76236    39967    -36269     
  Branches     8568        0     -8568     
===========================================
- Hits        46114    33247    -12867     
+ Misses      28017     6720    -21297     
+ Partials     2105        0     -2105     
```

| [Flag](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [hive](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `48.25% <40.00%> (-0.91%)` | :arrow_down: |
| [javascript](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [mysql](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `75.38% <100.00%> (?)` | |
| [postgres](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `75.44% <100.00%> (?)` | |
| [presto](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `52.71% <100.00%> (-1.10%)` | :arrow_down: |
| [python](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `83.18% <100.00%> (+19.68%)` | :arrow_up: |
| [sqlite](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `74.94% <100.00%> (?)` | |
| [unit](https://app.codecov.io/gh/apache/superset/pull/33357/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `61.30% <60.00%> (+3.67%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/superset/pull/33357?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: amaannawab923
created_at: 2025-05-06T13:41:36Z
body: Hey @Antonio-RiveroMartnez  , Can you please review this pr whenever available
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-06T13:47:26Z
body: @Antonio-RiveroMartnez Processing your ephemeral environment request [here](https://github.com/apache/superset/actions/runs/14861335391). Action: **up**. More information on [how to use or configure ephemeral environments](https://superset.apache.org/docs/contributing/howtos/#github-ephemeral-environments)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-06T13:56:05Z
body: @Antonio-RiveroMartnez Ephemeral environment spinning up at http://54.190.200.46:8080. Credentials are 'admin'/'admin'. Please allow several minutes for bootstrapping and startup.
author_association: CONTRIBUTOR
user: login: betodealmeida
created_at: 2025-05-06T14:31:05Z
body: Taking a look now, but a few initial comments:

1. In general, any PR that changes the UI/UX significantly should go [through a discussion](https://github.com/apache/superset/issues/5602). For uncontroversial changes the process is usually quick, since you can request "lazy consensus" and people don't have to vote. I think we're OK in this one, just letting you know for the future.
2. Splitting the PR into 4 would've been better, even if it takes more time/work upfront. It makes reviews quicker and it's simpler to revert features if something goes wrong.
3. I'm a bit concerned about the performance of the `LIKE` search, and wondering if we should have optional ways of optimizing it, but let me look at the PR in details before we start worrying about this. (For example, will this do a full table scan on BigQuery?)
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-07T20:49:40Z
body: @geido Processing your ephemeral environment request [here](https://github.com/apache/superset/actions/runs/14892995603). Action: **up**. More information on [how to use or configure ephemeral environments](https://superset.apache.org/docs/contributing/howtos/#github-ephemeral-environments)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-07T21:02:11Z
body: @geido Ephemeral environment spinning up at http://34.221.88.197:8080. Credentials are 'admin'/'admin'. Please allow several minutes for bootstrapping and startup.
author_association: CONTRIBUTOR
user: login: amaannawab923
created_at: 2025-05-08T05:51:56Z
body: Resolved all the comments @betodealmeida  @Antonio-RiveroMartnez 
Removed the Text search operator & refactored the process row limit function as per comments & removed redundant implementation

Please re review whenever available, Thanks !
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T13:57:55Z
body: @Antonio-RiveroMartnez Processing your ephemeral environment request [here](https://github.com/apache/superset/actions/runs/14908287774). Action: **up**. More information on [how to use or configure ephemeral environments](https://superset.apache.org/docs/contributing/howtos/#github-ephemeral-environments)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T14:08:38Z
body: @Antonio-RiveroMartnez Ephemeral environment spinning up at http://54.187.252.12:8080. Credentials are 'admin'/'admin'. Please allow several minutes for bootstrapping and startup.
author_association: CONTRIBUTOR
user: login: rusackas
created_at: 2025-05-09T17:27:28Z
body: Merging!
author_association: MEMBER