Skip to content

fix(sqllab): key overwrite-dataset options by id, not table name - #42620

Open
anthonyhungnguyen wants to merge 6 commits into
apache:masterfrom
anthonyhungnguyen:fix/save-dataset-overwrite-duplicate-options
Open

fix(sqllab): key overwrite-dataset options by id, not table name#42620
anthonyhungnguyen wants to merge 6 commits into
apache:masterfrom
anthonyhungnguyen:fix/save-dataset-overwrite-duplicate-options

Conversation

@anthonyhungnguyen

@anthonyhungnguyen anthonyhungnguyen commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

The "Overwrite existing" dropdown in SQL Lab's Save or Overwrite Dataset modal builds its options with value: r.table_name:

data: response.json.result.map(
  (r: { table_name: string; id: number; editors: Subject[] }) => ({
    value: r.table_name,
    label: r.table_name,
    datasetId: r.id,
    editors: r.editors,
  }),
),

Table names are not unique — the same table name can exist in several schemas, and a user can be an editor on datasets for all of them (Airflow metadata tables such as task_instance saved from a staging and a prod schema are a common case). When that happens:

  • duplicate values collide on one Select key, so the listbox intermittently renders the same row several times (reported as "sometimes I get 8 identical rows")
  • the labels are identical, so the datasets are indistinguishable to the user
  • the resulting selection is ambiguous, so Overwrite can target the wrong dataset

This PR keys the options by the dataset id, which is unique, and renders a schema-qualified label so same-named datasets can be told apart:

value: r.id,
label: r.schema ? `${r.schema}.${r.table_name}` : r.table_name,

schema is already part of the dataset API's list_columns, so no backend change is needed. filterAutocompleteOption now matches on the label rather than the value, since the value is no longer a string, and DatasetOptionAutocomplete.value is typed number. The overwrite path itself is unaffected — it already used datasetToOverwrite.datasetId.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Before: with three editable datasets named task_instance in different schemas, the dropdown shows repeated, identical task_instance rows and there is no way to tell which one Overwrite will hit.

After: one row per dataset, each labelled staging.task_instance, prod.task_instance, etc., and Overwrite targets the row that was clicked.

TESTING INSTRUCTIONS

cd superset-frontend
npx jest src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx

20/20 pass. Added a regression test — distinguishes datasets that share a table name and overwrites the selected one — that stubs two editable datasets sharing the table name task_instance across a staging and a prod schema, asserts each renders exactly once under its schema-qualified label, that typing the prod. prefix filters the staging row out (the autocomplete filter now matches on the label), and that selecting the prod row issues the PUT against that dataset's id. The test fails on master (both labels resolve to two rendered nodes) and passes with this change.

The three existing test call sites that looked up 'coolest table 0' were updated to the new qualified label. prettier --check and oxlint are clean on the touched files; tsc --noEmit reports no new errors.

Manual check: open SQL Lab → run a query → SaveSave or Overwrite DatasetOverwrite existing, with an account that can edit two datasets sharing a table name in different schemas.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
  • Introduces new feature or API
  • Removes existing feature or API

The "Overwrite existing" dropdown in the Save or Overwrite Dataset modal
built its options with `value: r.table_name`. Table names are not unique
across schemas, so a user who can edit several datasets that share a table
name (e.g. an `task_instance` saved from a staging and a prod schema) hit
three problems:

- duplicate `value`s collide on a single Select key, so the listbox
  intermittently rendered the same row several times
- identical labels made the datasets indistinguishable
- the resulting selection was ambiguous, so Overwrite could target the
  wrong dataset

Key the options by the dataset id, which is unique, and render a
schema-qualified label so same-named datasets can be told apart. The
autocomplete filter now matches on the label, since the value is no longer
a string.

Adds a regression test covering two same-named datasets in different
schemas: each renders once, and overwriting the selected row PUTs to that
dataset's id.
@dosubot dosubot Bot added change:frontend Requires changing the frontend sqllab Namespace | Anything related to the SQL Lab labels Jul 30, 2026
@bito-code-review

bito-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #ab9164

Actionable Suggestions - 0
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx - 1
Review Details
  • Files reviewed - 3 · Commit Range: 8c10eae..8c10eae
    • superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
    • superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
    • superset-frontend/src/SqlLab/types.ts
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.45%. Comparing base (6e1c2fb) to head (01fdda5).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #42620   +/-   ##
=======================================
  Coverage   65.44%   65.45%           
=======================================
  Files        2810     2810           
  Lines      159335   159336    +1     
  Branches    36362    36363    +1     
=======================================
+ Hits       104282   104287    +5     
+ Misses      53011    53006    -5     
- Partials     2042     2043    +1     
Flag Coverage Δ
javascript 71.54% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Types a schema prefix into the existing-dataset combobox so the regression
test also exercises filterAutocompleteOption, which now matches on the
rendered label rather than the option value.
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Jul 30, 2026

@sadpandajoe sadpandajoe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one autocomplete gap in the qualified-label path.

inputValue: string,
option: DatasetOverwriteOption,
) => option.value.toLowerCase().includes(inputValue.toLowerCase());
) => option.label.toLowerCase().includes(inputValue.toLowerCase());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the editable dataset list spans more than one page, typing the new schema prefix still sends prod. to the API as a table_name filter, so the async response replaces the options with an empty list even when prod.task_instance exists. Could the server query search schema/qualified labels too, or otherwise keep schema filtering client-side?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you're right, and the regression test I'd written was masking it: the SupersetClient.get stub ignored the request, so the search string never had to match anything server-side.

Fixed in c800f64. The search string is now split on the first dot and sent as two filters, schema and table_name (both are in the dataset API's search_columns), so prod.task_ reaches the server as schema ct 'prod' + table_name ct 'task_' instead of a table_name filter that can never match. filterAutocompleteOption mirrors the same split, so the local pass — which only exists to hide options left over from an earlier search — can't hide a row the API deliberately returned (e.g. prod. vs. schema production).

The test now pads the fixture past one API page and applies the rison filters the way the API does. I checked it's not vacuous: reverting just the server-side split makes it fail on exactly the scenario you described (the response comes back empty and the prod.task_instance row disappears).

@bito-code-review

Copy link
Copy Markdown
Contributor

The pull request addresses this issue by changing how dataset options are keyed and filtered in the SaveDatasetModal. Previously, options were keyed by table_name, which caused collisions when multiple datasets shared the same name. The changes update the value to use the unique id and update the label to include the schema (e.g., schema.table_name). Additionally, the filterAutocompleteOption function was updated to filter based on the label (which now includes the schema) rather than the value (the ID), ensuring that typing a schema prefix correctly narrows the list to the intended dataset.

superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx

// `id` is unique; `table_name` is not. Keying options by the table
          // name collapses same-named datasets onto a single Select key, which
          // renders duplicate rows and makes the overwrite target ambiguous.
          value: r.id,
          label: r.schema ? `${r.schema}.${r.table_name}` : r.table_name,

superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx

const filterAutocompleteOption = (
    inputValue: string,
    option: DatasetOverwriteOption,
  ) => option.label.toLowerCase().includes(inputValue.toLowerCase());

The overwrite options are labelled `schema.table_name`, but the search
string was sent to the API as a table_name filter only, so typing the
schema prefix matched nothing server-side. When the editable dataset
list spans more than one page the target row is never fetched, so a
client-side filter cannot recover it.

Split the search on the first dot and filter on `schema` and
`table_name` separately, and mirror that split in the local
filterOption so it cannot hide a row the API returned.
@bito-code-review

bito-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #983633

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 8c10eae..c800f64
    • superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
    • superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

// name collapses same-named datasets onto a single Select key, which
// renders duplicate rows and makes the overwrite target ambiguous.
value: r.id,
label: r.schema ? `${r.schema}.${r.table_name}` : r.table_name,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datasets are unique by database/catalog/schema/table, so editable datasets from different databases or catalogs can still render the same label here and leave the user unable to tell which one will be overwritten. Could the label and autocomplete search include the database/catalog identity as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in 9ddff12.

Options are now labelled with every part that is set: database.catalog.schema.table_name (e.g. analytics.reporting.prod.task_instance), which is the actual uniqueness key.

On the search: only the table part can go to the API. The qualifiers span three columns, and database is a relationship the list endpoint can't match on by name (search_columns exposes it, but not as a ct filter on database_name). So the request sends the trailing part of the search as the table_name filter and the qualifiers are narrowed client-side over the rows that came back. Parts are matched positionally-independently, so analytics.prod.sales still matches a dataset that also has a catalog, and a trailing . is treated as qualification rather than a table name.

That does mean the qualifier narrowing only sees the rows the table search returned — a limitation if more than a page of datasets share one table name, though that's far rarer than having more than a page of datasets overall, which was the case that broke before. This also replaces the server-side schema filter from the previous commit: it can't be kept, since a two-part search can just as easily be database.table as schema.table, and guessing wrong sends a filter that matches nothing.

Test covers three datasets sharing task_instance across two databases and two schemas, one with a catalog, with the fixture padded past one API page. I checked it's not vacuous — it fails if the database is dropped from the label, if the whole search string is sent as the table_name filter, or if the local filter is stubbed out.

Datasets are unique by database, catalog, schema and table name, so two
editable datasets could still render the same `schema.table_name` label
and leave the user unable to tell which one they were overwriting.

Label the options with every part that is set, and match the search
against those parts. Only the table part can be pushed to the API — the
qualifiers span three columns, one of which is a relationship the list
endpoint cannot match on by name — so the qualifiers are narrowed
client-side over the rows the table search returned.
Comment on lines 367 to 385
@@ -342,9 +385,19 @@ export const SaveDatasetModal = ({
endpoint: `/api/v1/dataset/?q=${queryParams}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The loader ignores the page and pageSize arguments required by AsyncSelect and never sends pagination parameters to the dataset API. When more than one page matches the table-name filter, subsequent scroll requests retrieve the same first page, so editable datasets beyond the first page cannot be selected. Accept the pagination arguments and include the corresponding page and page-size query parameters. [api mismatch]

Severity Level: Major ⚠️
- ❌ Large overwrite lists cannot expose later datasets.
- ❌ SQL Lab users cannot select some editable datasets.
- ⚠️ AsyncSelect pagination repeatedly fetches page one.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
**Line:** 367:385
**Comment:**
	*Api Mismatch: The loader ignores the `page` and `pageSize` arguments required by `AsyncSelect` and never sends pagination parameters to the dataset API. When more than one page matches the table-name filter, subsequent scroll requests retrieve the same first page, so editable datasets beyond the first page cannot be selected. Accept the pagination arguments and include the corresponding page and page-size query parameters.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@netlify

netlify Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 7e013dd
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a6cd3b6389fc500084df0b1
😎 Deploy Preview https://deploy-preview-42620--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

col: 'table_name',
opr: 'ct',
value: input,
value: tableSearch,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: When the input contains a separator, only the table-name suffix is sent to the API and qualifier filtering is deferred to the browser. The async select receives only the first API page, so if more than one page of editable datasets shares that table-name suffix, a matching database/schema dataset on a later page is never loaded and cannot be selected. Apply the qualifier filters server-side, or request and merge all relevant pages before client-side filtering. [logic error]

Severity Level: Major ⚠️
- ⚠️ Large table-name result sets hide editable datasets.
- ❌ Users cannot overwrite matching datasets beyond page one.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
**Line:** 366:366
**Comment:**
	*Logic Error: When the input contains a separator, only the table-name suffix is sent to the API and qualifier filtering is deferred to the browser. The async select receives only the first API page, so if more than one page of editable datasets shares that table-name suffix, a matching database/schema dataset on a later page is never loaded and cannot be selected. Apply the qualifier filters server-side, or request and merge all relevant pages before client-side filtering.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +477 to +479
return parseQualifiedSearch(inputValue.toLowerCase()).parts.every(part =>
label.includes(part),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The client-side filter treats every qualified-search component as an unordered substring, so a search such as foo.bar can match a dataset whose label contains bar.foo or where one component merely occurs inside another value. This can present unrelated datasets and make the selection ambiguous. Match the qualified components in their actual database/catalog/schema/table order, or use structured fields for filtering. [incorrect condition logic]

Severity Level: Major ⚠️
- ⚠️ Qualified searches can show unrelated datasets.
- ⚠️ SQL Lab overwrite selection remains ambiguous.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
**Line:** 477:479
**Comment:**
	*Incorrect Condition Logic: The client-side filter treats every qualified-search component as an unordered substring, so a search such as `foo.bar` can match a dataset whose label contains `bar.foo` or where one component merely occurs inside another value. This can present unrelated datasets and make the selection ambiguous. Match the qualified components in their actual database/catalog/schema/table order, or use structured fields for filtering.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

bito-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #42577f

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: c800f64..7e013dd
    • superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
    • superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:frontend Requires changing the frontend size/L sqllab Namespace | Anything related to the SQL Lab

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants