diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.ts b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts index 960e0943840..d13205fa183 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.ts +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts @@ -34,6 +34,12 @@ import { import { SupersetClient, isFeatureEnabled } from '@superset-ui/core'; import { ADD_TOAST } from 'src/components/MessageToasts/actions'; import { EMPTY_STATE_QE_ID } from 'src/SqlLab/hooks/useQueryEditor'; +import { api } from 'src/hooks/apiResources/queryApi'; +import { + queryHistoryApi, + type QueryResult, +} from 'src/hooks/apiResources/queries'; +import { defaultStore } from 'spec/helpers/testing-library'; import { ToastType } from '../../components/MessageToasts/types'; const isFeatureEnabledMock = isFeatureEnabled as unknown as jest.Mock; @@ -1108,6 +1114,80 @@ describe('async actions', () => { }); }); + test('removeQuery removes the deleted query from the editorQueries cache', async () => { + isFeatureEnabledMock.mockReturnValue(false); + const editorId = 'editor1'; + const queryToRemove = { + ...query, + id: 'queryToRemove', + sqlEditorId: editorId, + }; + + await defaultStore.dispatch( + queryHistoryApi.util.upsertQueryData('editorQueries', { editorId }, { + count: 2, + ids: [], + result: [{ id: 'queryToRemove' }, { id: 'keep' }], + } as unknown as QueryResult), + ); + + await defaultStore.dispatch( + actions.removeQuery(queryToRemove) as unknown as AnyAction, + ); + + const { data } = queryHistoryApi.endpoints.editorQueries.select({ + editorId, + })(defaultStore.getState()); + + expect(data).toEqual({ + count: 1, + ids: [], + result: [{ id: 'keep' }], + }); + + defaultStore.dispatch(api.util.resetApiState()); + }); + + test('removeQuery removes duplicate cache entries for the deleted query', async () => { + isFeatureEnabledMock.mockReturnValue(false); + const editorId = 'editor1'; + const queryToRemove = { + ...query, + id: 'queryToRemove', + sqlEditorId: editorId, + }; + + // The infinite-scroll merge can append the same query twice when + // offsets shift between page fetches; deletion must drop every copy. + await defaultStore.dispatch( + queryHistoryApi.util.upsertQueryData('editorQueries', { editorId }, { + count: 3, + ids: [], + result: [ + { id: 'queryToRemove' }, + { id: 'keep' }, + { id: 'queryToRemove' }, + ], + } as unknown as QueryResult), + ); + + await defaultStore.dispatch( + actions.removeQuery(queryToRemove) as unknown as AnyAction, + ); + + const { data } = queryHistoryApi.endpoints.editorQueries.select({ + editorId, + })(defaultStore.getState()); + + expect(data).toEqual({ + count: 2, + ids: [], + result: [{ id: 'keep' }], + }); + + defaultStore.dispatch(api.util.resetApiState()); + }); + // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('queryEditorSetDb', () => { test('updates the tab state in the backend', () => { diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.ts b/superset-frontend/src/SqlLab/actions/sqlLab.ts index a64329b06e7..9c0eb4c38de 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.ts +++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts @@ -48,6 +48,8 @@ import getInitialState from '../reducers/getInitialState'; import { rehydratePersistedState } from '../utils/reduxStateToLocalStorageHelper'; import { PREVIEW_QUERY_LIMIT } from '../constants'; import { EMPTY_STATE_QE_ID } from '../hooks/useQueryEditor'; +import { queryHistoryApi } from '../../hooks/apiResources/queries'; +import type { AppDispatch as RootDispatch } from '../../views/store'; // Type definitions for SqlLab actions export interface Query { @@ -985,7 +987,7 @@ export function removeAllOtherQueryEditors( } export function removeQuery(query: Query): SqlLabThunkAction> { - return function (dispatch: AppDispatch) { + return function (dispatch: RootDispatch) { const queryEditorId = query.sqlEditorId ?? query.id; const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) ? SupersetClient.delete({ @@ -996,7 +998,26 @@ export function removeQuery(query: Query): SqlLabThunkAction> { : Promise.resolve(); return sync - .then(() => dispatch({ type: REMOVE_QUERY, query })) + .then(() => { + dispatch({ type: REMOVE_QUERY, query }); + dispatch( + queryHistoryApi.util.updateQueryData( + 'editorQueries', + { editorId: queryEditorId }, + draft => { + // The infinite-scroll merge can leave duplicate entries for the + // same id (offset shifts between page fetches), so drop them all. + const remaining = draft.result.filter( + ({ id }) => id !== query.id, + ); + if (remaining.length !== draft.result.length) { + draft.result = remaining; + draft.count = Math.max(0, draft.count - 1); + } + }, + ), + ); + }) .catch(() => dispatch( addDangerToast( diff --git a/superset-frontend/src/hooks/apiResources/queries.ts b/superset-frontend/src/hooks/apiResources/queries.ts index 2fc9a439156..282edfeca65 100644 --- a/superset-frontend/src/hooks/apiResources/queries.ts +++ b/superset-frontend/src/hooks/apiResources/queries.ts @@ -107,7 +107,7 @@ export const mapQueryResponse = ( user: query.user, }); -const queryHistoryApi = api.injectEndpoints({ +export const queryHistoryApi = api.injectEndpoints({ endpoints: builder => ({ editorQueries: builder.query({ providesTags: ['EditorQueries'],