You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2025/09/11 21:26:12 UTC
(superset) branch 5.0-extensions updated (5c43b13c1e -> 84b81b814e)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a change to branch 5.0-extensions
in repository https://gitbox.apache.org/repos/asf/superset.git
from 5c43b13c1e fix: Change database event in core (#35071)
new 47485093b9 feat(sqllab extension): core query interface api (#35077)
new 84b81b814e fix: SQL Lab tab events (#35105)
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../packages/superset-core/src/api/core.ts | 67 +++-
.../packages/superset-core/src/api/sqlLab.ts | 153 ++++++++-
superset-frontend/src/SqlLab/actions/sqlLab.js | 8 +-
.../src/SqlLab/actions/sqlLab.test.js | 4 +
.../SqlLab/components/QueryAutoRefresh/index.tsx | 11 +-
superset-frontend/src/SqlLab/fixtures.ts | 3 +
.../src/SqlLab/reducers/getInitialState.ts | 4 +
superset-frontend/src/SqlLab/types.ts | 1 +
.../SqlLab/utils/reduxStateToLocalStorageHelper.ts | 1 +
superset-frontend/src/core/core.ts | 23 +-
superset-frontend/src/core/sqlLab.ts | 381 +++++++++++++++++++--
.../src/hooks/apiResources/sqlEditorTabs.test.ts | 1 +
12 files changed, 592 insertions(+), 65 deletions(-)
(superset) 01/02: feat(sqllab extension): core query interface api (#35077)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 5.0-extensions
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 47485093b9f26777dbe0c0911a145407bfaefa81
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Wed Sep 10 13:17:09 2025 -0700
feat(sqllab extension): core query interface api (#35077)
(cherry picked from commit 078c1701f44048b64d217f0e6f635f3e5e340518)
---
.../packages/superset-core/src/api/core.ts | 67 +++-
.../packages/superset-core/src/api/sqlLab.ts | 153 ++++++++-
superset-frontend/src/SqlLab/actions/sqlLab.js | 6 +-
.../SqlLab/components/QueryAutoRefresh/index.tsx | 11 +-
superset-frontend/src/core/core.ts | 23 +-
superset-frontend/src/core/sqlLab.ts | 346 +++++++++++++++++++--
6 files changed, 542 insertions(+), 64 deletions(-)
diff --git a/superset-frontend/packages/superset-core/src/api/core.ts b/superset-frontend/packages/superset-core/src/api/core.ts
index 2c7e4977dd..19bac3ae47 100644
--- a/superset-frontend/packages/superset-core/src/api/core.ts
+++ b/superset-frontend/packages/superset-core/src/api/core.ts
@@ -31,12 +31,32 @@ import { Contributions } from './contributions';
/**
* Represents a database column with its name and data type.
*/
-export declare interface Column {
- /** The name of the column */
+export type Column = {
+ /**
+ * Label of the column
+ */
name: string;
- /** The data type of the column (e.g., 'INTEGER', 'VARCHAR', 'TIMESTAMP') */
+
+ /**
+ * Column name defined
+ */
+ column_name: string;
+
+ /**
+ * The data type of the column (e.g., 'INTEGER', 'VARCHAR', 'TIMESTAMP')
+ */
type: string;
-}
+
+ /**
+ * Generic data type format
+ */
+ type_generic: GenericDataType;
+
+ /**
+ * True if the column is date format
+ */
+ is_dttm: boolean;
+};
/**
* Represents a database table with its name and column definitions.
@@ -76,6 +96,45 @@ export declare interface Database {
schemas: Schema[];
}
+// Keep in sync with superset/errors.py
+export type ErrorLevel = 'info' | 'warning' | 'error';
+
+/**
+ * Superset error object structure.
+ * Contains details about an error that occurred within Superset.
+ */
+export type SupersetError = {
+ /**
+ * Error types, see enum of SupersetErrorType in superset/errors.py
+ */
+ error_type: string;
+
+ /**
+ * Extra properties based on the error types
+ */
+ extra: Record<string, any>;
+
+ /**
+ * Level of the error type
+ */
+ level: ErrorLevel;
+
+ /**
+ * Detail description for the error
+ */
+ message: string;
+};
+
+/**
+ * Generic data types, see enum of the same name in superset/utils/core.py.
+ */
+export enum GenericDataType {
+ Numeric = 0,
+ String = 1,
+ Temporal = 2,
+ Boolean = 3,
+}
+
/**
* Represents a type which can release resources, such
* as event listening or a timer.
diff --git a/superset-frontend/packages/superset-core/src/api/sqlLab.ts b/superset-frontend/packages/superset-core/src/api/sqlLab.ts
index 997ec57809..a1abe66b83 100644
--- a/superset-frontend/packages/superset-core/src/api/sqlLab.ts
+++ b/superset-frontend/packages/superset-core/src/api/sqlLab.ts
@@ -29,7 +29,7 @@
* - Global APIs: Functions and events available across the entire SQL Lab interface
*/
-import { Event, Database } from './core';
+import { Event, Database, SupersetError, Column } from './core';
/**
* Represents an SQL editor instance within a SQL Lab tab.
@@ -111,6 +111,126 @@ export interface Tab {
panels: Panel[];
}
+export enum CTASMethod {
+ Table = 'TABLE',
+ View = 'VIEW',
+}
+
+export interface CTAS {
+ /**
+ * Create method for CTAS creation request.
+ */
+ method: CTASMethod;
+
+ /**
+ * Temporary table name for creation using a CTAS query.
+ */
+ tempTable: string | null;
+}
+
+export interface QueryContext {
+ /**
+ * Unique query ID on client side.
+ */
+ clientId: string;
+
+ /**
+ * Contains CTAS if the query requests table creation.
+ */
+ ctas: CTAS | null;
+
+ /**
+ * Requested row limit for the query.
+ */
+ requestedLimit: number | null;
+
+ /**
+ * True if the query execution result will be/was delivered asynchronously
+ */
+ runAsync?: boolean;
+
+ /**
+ * Start datetime for the query in a numerical timestamp
+ */
+ startDttm: number;
+
+ /**
+ * The tab instance associated with the request query
+ */
+ tab: Tab;
+
+ /**
+ * A key-value JSON associated with Jinja template variables
+ */
+ templateParameters: Record<string, any>;
+}
+
+export interface QueryErrorResultContext extends QueryContext {
+ /**
+ * Finished datetime for the query in a numerical timestamp
+ */
+ endDttm: number;
+
+ /**
+ * Error message returned from DB engine
+ */
+ errorMessage: string;
+
+ /**
+ * Error details in a SupersetError structure
+ */
+ errors: SupersetError[] | null;
+
+ /**
+ * Executed SQL after parsing Jinja templates.
+ */
+ executedSql: string | null;
+}
+
+export interface QueryResultContext extends QueryContext {
+ /**
+ * Actual number of rows returned by the query.
+ */
+ appliedLimit: number;
+
+ /**
+ * Major factor that is determining the row limit of the query results.
+ */
+ appliedLimitingFactor: string;
+
+ /**
+ * Finished datetime for the query in a numerical timestamp.
+ */
+ endDttm: number;
+
+ /**
+ * Executed SQL after parsing Jinja templates.
+ */
+ executedSql: string;
+
+ /**
+ * Remote query id stored in backend.
+ */
+ remoteId: number;
+
+ /**
+ * Query result data and metadata.
+ */
+ result: QueryResult;
+}
+
+export interface QueryResult {
+ /**
+ * Column metadata associated with the query result.
+ */
+ columns: Column[];
+
+ /**
+ * Query result data.
+ */
+ data: Record<string, any>[];
+}
+
/**
* Tab-scoped Events and Functions
*
@@ -240,30 +360,30 @@ export declare const onDidChangeTabTitle: Event<string>;
/**
* Event fired when a query starts running in the current tab.
- * Provides the editor state at the time of query execution.
+ * Provides the query request state at the time of query execution.
*
* @example
* ```typescript
- * onDidQueryRun.event((editor) => {
- * console.log('Query started on database:', editor.databaseId);
- * console.log('Query content:', editor.content);
+ * onDidQueryRun.event((query) => {
+ * console.log('Query started on database:', query.tab.editor.databaseId);
+ * console.log('Query content:', query.tab.editor.content);
* });
* ```
*/
-export declare const onDidQueryRun: Event<Editor>;
+export declare const onDidQueryRun: Event<QueryContext>;
/**
* Event fired when a running query is stopped in the current tab.
- * Provides the editor state when the query was stopped.
+ * Provides the query request state when the query was stopped.
*
* @example
* ```typescript
- * onDidQueryStop.event((editor) => {
- * console.log('Query stopped for database:', editor.databaseId);
+ * onDidQueryStop.event((query) => {
+ * console.log('Query stopped for database:', query.tab.editor.databaseId);
* });
* ```
*/
-export declare const onDidQueryStop: Event<Editor>;
+export declare const onDidQueryStop: Event<QueryContext>;
/**
* Event fired when a query fails in the current tab.
@@ -273,12 +393,12 @@ export declare const onDidQueryStop: Event<Editor>;
*
* @example
* ```typescript
- * onDidQueryFail.event((error) => {
- * console.error('Query failed:', error);
+ * onDidQueryFail.event((result) => {
+ * console.error('Query failed:', result.errorMessage);
* });
* ```
*/
-export declare const onDidQueryFail: Event<string>;
+export declare const onDidQueryFail: Event<QueryErrorResultContext>;
/**
* Event fired when a query succeeds in the current tab.
@@ -288,12 +408,13 @@ export declare const onDidQueryFail: Event<string>;
*
* @example
* ```typescript
- * onDidQuerySuccess.event((result) => {
- * console.log('Query succeeded:', result);
+ * onDidQuerySuccess.event((query) => {
+ * console.log('Query succeeded:', query.result.data);
+ * console.log('Query executed content:', query.executedSql);
* });
* ```
*/
-export declare const onDidQuerySuccess: Event<string>;
+export declare const onDidQuerySuccess: Event<QueryResultContext>;
/**
* Global Events and Functions
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 8b54319a56..fb94af58c0 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -273,10 +273,14 @@ export function logFailedQuery(query, errors) {
};
}
+export function createQueryFailedAction(query, msg, link, errors) {
+ return { type: QUERY_FAILED, query, msg, link, errors };
+}
+
export function queryFailed(query, msg, link, errors) {
return function (dispatch) {
dispatch(logFailedQuery(query, errors));
- dispatch({ type: QUERY_FAILED, query, msg, link, errors });
+ dispatch(createQueryFailedAction(query, msg, link, errors));
};
}
diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
index c7de1208b5..f941fb4587 100644
--- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
@@ -33,7 +33,7 @@ import useInterval from 'src/SqlLab/utils/useInterval';
import {
refreshQueries,
clearInactiveQueries,
- logFailedQuery,
+ queryFailed,
} from 'src/SqlLab/actions/sqlLab';
import type { DatabaseObject } from 'src/features/databases/types';
@@ -119,7 +119,14 @@ function QueryAutoRefresh({
!failedQueries.current.has(id) &&
state === QueryState.Failed
) {
- dispatch(logFailedQuery(query, query.extra?.errors));
+ dispatch(
+ queryFailed(
+ query,
+ query.errorMessage,
+ query.extra?.errors?.[0]?.extra?.link,
+ query.extra?.errors,
+ ),
+ );
failedQueries.current.set(id, true);
}
});
diff --git a/superset-frontend/src/core/core.ts b/superset-frontend/src/core/core.ts
index 34ba480248..14c87d06a0 100644
--- a/superset-frontend/src/core/core.ts
+++ b/superset-frontend/src/core/core.ts
@@ -16,34 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-import type {
- core as coreType,
- sqlLab as sqlLabType,
-} from '@apache-superset/core';
+import { core as coreType, sqlLab as sqlLabType } from '@apache-superset/core';
import { getExtensionsContextValue } from '../extensions/ExtensionsContextUtils';
-export class Column implements coreType.Column {
- name: string;
-
- type: string;
-
- constructor(name: string, type: string) {
- this.name = name;
- this.type = type;
- }
-}
+const { GenericDataType } = coreType;
export class Table implements coreType.Table {
name: string;
- columns: Column[];
+ columns: coreType.Column[];
- constructor(name: string, columns: Column[]) {
+ constructor(name: string, columns: coreType.Column[]) {
this.name = name;
this.columns = columns;
}
- addColumn(column: Column): void {
+ addColumn(column: coreType.Column): void {
this.columns.push(column);
}
}
@@ -190,6 +178,7 @@ const registerViewProvider: typeof coreType.registerViewProvider = (
};
export const core: typeof coreType = {
+ GenericDataType,
registerViewProvider,
Disposable,
};
diff --git a/superset-frontend/src/core/sqlLab.ts b/superset-frontend/src/core/sqlLab.ts
index 719d984fc4..e59bb65742 100644
--- a/superset-frontend/src/core/sqlLab.ts
+++ b/superset-frontend/src/core/sqlLab.ts
@@ -16,12 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import type { sqlLab as sqlLabType } from '@apache-superset/core';
+import { sqlLab as sqlLabType, core as coreType } from '@apache-superset/core';
import {
QUERY_FAILED,
QUERY_SUCCESS,
QUERY_EDITOR_SETDB,
querySuccess,
+ startQuery,
+ START_QUERY,
+ stopQuery,
+ STOP_QUERY,
+ createQueryFailedAction,
} from 'src/SqlLab/actions/sqlLab';
import { RootState, store } from 'src/views/store';
import { AnyListenerPredicate } from '@reduxjs/toolkit';
@@ -29,6 +34,169 @@ import type { SqlLabRootState } from 'src/SqlLab/types';
import { Disposable, Editor, Panel, Tab } from './core';
import { createActionListener } from './utils';
+const { CTASMethod } = sqlLabType;
+
+export class CTAS implements sqlLabType.CTAS {
+ method: sqlLabType.CTASMethod;
+
+ tempTable: string;
+
+ constructor(asView: boolean, tempTable: string) {
+ this.method = asView ? CTASMethod.View : CTASMethod.Table;
+ this.tempTable = tempTable;
+ }
+}
+
+export class QueryContext implements sqlLabType.QueryContext {
+ clientId: string;
+
+ ctas: sqlLabType.CTAS | null;
+
+ editor: Editor;
+
+ requestedLimit: number | null;
+
+ runAsync: boolean;
+
+ startDttm: number;
+
+ tab: Tab;
+
+ private templateParams: string;
+
+ private parsedParams: Record<string, any>;
+
+ constructor(
+ clientId: string,
+ tab: Tab,
+ runAsync: boolean,
+ startDttm: number,
+ options: {
+ templateParams?: string;
+ ctasMethod?: string;
+ tempTable?: string;
+ requestedLimit?: number;
+ } = {},
+ ) {
+ this.clientId = clientId;
+ this.tab = tab;
+ this.runAsync = runAsync;
+ this.startDttm = startDttm;
+ this.requestedLimit = options.requestedLimit ?? null;
+ this.ctas = options.tempTable
+ ? new CTAS(options.ctasMethod === CTASMethod.View, options.tempTable)
+ : null;
+ this.templateParams = options.templateParams ?? '';
+ }
+
+ /**
+ * A custom accessor is used to process JSON parsing only
+ * when necessary for better performance.
+ */
+ get templateParameters() {
+ if (this.parsedParams) {
+ return this.parsedParams;
+ }
+
+ let parsed = {};
+ try {
+ parsed = JSON.parse(this.templateParams);
+ } catch (e) {
+ // ignore invalid format string.
+ }
+ this.parsedParams = parsed;
+
+ return parsed;
+ }
+}
+
+export class QueryResultContext
+ extends QueryContext
+ implements sqlLabType.QueryResultContext
+{
+ appliedLimit: number;
+
+ appliedLimitingFactor: string;
+
+ endDttm: number;
+
+ executedSql: string;
+
+ remoteId: number;
+
+ result: sqlLabType.QueryResult;
+
+ constructor(
+ clientId: string,
+ remoteId: number,
+ executedSql: string,
+ columns: sqlLabType.QueryResult['columns'],
+ data: sqlLabType.QueryResult['data'],
+ tab: Tab,
+ runAsync: boolean,
+ startDttm: number,
+ endDttm: number,
+ options: {
+ appliedLimit?: number;
+ appliedLimitingFactor?: string;
+ templateParams?: string;
+ ctasMethod?: string;
+ tempTable?: string;
+ requestedLimit?: number;
+ } = {},
+ ) {
+ const { appliedLimit, appliedLimitingFactor, ...opt } = options;
+ super(clientId, tab, runAsync, startDttm, opt);
+ this.remoteId = remoteId;
+ this.executedSql = executedSql;
+ this.endDttm = endDttm;
+ this.result = {
+ columns,
+ data,
+ };
+ this.appliedLimit = appliedLimit ?? data.length;
+ this.appliedLimitingFactor = options.appliedLimitingFactor ?? '';
+ }
+}
+
+export class QueryErrorResultContext
+ extends QueryContext
+ implements sqlLabType.QueryErrorResultContext
+{
+ endDttm: number;
+
+ errorMessage: string;
+
+ errors: coreType.SupersetError[] | null;
+
+ executedSql: string | null;
+
+ constructor(
+ clientId: string,
+ errorMessage: string,
+ errors: coreType.SupersetError[],
+ tab: Tab,
+ runAsync: boolean,
+ startDttm: number,
+ options: {
+ ctasMethod?: string;
+ executedSql?: string;
+ endDttm?: number;
+ templateParams?: string;
+ tempTable?: string;
+ requestedLimist?: number;
+ queryId?: number;
+ } = {},
+ ) {
+ const { queryId, executedSql, endDttm, ...opt } = options;
+ super(clientId, tab, runAsync, startDttm, opt);
+ this.executedSql = executedSql ?? null;
+ this.errorMessage = errorMessage;
+ this.errors = errors;
+ this.endDttm = endDttm ?? Date.now();
+ }
+}
+
const activeEditorId = () => {
const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState();
const { tabHistory } = sqlLab;
@@ -57,43 +225,180 @@ const getCurrentTab: typeof sqlLabType.getCurrentTab = () => {
return undefined;
};
-const predicate = (actionType: string): AnyListenerPredicate<RootState> => {
+const predicate = (
+ actionType: string,
+ currentTabOnly: boolean = true,
+): AnyListenerPredicate<RootState> => {
// Uses closure to capture the active editor ID at the time the listener is created
const id = activeEditorId();
return action =>
// Compares the original id with the current active editor ID
- action.type === actionType && activeEditorId() === id;
+ action.type === actionType && (!currentTabOnly || activeEditorId() === id);
};
export const onDidQueryRun: typeof sqlLabType.onDidQueryRun = (
- listener: (editor: sqlLabType.Editor) => void,
+ listener: (editor: sqlLabType.QueryContext) => void,
thisArgs?: any,
): Disposable =>
createActionListener(
- predicate(QUERY_SUCCESS),
+ predicate(START_QUERY),
+ listener,
+ (action: ReturnType<typeof startQuery>) => {
+ const { query } = action;
+ const {
+ id,
+ dbId,
+ catalog,
+ schema,
+ sql,
+ startDttm,
+ ctas_method: ctasMethod,
+ runAsync,
+ tempTable,
+ templateParams,
+ queryLimit,
+ } = query;
+ const editor = new Editor(sql, dbId, catalog, schema);
+ const panels: Panel[] = []; // TODO: Populate panels
+ const tab = new Tab(query.sqlEditorId, query.tab, editor, panels);
+ return new QueryContext(id, tab, runAsync, startDttm, {
+ ctasMethod,
+ tempTable,
+ templateParams,
+ requestedLimit: queryLimit,
+ });
+ },
+ thisArgs,
+ );
+
+export const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = (
+ listener: (query: sqlLabType.QueryResultContext) => void,
+ thisArgs?: any,
+): Disposable =>
+ createActionListener(
+ predicate(QUERY_SUCCESS, false),
listener,
(action: ReturnType<typeof querySuccess>) => {
+ const { query, results } = action;
+ const {
+ id,
+ dbId,
+ catalog,
+ schema,
+ sql,
+ startDttm,
+ ctas_method: ctasMethod,
+ runAsync,
+ templateParams,
+ } = query;
+ const {
+ query_id: queryId,
+ columns,
+ data,
+ query: { endDttm, executedSql, tempTable, limit, limitingFactor },
+ } = results;
+ const editor = new Editor(sql, dbId, catalog, schema);
+ const panels: Panel[] = []; // TODO: Populate panels
+ const tab = new Tab(query.sqlEditorId, query.tab, editor, panels);
+ return new QueryResultContext(
+ id,
+ queryId,
+ executedSql ?? sql,
+ columns,
+ data,
+ tab,
+ runAsync,
+ startDttm,
+ endDttm,
+ {
+ ctasMethod,
+ tempTable,
+ templateParams,
+ appliedLimit: limit,
+ appliedLimitingFactor: limitingFactor,
+ },
+ );
+ },
+ thisArgs,
+ );
+
+export const onDidQueryStop: typeof sqlLabType.onDidQueryStop = (
+ listener: (query: sqlLabType.QueryContext) => void,
+ thisArgs?: any,
+): Disposable =>
+ createActionListener(
+ predicate(STOP_QUERY),
+ listener,
+ (action: ReturnType<typeof stopQuery>) => {
const { query } = action;
- const { dbId, catalog, schema, sql } = query;
- return new Editor(sql, dbId, catalog, schema);
+ const {
+ id,
+ dbId,
+ catalog,
+ schema,
+ sql,
+ startDttm,
+ ctas_method: ctasMethod,
+ runAsync,
+ tempTable,
+ templateParams,
+ } = query;
+ const editor = new Editor(sql, dbId, catalog, schema);
+ const panels: Panel[] = []; // TODO: Populate panels
+ const tab = new Tab(query.sqlEditorId, query.tab, editor, panels);
+ return new QueryContext(id, tab, runAsync, startDttm, {
+ ctasMethod,
+ tempTable,
+ templateParams,
+ });
},
thisArgs,
);
export const onDidQueryFail: typeof sqlLabType.onDidQueryFail = (
- listener: (e: string) => void,
+ listener: (query: sqlLabType.QueryErrorResultContext) => void,
thisArgs?: any,
): Disposable =>
createActionListener(
- predicate(QUERY_FAILED),
+ predicate(QUERY_FAILED, false),
listener,
- (action: {
- type: string;
- query: any;
- msg: string;
- link: any;
- errors: any;
- }) => action.msg,
+ (action: ReturnType<typeof createQueryFailedAction>) => {
+ const { query, msg: errorMessage, errors } = action;
+ const {
+ id,
+ dbId,
+ catalog,
+ endDttm,
+ executedSql,
+ schema,
+ sql,
+ startDttm,
+ ctas_method: ctasMethod,
+ runAsync,
+ templateParams,
+ query_id: queryId,
+ tempTable,
+ } = query;
+ const editor = new Editor(sql, dbId, catalog, schema);
+ const panels: Panel[] = []; // TODO: Populate panels
+ const tab = new Tab(query.sqlEditorId, query.tab, editor, panels);
+ return new QueryErrorResultContext(
+ id,
+ errorMessage,
+ errors,
+ tab,
+ runAsync,
+ startDttm,
+ {
+ queryId,
+ executedSql,
+ endDttm,
+ ctasMethod,
+ tempTable,
+ templateParams,
+ },
+ );
+ },
thisArgs,
);
@@ -141,14 +446,6 @@ const onDidChangeTabTitle: typeof sqlLabType.onDidChangeTabTitle = () => {
throw new Error('Not implemented yet');
};
-const onDidQueryStop: typeof sqlLabType.onDidQueryStop = () => {
- throw new Error('Not implemented yet');
-};
-
-const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = () => {
- throw new Error('Not implemented yet');
-};
-
const getDatabases: typeof sqlLabType.getDatabases = () => {
throw new Error('Not implemented yet');
};
@@ -182,6 +479,7 @@ const onDidRefreshTables: typeof sqlLabType.onDidRefreshTables = () => {
};
export const sqlLab: typeof sqlLabType = {
+ CTASMethod,
getCurrentTab,
onDidChangeEditorContent,
onDidChangeEditorDatabase,
(superset) 02/02: fix: SQL Lab tab events (#35105)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 5.0-extensions
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 84b81b814e094973b6efead06111d1c8ac1cdf1a
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Sep 11 17:53:51 2025 -0300
fix: SQL Lab tab events (#35105)
(cherry picked from commit e729b2dbb42ada7feb6484a2236be73932b630bc)
---
superset-frontend/src/SqlLab/actions/sqlLab.js | 2 +
.../src/SqlLab/actions/sqlLab.test.js | 4 ++
superset-frontend/src/SqlLab/fixtures.ts | 3 ++
.../src/SqlLab/reducers/getInitialState.ts | 4 ++
superset-frontend/src/SqlLab/types.ts | 1 +
.../SqlLab/utils/reduxStateToLocalStorageHelper.ts | 1 +
superset-frontend/src/core/sqlLab.ts | 63 ++++++++++++++++------
.../src/hooks/apiResources/sqlEditorTabs.test.ts | 1 +
8 files changed, 64 insertions(+), 15 deletions(-)
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index fb94af58c0..708f14c76e 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -396,6 +396,7 @@ export function runQueryFromSqlEditor(
dbId: qe.dbId,
sql: qe.selectedText || qe.sql,
sqlEditorId: qe.id,
+ immutableId: qe.immutableId,
tab: qe.name,
catalog: qe.catalog,
schema: qe.schema,
@@ -543,6 +544,7 @@ export function addQueryEditor(queryEditor) {
const newQueryEditor = {
...queryEditor,
id: nanoid(11),
+ immutableId: nanoid(11),
loaded: true,
inLocalStorage: true,
};
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index d01bfd1b1c..cb7d5fc783 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -441,6 +441,7 @@ describe('async actions', () => {
queryLimit: undefined,
maxRow: undefined,
id: 'abcd',
+ immutableId: 'abcd',
templateParams: undefined,
inLocalStorage: true,
loaded: true,
@@ -570,6 +571,7 @@ describe('async actions', () => {
type: actions.ADD_QUERY_EDITOR,
queryEditor: {
...queryEditor,
+ immutableId: 'abcd',
inLocalStorage: true,
loaded: true,
},
@@ -597,6 +599,7 @@ describe('async actions', () => {
type: actions.ADD_QUERY_EDITOR,
queryEditor: {
id: 'abcd',
+ immutableId: 'abcd',
sql: expect.stringContaining('SELECT ...'),
name: `Untitled Query 7`,
dbId: defaultQueryEditor.dbId,
@@ -753,6 +756,7 @@ describe('async actions', () => {
queryEditor: {
...queryEditor,
id: 'abcd',
+ immutableId: 'abcd',
loaded: true,
inLocalStorage: true,
},
diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts
index 075ccc2e53..f677dabc11 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -188,6 +188,7 @@ export const table = {
export const defaultQueryEditor = {
version: LatestQueryEditorVersion,
id: 'dfsadfs',
+ immutableId: 'immutable-id',
autorun: false,
dbId: 1,
latestQueryId: null,
@@ -204,6 +205,7 @@ export const defaultQueryEditor = {
export const extraQueryEditor1 = {
...defaultQueryEditor,
id: 'diekd23',
+ immutableId: 'immutable-id',
sql: 'SELECT *\nFROM\nWHERE\nLIMIT',
name: 'Untitled Query 2',
selectedText: 'SELECT',
@@ -212,6 +214,7 @@ export const extraQueryEditor1 = {
export const extraQueryEditor2 = {
...defaultQueryEditor,
id: 'owkdi998',
+ immutableId: 'immutable-id',
sql: '',
name: 'Untitled Query 3',
};
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
index 361cc5621d..f11b6fef68 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
@@ -17,6 +17,7 @@
* under the License.
*/
import { t } from '@superset-ui/core';
+import { nanoid } from 'nanoid';
import type { BootstrapData } from 'src/types/bootstrapTypes';
import type { InitialState } from 'src/hooks/apiResources/sqlLab';
import {
@@ -55,6 +56,7 @@ export default function getInitialState({
let queryEditors: Record<string, QueryEditor> = {};
const defaultQueryEditor = {
version: LatestQueryEditorVersion,
+ immutableId: nanoid(11),
loaded: true,
name: t('Untitled query'),
sql: '',
@@ -78,6 +80,7 @@ export default function getInitialState({
queryEditor = {
version: activeTab.extra_json?.version ?? QueryEditorVersion.V1,
id: id.toString(),
+ immutableId: activeTab.extra_json?.immutableId ?? nanoid(11),
loaded: true,
name: activeTab.label,
sql: activeTab.sql || '',
@@ -100,6 +103,7 @@ export default function getInitialState({
queryEditor = {
...defaultQueryEditor,
id: id.toString(),
+ immutableId: nanoid(11),
loaded: false,
name: label,
dbId: undefined,
diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts
index 3cff1d7eb2..384d482f0f 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -47,6 +47,7 @@ export interface CursorPosition {
export interface QueryEditor {
version: QueryEditorVersion;
id: string;
+ immutableId: string;
dbId?: number;
name: string;
title?: string; // keep it optional for backward compatibility
diff --git a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
index e8e1e15673..683b082b38 100644
--- a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
+++ b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
@@ -45,6 +45,7 @@ const PERSISTENT_QUERY_EDITOR_KEYS = new Set([
'dbId',
'height',
'id',
+ 'immutableId',
'latestQueryId',
'northPercent',
'queryLimit',
diff --git a/superset-frontend/src/core/sqlLab.ts b/superset-frontend/src/core/sqlLab.ts
index e59bb65742..03321e4565 100644
--- a/superset-frontend/src/core/sqlLab.ts
+++ b/superset-frontend/src/core/sqlLab.ts
@@ -197,6 +197,16 @@ export class QueryErrorResultContext
}
}
+const getActiveEditorImmutableId = () => {
+ const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState();
+ const { queryEditors, tabHistory } = sqlLab;
+ const activeEditorId = tabHistory[tabHistory.length - 1];
+ const activeEditor = queryEditors.find(
+ editor => editor.id === activeEditorId,
+ );
+ return activeEditor?.immutableId;
+};
+
const activeEditorId = () => {
const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState();
const { tabHistory } = sqlLab;
@@ -225,19 +235,40 @@ const getCurrentTab: typeof sqlLabType.getCurrentTab = () => {
return undefined;
};
-const predicate = (
- actionType: string,
- currentTabOnly: boolean = true,
-): AnyListenerPredicate<RootState> => {
- // Uses closure to capture the active editor ID at the time the listener is created
- const id = activeEditorId();
- return action =>
- // Compares the original id with the current active editor ID
- action.type === actionType && (!currentTabOnly || activeEditorId() === id);
+const predicate = (actionType: string): AnyListenerPredicate<RootState> => {
+ // Capture the immutable ID of the active editor at the time the listener is created
+ // This ID never changes for a tab, ensuring stable event routing
+ const registrationImmutableId = getActiveEditorImmutableId();
+
+ return action => {
+ if (action.type !== actionType) return false;
+
+ // If we don't have a registration ID, don't filter events
+ if (!registrationImmutableId) return true;
+
+ // For query events, use the immutableId directly from the action payload
+ if (action.query?.immutableId) {
+ return action.query.immutableId === registrationImmutableId;
+ }
+
+ // For tab events, we need to find the immutable ID of the affected tab
+ if (action.queryEditor?.id) {
+ const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } =
+ store.getState();
+ const { queryEditors } = sqlLab;
+ const queryEditor = queryEditors.find(
+ editor => editor.id === action.queryEditor.id,
+ );
+ return queryEditor?.immutableId === registrationImmutableId;
+ }
+
+ // Fallback: do not allow the event if we can't determine the source
+ return false;
+ };
};
export const onDidQueryRun: typeof sqlLabType.onDidQueryRun = (
- listener: (editor: sqlLabType.QueryContext) => void,
+ listener: (queryContext: sqlLabType.QueryContext) => void,
thisArgs?: any,
): Disposable =>
createActionListener(
@@ -272,11 +303,11 @@ export const onDidQueryRun: typeof sqlLabType.onDidQueryRun = (
);
export const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = (
- listener: (query: sqlLabType.QueryResultContext) => void,
+ listener: (queryResultContext: sqlLabType.QueryResultContext) => void,
thisArgs?: any,
): Disposable =>
createActionListener(
- predicate(QUERY_SUCCESS, false),
+ predicate(QUERY_SUCCESS),
listener,
(action: ReturnType<typeof querySuccess>) => {
const { query, results } = action;
@@ -323,7 +354,7 @@ export const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = (
);
export const onDidQueryStop: typeof sqlLabType.onDidQueryStop = (
- listener: (query: sqlLabType.QueryContext) => void,
+ listener: (queryContext: sqlLabType.QueryContext) => void,
thisArgs?: any,
): Disposable =>
createActionListener(
@@ -356,11 +387,13 @@ export const onDidQueryStop: typeof sqlLabType.onDidQueryStop = (
);
export const onDidQueryFail: typeof sqlLabType.onDidQueryFail = (
- listener: (query: sqlLabType.QueryErrorResultContext) => void,
+ listener: (
+ queryErrorResultContext: sqlLabType.QueryErrorResultContext,
+ ) => void,
thisArgs?: any,
): Disposable =>
createActionListener(
- predicate(QUERY_FAILED, false),
+ predicate(QUERY_FAILED),
listener,
(action: ReturnType<typeof createQueryFailedAction>) => {
const { query, msg: errorMessage, errors } = action;
diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
index 42f738f3c9..5d6c9a55a9 100644
--- a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
+++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
@@ -33,6 +33,7 @@ import {
const expectedQueryEditor = {
version: LatestQueryEditorVersion,
id: '123',
+ immutableId: 'immutable-id',
dbId: 456,
name: 'tab 1',
sql: 'SELECT * from example_table',