/** * 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 { QueryState } from '@superset-ui/core'; import sqlLabReducer from 'src/SqlLab/reducers/sqlLab'; import * as actions from 'src/SqlLab/actions/sqlLab'; import { table, initialState as mockState } from '../fixtures'; const initialState = mockState.sqlLab; // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('sqlLabReducer', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('Query editors actions', () => { let newState; let defaultQueryEditor; let qe; beforeEach(() => { newState = { ...initialState }; defaultQueryEditor = newState.queryEditors[0]; const action = { type: actions.ADD_QUERY_EDITOR, queryEditor: { ...initialState.queryEditors[0], id: 'abcd' }, }; newState = sqlLabReducer(newState, action); qe = newState.queryEditors.find(e => e.id === 'abcd'); }); test('should add a query editor', () => { expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length + 1, ); }); test('should merge the current unsaved changes when adding a query editor', () => { const expectedTitle = 'new updated title'; const updateAction = { type: actions.QUERY_EDITOR_SET_TITLE, queryEditor: initialState.queryEditors[0], name: expectedTitle, }; newState = sqlLabReducer(newState, updateAction); const addAction = { type: actions.ADD_QUERY_EDITOR, queryEditor: { ...initialState.queryEditors[0], id: 'efgh' }, }; newState = sqlLabReducer(newState, addAction); expect(newState.queryEditors[0].name).toEqual(expectedTitle); expect( newState.queryEditors[newState.queryEditors.length - 1].id, ).toEqual('efgh'); }); test('should remove a query editor', () => { expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length + 1, ); const action = { type: actions.REMOVE_QUERY_EDITOR, queryEditor: qe, }; newState = sqlLabReducer(newState, action); expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length, ); }); test('should select the latest query editor when tabHistory is empty', () => { const currentQE = newState.queryEditors[0]; newState = { ...initialState, tabHistory: [initialState.queryEditors[0]], }; const action = { type: actions.REMOVE_QUERY_EDITOR, queryEditor: currentQE, }; newState = sqlLabReducer(newState, action); expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length - 1, ); expect(newState.queryEditors.map(qe => qe.id)).not.toContainEqual( currentQE.id, ); expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]); }); test('should remove a query editor including unsaved changes', () => { expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length + 1, ); let action = { type: actions.QUERY_EDITOR_SETDB, queryEditor: qe, dbId: 123, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.dbId).toEqual(action.dbId); action = { type: actions.REMOVE_QUERY_EDITOR, queryEditor: qe, }; newState = sqlLabReducer(newState, action); expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length, ); expect(newState.unsavedQueryEditor.dbId).toBeUndefined(); expect(newState.unsavedQueryEditor.id).toBeUndefined(); }); test('should set q query editor active', () => { const expectedTitle = 'new updated title'; const addQueryEditorAction = { type: actions.ADD_QUERY_EDITOR, queryEditor: { ...initialState.queryEditors[0], id: 'abcd' }, }; newState = sqlLabReducer(newState, addQueryEditorAction); const updateAction = { type: actions.QUERY_EDITOR_SET_TITLE, queryEditor: initialState.queryEditors[1], name: expectedTitle, }; newState = sqlLabReducer(newState, updateAction); const setActiveQueryEditorAction = { type: actions.SET_ACTIVE_QUERY_EDITOR, queryEditor: defaultQueryEditor, }; newState = sqlLabReducer(newState, setActiveQueryEditorAction); expect(newState.tabHistory[newState.tabHistory.length - 1]).toBe( defaultQueryEditor.id, ); expect(newState.queryEditors[1].name).toEqual(expectedTitle); }); test('should not fail while setting DB', () => { const dbId = 9; const action = { type: actions.QUERY_EDITOR_SETDB, queryEditor: qe, dbId, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.dbId).toBe(dbId); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should not fail while setting schema', () => { const schema = 'foo'; const action = { type: actions.QUERY_EDITOR_SET_SCHEMA, queryEditor: qe, schema, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.schema).toBe(schema); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should not fail while setting autorun', () => { const action = { type: actions.QUERY_EDITOR_SET_AUTORUN, queryEditor: qe, }; newState = sqlLabReducer(newState, { ...action, autorun: false }); expect(newState.unsavedQueryEditor.autorun).toBe(false); expect(newState.unsavedQueryEditor.id).toBe(qe.id); newState = sqlLabReducer(newState, { ...action, autorun: true }); expect(newState.unsavedQueryEditor.autorun).toBe(true); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should not fail while setting title', () => { const title = 'Untitled Query 1'; const action = { type: actions.QUERY_EDITOR_SET_TITLE, queryEditor: qe, name: title, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.name).toBe(title); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should not fail while setting Sql', () => { const sql = 'SELECT nothing from dev_null'; const action = { type: actions.QUERY_EDITOR_SET_SQL, queryEditor: qe, sql, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.sql).toBe(sql); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should not fail while setting queryLimit', () => { const queryLimit = 101; const action = { type: actions.QUERY_EDITOR_SET_QUERY_LIMIT, queryEditor: qe, queryLimit, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should set selectedText', () => { const selectedText = 'TEST'; const action = { type: actions.QUERY_EDITOR_SET_SELECTED_TEXT, queryEditor: qe, sql: selectedText, }; expect(qe.selectedText).toBeFalsy(); newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText); expect(newState.unsavedQueryEditor.id).toBe(qe.id); }); test('should not wiped out unsaved changes while delayed async call intercepted', () => { const expectedSql = 'Updated SQL WORKING IN PROGRESS--'; const action = { type: actions.QUERY_EDITOR_SET_SQL, queryEditor: qe, sql: expectedSql, }; newState = sqlLabReducer(newState, action); expect(newState.unsavedQueryEditor.sql).toBe(expectedSql); const interceptedAction = { type: actions.QUERY_EDITOR_PERSIST_HEIGHT, queryEditor: newState.queryEditors[0], northPercent: 46, southPercent: 54, }; newState = sqlLabReducer(newState, interceptedAction); expect(newState.unsavedQueryEditor.sql).toBe(expectedSql); expect(newState.queryEditors[0].northPercent).toBe( interceptedAction.northPercent, ); }); test('should migrate query editor by new query editor id', () => { const { length } = newState.queryEditors; const index = newState.queryEditors.findIndex(({ id }) => id === qe.id); const newQueryEditor = { ...qe, tabViewId: 'updatedNewId', schema: 'updatedSchema', inLocalStorage: false, }; const action = { type: actions.MIGRATE_QUERY_EDITOR, oldQueryEditor: qe, newQueryEditor, }; newState = sqlLabReducer(newState, action); expect(newState.queryEditors[index].id).toEqual(qe.id); expect(newState.queryEditors[index].tabViewId).toEqual('updatedNewId'); expect(newState.queryEditors[index]).toEqual(newQueryEditor); const removeAction = { type: actions.REMOVE_QUERY_EDITOR, queryEditor: newQueryEditor, }; newState = sqlLabReducer(newState, removeAction); expect(newState.queryEditors).toHaveLength(length - 1); expect(Object.keys(newState.destroyedQueryEditors)).toContain( newQueryEditor.tabViewId, ); }); test('should clear the destroyed query editors', () => { const expectedQEId = '1233289'; const action = { type: actions.CLEAR_DESTROYED_QUERY_EDITOR, queryEditorId: expectedQEId, }; newState = sqlLabReducer( { ...newState, destroyedQueryEditors: { [expectedQEId]: Date.now(), }, }, action, ); expect(newState.destroyedQueryEditors).toEqual({}); }); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('Tables', () => { let newState; let newTable; beforeEach(() => { newTable = { ...table }; const action = { type: actions.MERGE_TABLE, table: newTable, }; newState = sqlLabReducer(initialState, action); newTable = newState.tables[0]; }); test('should add a table', () => { // Testing that beforeEach actually added the table expect(newState.tables).toHaveLength(1); expect(newState.tables[0].expanded).toBe(true); }); test('should merge the table attributes', () => { // Merging the extra attribute newTable.extra = true; const action = { type: actions.MERGE_TABLE, table: newTable, }; newState = sqlLabReducer(newState, action); expect(newState.tables).toHaveLength(1); expect(newState.tables[0].extra).toBe(true); }); test('should overwrite table ID be ignored when the existing table is already initialized', () => { const action = { type: actions.MERGE_TABLE, table: newTable, }; newState = sqlLabReducer(newState, action); expect(newState.tables).toHaveLength(1); // Merging the initialized remote id const remoteId = 1; const syncAction = { type: actions.MERGE_TABLE, table: { ...newTable, id: remoteId, initialized: true, }, }; newState = sqlLabReducer(newState, syncAction); expect(newState.tables).toHaveLength(1); expect(newState.tables[0].initialized).toBe(true); expect(newState.tables[0].id).toBe(remoteId); const overwriteAction = { type: actions.MERGE_TABLE, table: { id: 'rnd_new_id', ...newTable, }, }; newState = sqlLabReducer(newState, overwriteAction); expect(newState.tables).toHaveLength(1); expect(newState.tables[0].id).toBe(remoteId); }); test('should expand and collapse a table', () => { const collapseTableAction = { type: actions.COLLAPSE_TABLE, table: newTable, }; newState = sqlLabReducer(newState, collapseTableAction); expect(newState.tables[0].expanded).toBe(false); const expandTableAction = { type: actions.EXPAND_TABLE, table: newTable, }; newState = sqlLabReducer(newState, expandTableAction); expect(newState.tables[0].expanded).toBe(true); }); test('should remove a table', () => { const action = { type: actions.REMOVE_TABLES, tables: [newTable], }; newState = sqlLabReducer(newState, action); expect(newState.tables).toHaveLength(0); }); test('should set activeSouthPaneTab when adding expanded table', () => { const expandedTable = { ...table, id: 'expanded_table_id', name: 'expanded_table', expanded: true, }; const action = { type: actions.MERGE_TABLE, table: expandedTable, }; newState = sqlLabReducer(initialState, action); expect(newState.tables).toHaveLength(1); expect(newState.activeSouthPaneTab).toBe(expandedTable.id); }); test('should not set activeSouthPaneTab when adding collapsed table', () => { const collapsedTable = { ...table, id: 'collapsed_table_id', name: 'collapsed_table', expanded: false, }; const action = { type: actions.MERGE_TABLE, table: collapsedTable, }; newState = sqlLabReducer(initialState, action); expect(newState.tables).toHaveLength(1); expect(newState.activeSouthPaneTab).toBe(initialState.activeSouthPaneTab); }); test('should set activeSouthPaneTab when merging existing table with expanded=true', () => { // First add a table with expanded=false const collapsedTable = { ...table, id: 'existing_table_id', name: 'existing_table', expanded: false, }; const addAction = { type: actions.MERGE_TABLE, table: collapsedTable, }; newState = sqlLabReducer(initialState, addAction); const previousActiveSouthPaneTab = newState.activeSouthPaneTab; // Now merge the same table with expanded=true const expandedTable = { ...collapsedTable, expanded: true, }; const mergeAction = { type: actions.MERGE_TABLE, table: expandedTable, }; newState = sqlLabReducer(newState, mergeAction); expect(newState.tables).toHaveLength(1); expect(newState.activeSouthPaneTab).toBe(expandedTable.id); expect(newState.activeSouthPaneTab).not.toBe(previousActiveSouthPaneTab); }); test('should not set activeSouthPaneTab when merging existing table with expanded=false', () => { // First add a table with expanded=true const expandedTable = { ...table, id: 'existing_table_id_2', name: 'existing_table_2', expanded: true, }; const addAction = { type: actions.MERGE_TABLE, table: expandedTable, }; newState = sqlLabReducer(initialState, addAction); expect(newState.activeSouthPaneTab).toBe(expandedTable.id); // Now merge the same table with expanded=false const collapsedTable = { ...expandedTable, expanded: false, }; const mergeAction = { type: actions.MERGE_TABLE, table: collapsedTable, }; newState = sqlLabReducer(newState, mergeAction); expect(newState.tables).toHaveLength(1); expect(newState.activeSouthPaneTab).toBe(expandedTable.id); }); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('Run Query', () => { const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439'; const CHANGED_ON_TIMESTAMP = 1687765985439; let newState; let query; beforeEach(() => { newState = { ...initialState }; query = { id: 'abcd', progress: 0, changed_on: DENORMALIZED_CHANGED_ON, startDttm: CHANGED_ON_TIMESTAMP, state: 'running', cached: false, sqlEditorId: 'dfsadfs', }; }); test('should start a query', () => { const action = { type: actions.START_QUERY, query: { id: 'abcd', progress: 0, changed_on: DENORMALIZED_CHANGED_ON, startDttm: CHANGED_ON_TIMESTAMP, state: 'running', cached: false, sqlEditorId: 'dfsadfs', }, }; newState = sqlLabReducer(newState, action); expect(Object.keys(newState.queries)).toHaveLength(1); }); test('should stop the query', () => { const startQueryAction = { type: actions.START_QUERY, query, }; newState = sqlLabReducer(newState, startQueryAction); const stopQueryAction = { type: actions.STOP_QUERY, query, }; newState = sqlLabReducer(newState, stopQueryAction); const q = newState.queries[Object.keys(newState.queries)[0]]; expect(q.state).toBe('stopped'); }); test('should remove a query', () => { const startQueryAction = { type: actions.START_QUERY, query, }; newState = sqlLabReducer(newState, startQueryAction); const removeQueryAction = { type: actions.REMOVE_QUERY, query, }; newState = sqlLabReducer(newState, removeQueryAction); expect(Object.keys(newState.queries)).toHaveLength(0); }); test('should refresh queries when polling returns new results', () => { const startDttmInStr = '1693433503447.166992'; const endDttmInStr = '1693433503500.23132'; newState = sqlLabReducer( { ...newState, queries: { abcd: {} }, }, actions.refreshQueries({ abcd: { ...query, startDttm: startDttmInStr, endDttm: endDttmInStr, }, }), ); expect(newState.queries.abcd.changed_on).toBe(DENORMALIZED_CHANGED_ON); expect(newState.queries.abcd.startDttm).toBe(Number(startDttmInStr)); expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr)); expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP); }); test('should skip refreshing queries when polling contains existing results', () => { const completedQuery = { ...query, extra: { columns: [], progress: null, }, }; newState = sqlLabReducer( { ...newState, queries: { abcd: query, def: completedQuery }, }, actions.refreshQueries({ abcd: { ...query, }, def: { ...completedQuery, extra: { columns: [], progress: null, }, }, }), ); expect(newState.queries.abcd).toBe(query); expect(newState.queries.def).toBe(completedQuery); }); test('should refresh queries when polling returns empty', () => { newState = sqlLabReducer(newState, actions.refreshQueries({})); }); }); // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('CLEAR_INACTIVE_QUERIES', () => { let newState; let query; beforeEach(() => { query = { id: 'abcd', changed_on: Date.now(), startDttm: Date.now(), state: QueryState.Fetching, progress: 100, resultsKey: 'fa3dccc4-c549-4fbf-93c8-b4fb5a6fb8b7', cached: false, }; }); test('updates queries that have already been completed', () => { newState = sqlLabReducer( { ...newState, queries: { abcd: { ...query, results: { query_id: 1234, status: QueryState.Success, data: [], }, }, }, }, actions.clearInactiveQueries(Date.now()), ); expect(newState.queries.abcd.state).toBe(QueryState.Success); }); }); });