codeant-ai-for-open-source[bot] commented on code in PR #37642:
URL: https://github.com/apache/superset/pull/37642#discussion_r2763911945
##########
superset-frontend/src/core/sqlLab/index.ts:
##########
@@ -392,10 +492,212 @@ const onDidChangeTabTitle: typeof
sqlLabApi.onDidChangeTabTitle = (
thisArgs,
);
+/**
+ * Event fired when a new tab is created.
+ */
+const onDidCreateTab: typeof sqlLabApi.onDidCreateTab = (
+ listener: (tab: sqlLabApi.Tab) => void,
+ thisArgs?: any,
+): Disposable =>
+ createActionListener(
+ globalPredicate(ADD_QUERY_EDITOR),
+ listener,
+ (action: { type: string; queryEditor: QueryEditor }) =>
+ makeTab(
+ action.queryEditor.id!,
+ action.queryEditor.name ?? '',
+ action.queryEditor.dbId ?? 0,
+ action.queryEditor.catalog,
+ action.queryEditor.schema ?? undefined,
+ ),
+ thisArgs,
+ );
+
+/**
+ * Tab/Editor Management APIs
+ */
+
+const createTab: typeof sqlLabApi.createTab = async (
+ options?: sqlLabApi.CreateTabOptions,
+) => {
+ const {
+ sqlLab: { queryEditors, tabHistory, unsavedQueryEditor, databases },
+ common,
+ } = store.getState() as SqlLabRootState;
+
+ const activeQueryEditor = queryEditors.find(
+ (qe: QueryEditor) => qe.id === tabHistory[tabHistory.length - 1],
+ );
+ const dbIds = Object.values(databases).map(db => db.id);
+ const defaultDbId = common?.conf?.SQLLAB_DEFAULT_DBID;
+ const firstDbId = dbIds.length > 0 ? Math.min(...dbIds) : undefined;
+
+ // Inherit from active tab or use defaults
+ const inheritedValues = {
+ ...queryEditors[0],
+ ...activeQueryEditor,
+ ...(unsavedQueryEditor?.id === activeQueryEditor?.id &&
unsavedQueryEditor),
+ } as Partial<QueryEditor>;
+
+ // Generate default name if no title provided
+ const name =
+ options?.title ??
+ newQueryTabName(
+ queryEditors?.map((qe: QueryEditor) => ({
+ ...qe,
+ ...(qe.id === unsavedQueryEditor?.id && unsavedQueryEditor),
+ })) || [],
+ );
+
+ const newQueryEditor: Partial<QueryEditor> = {
+ dbId:
+ options?.databaseId ?? inheritedValues.dbId ?? defaultDbId ?? firstDbId,
+ catalog: options?.catalog ?? inheritedValues.catalog,
+ schema: options?.schema ?? inheritedValues.schema,
+ sql: options?.sql ?? 'SELECT ...',
+ queryLimit:
+ inheritedValues.queryLimit ?? common?.conf?.DEFAULT_SQLLAB_LIMIT,
+ autorun: false,
+ name,
+ };
+
+ store.dispatch(addQueryEditor(newQueryEditor) as any);
+
+ // Get the newly created tab
+ const updatedState = store.getState() as SqlLabRootState;
+ const newTab =
+ updatedState.sqlLab.queryEditors[
+ updatedState.sqlLab.queryEditors.length - 1
+ ];
+
+ return makeTab(
+ newTab.id,
+ newTab.name ?? '',
+ newTab.dbId ?? 0,
+ newTab.catalog,
+ newTab.schema ?? undefined,
+ );
+};
+
+const closeTab: typeof sqlLabApi.closeTab = async (tabId: string) => {
+ const queryEditor = findQueryEditor(tabId);
+ if (queryEditor) {
+ store.dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
+ }
+};
+
+const setActiveTab: typeof sqlLabApi.setActiveTab = async (tabId: string) => {
+ const queryEditor = findQueryEditor(tabId);
+ if (queryEditor) {
+ store.dispatch(setActiveQueryEditor(queryEditor as QueryEditor));
+ }
+};
+
+const executeQuery: typeof sqlLabApi.executeQuery = async options => {
+ const state = store.getState() as SqlLabRootState;
+ const editorId = activeEditorId();
+ const queryEditor = findQueryEditor(editorId);
+
+ if (!queryEditor) {
+ throw new Error('No active query editor');
+ }
+
+ const { databases, unsavedQueryEditor } = state.sqlLab;
+ const qe = {
+ ...queryEditor,
+ ...(queryEditor.id === unsavedQueryEditor?.id && unsavedQueryEditor),
+ } as QueryEditor;
+
+ const database = qe.dbId ? databases[qe.dbId] : null;
+ const defaultLimit = state.common?.conf?.DEFAULT_SQLLAB_LIMIT ?? 1000;
+
+ // Determine SQL to execute
+ let sql: string;
+ let updateTabState = true;
+
+ if (options?.sql !== undefined) {
+ // Custom SQL provided - don't update tab state
+ ({ sql } = options);
+ updateTabState = false;
+ } else if (options?.selectedOnly && qe.selectedText) {
+ // Run selected text only
+ sql = qe.selectedText;
+ updateTabState = false;
+ } else {
+ // Default: use editor content (selected text takes precedence)
+ sql = qe.selectedText || qe.sql;
+ updateTabState = !qe.selectedText;
+ }
+
+ // Merge template params
+ const templateParams = options?.templateParams
+ ? JSON.stringify({
+ ...JSON.parse(qe.templateParams || '{}'),
+ ...options.templateParams,
+ })
+ : qe.templateParams;
+
+ const queryId = nanoid(11);
+
+ const query: Query = {
+ id: queryId,
+ dbId: qe.dbId,
+ sql,
+ sqlEditorId: qe.tabViewId ?? qe.id,
+ sqlEditorImmutableId: qe.immutableId,
+ tab: qe.name,
+ catalog: qe.catalog,
+ schema: qe.schema,
+ tempTable: options?.ctas?.tableName,
+ templateParams,
+ queryLimit: options?.limit ?? qe.queryLimit ?? defaultLimit,
+ runAsync: database ? database.allow_run_async : false,
+ ctas: !!options?.ctas,
+ ctas_method: options?.ctas?.method,
+ updateTabState,
+ };
+
+ store.dispatch(runQueryAction(query));
+
+ return queryId;
+};
+
+const cancelQuery: typeof sqlLabApi.cancelQuery = async (queryId: string) => {
+ const state = store.getState() as SqlLabRootState;
+ const query = state.sqlLab.queries[queryId];
+
+ if (query) {
+ store.dispatch(postStopQuery(query as any) as any);
Review Comment:
**Suggestion:** The `cancelQuery` API dispatches `postStopQuery` instead of
`stopQueryAction`, so no `STOP_QUERY` action is emitted and the server-side
cancellation logic that likely listens to `STOP_QUERY` will not run, meaning
queries may continue running and `onDidQueryStop` listeners will never fire for
cancellations initiated via this API. [logic error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Canceled queries don't trigger STOP_QUERY listeners.
- ❌ onDidQueryStop consumers won't receive cancellations.
- ⚠️ Server-side cancellation flows may not run.
- ⚠️ UI features relying on STOP_QUERY stay inconsistent.
```
</details>
```suggestion
store.dispatch(stopQueryAction(query as any) as any);
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. In the running frontend, call the exported API
`sqlLab.cancelQuery(queryId)` (caller
location: any consumer of the embedded SQL Lab API). The implementation
being executed is
in superset-frontend/src/core/sqlLab/index.ts at the cancelQuery function
(lines 665-671).
2. cancelQuery looks up the query in the Redux slice via store.getState()
and finds the
query object (lines 666-667). For an existing query it dispatches
postStopQuery (line 669)
instead of the STOP/stop action variant.
3. Event listeners that subscribe to query stop events use the STOP_QUERY
action path (see
onDidQueryStop which filters predicate(STOP_QUERY) and maps the action to
QueryContext —
the onDidQueryStop registration lives earlier in the same file where
createActionListener
is invoked for STOP_QUERY). Because cancelQuery dispatches postStopQuery,
the STOP_QUERY
action is not emitted and those listeners never receive the stop event.
4. Observed behavior: cancelling a running query via sqlLab.cancelQuery does
not trigger
onDidQueryStop handlers and does not follow the same server cancellation
flow that is
wired to STOP_QUERY listeners. This reproduces reliably by running a query,
invoking
sqlLab.cancelQuery with that query's id, and observing that registered
onDidQueryStop
callbacks are not invoked while postStopQuery is dispatched instead.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/core/sqlLab/index.ts
**Line:** 670:670
**Comment:**
*Logic Error: The `cancelQuery` API dispatches `postStopQuery` instead
of `stopQueryAction`, so no `STOP_QUERY` action is emitted and the server-side
cancellation logic that likely listens to `STOP_QUERY` will not run, meaning
queries may continue running and `onDidQueryStop` listeners will never fire for
cancellations initiated via this API.
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.
```
</details>
##########
superset-frontend/src/core/sqlLab/index.ts:
##########
@@ -340,10 +433,17 @@ const onDidCloseTab: typeof sqlLabApi.onDidCloseTab = (
thisArgs?: any,
): Disposable =>
createActionListener(
- predicate(REMOVE_QUERY_EDITOR),
+ globalPredicate(REMOVE_QUERY_EDITOR),
listener,
- (action: { type: string; queryEditor: { id: string } }) =>
- getTab(action.queryEditor.id)!,
+ (action: { type: string; queryEditor: QueryEditor }) =>
+ // Construct tab from action data since the tab has already been removed
from state
+ makeTab(
+ action.queryEditor.id,
+ action.queryEditor.name ?? '',
+ action.queryEditor.dbId ?? 0,
+ action.queryEditor.catalog,
+ action.queryEditor.schema,
+ ),
Review Comment:
**Suggestion:** In `onDidCloseTab`, the returned `Tab` is built via
`makeTab`, whose `getEditor()` uses `getEditorAsync`; for a tab that has
already been removed there will never be a corresponding editor handle
registration, so any listener that calls `tab.getEditor()` in a close handler
will get a promise that never resolves and leak a resolver in
`pendingEditorPromises`. [resource leak]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ onDidCloseTab handlers calling getEditor hang forever.
- ⚠️ pendingEditorPromises entries leak memory over time.
- ⚠️ Close-tab cleanup code may not complete.
- ⚠️ Consumers must handle closed-tab editor absence explicitly.
```
</details>
```suggestion
(action: { type: string; queryEditor: QueryEditor }) => {
const { id, name, dbId, catalog, schema } = action.queryEditor;
// For closed tabs, expose a Tab whose editor accessor fails fast
instead of waiting on a handle that will never be registered
return new Tab(
id,
name ?? '',
dbId ?? 0,
catalog,
schema,
() =>
Promise.reject(
new Error(`Editor handle not available for closed tab ${id}`),
),
[],
);
},
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Register a close-tab listener via the public API: call
sqlLab.onDidCloseTab(listener).
The onDidCloseTab registration is implemented in
superset-frontend/src/core/sqlLab/index.ts (lines ~435-448) and constructs a
Tab via
makeTab(...) using the removed queryEditor payload.
2. In the listener implementation, call tab.getEditor() (common pattern:
consumer wants to
inspect editor contents on close). The Tab returned by makeTab uses
makeTab(..., () =>
getEditorAsync(id), ...) so tab.getEditor() calls getEditorAsync(tabId).
3. getEditorAsync (defined earlier in the same file) first checks
editorHandleRegistry for
the tabId; for a recently closed tab the editor component has already
unmounted and no
handle will be (re)registered. getEditorAsync instead pushes a resolver into
pendingEditorPromises and returns a Promise that will never resolve.
4. Result: the listener's call to tab.getEditor() returns a never-resolving
Promise and
pendingEditorPromises retains the resolver (memory leak). This reproduces
deterministically when a consumer of sqlLab.onDidCloseTab calls
tab.getEditor() in the
close callback for a tab that has already been removed.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/core/sqlLab/index.ts
**Line:** 442:446
**Comment:**
*Resource Leak: In `onDidCloseTab`, the returned `Tab` is built via
`makeTab`, whose `getEditor()` uses `getEditorAsync`; for a tab that has
already been removed there will never be a corresponding editor handle
registration, so any listener that calls `tab.getEditor()` in a close handler
will get a promise that never resolves and leak a resolver in
`pendingEditorPromises`.
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.
```
</details>
##########
superset-frontend/packages/superset-core/src/api/sqlLab.ts:
##########
@@ -444,3 +450,253 @@ export declare const onDidCloseTab: Event<Tab>;
* ```
*/
export declare const onDidChangeActiveTab: Event<Tab>;
+
+/**
+ * Event fired when a new tab is created in SQL Lab.
+ * Provides the newly created tab object as the event payload.
+ *
+ * @example
+ * ```typescript
+ * onDidCreateTab.event((tab) => {
+ * console.log('New tab created:', tab.title);
+ * // Initialize extension state for new tab
+ * });
+ * ```
+ */
+export declare const onDidCreateTab: Event<Tab>;
+
+/**
+ * Tab/Editor Management APIs
+ *
+ * These APIs allow extensions to create, close, and manage SQL Lab tabs.
+ */
+
+/**
+ * Options for creating a new SQL Lab tab.
+ */
+export interface CreateTabOptions {
+ /**
+ * Initial SQL content for the editor.
+ */
+ sql?: string;
+
+ /**
+ * Display title for the tab.
+ * If not provided, defaults to "Untitled Query N".
+ */
+ title?: string;
+
+ /**
+ * Database ID to connect to.
+ * If not provided, inherits from the active tab or uses default.
+ */
+ databaseId?: number;
+
+ /**
+ * Catalog name (for multi-catalog databases like Trino).
+ */
+ catalog?: string | null;
+
+ /**
+ * Schema name for the query context.
+ */
+ schema?: string | null;
+}
+
+/**
+ * Creates a new query editor tab in SQL Lab.
+ *
+ * @param options Optional configuration for the new tab
+ * @returns The newly created tab object
+ *
+ * @example
+ * ```typescript
+ * // Create a tab with default settings
+ * const tab = await createTab();
+ *
+ * // Create a tab with specific SQL and database
+ * const tab = await createTab({
+ * sql: "SELECT * FROM users LIMIT 10",
+ * title: "User Query",
+ * databaseId: 1,
+ * schema: "public"
+ * });
+ * ```
+ */
+export declare function createTab(options?: CreateTabOptions): Promise<Tab>;
+
+/**
+ * Closes a specific tab in SQL Lab.
+ *
+ * @param tabId The ID of the tab to close
+ * @returns Promise that resolves when the tab is closed
+ *
+ * @example
+ * ```typescript
+ * const tabs = getTabs();
+ * if (tabs.length > 1) {
+ * await closeTab(tabs[0].id);
+ * }
+ * ```
+ */
+export declare function closeTab(tabId: string): Promise<void>;
+
+/**
+ * Switches to a specific tab in SQL Lab.
+ *
+ * @param tabId The ID of the tab to activate
+ * @returns Promise that resolves when the tab is activated
+ *
+ * @example
+ * ```typescript
+ * const tabs = getTabs();
+ * const targetTab = tabs.find(t => t.title === "My Query");
+ * if (targetTab) {
+ * await setActiveTab(targetTab.id);
+ * }
+ * ```
+ */
+export declare function setActiveTab(tabId: string): Promise<void>;
+
+/**
+ * Query Execution APIs
+ *
+ * These APIs allow extensions to execute and control SQL queries.
+ */
+
+/**
+ * Options for executing a SQL query.
+ */
+export interface QueryOptions {
+ /**
+ * SQL to execute without modifying editor content.
+ * If not provided, uses the current editor content.
+ */
+ sql?: string;
+
+ /**
+ * Run only the selected text in the editor.
+ * Ignored if `sql` option is provided.
+ */
+ selectedOnly?: boolean;
+
+ /**
+ * Override the query row limit.
+ * If not provided, uses the tab's configured limit.
+ */
+ limit?: number;
+
+ /**
+ * Template parameters for Jinja templating.
+ * Merged with existing template parameters from the editor.
+ */
Review Comment:
**Suggestion:** The query execution options use a `templateParams` property
name while the corresponding field in the query context is
`templateParameters`, which is likely to cause confusion and mismatched
payloads; renaming the options field to `templateParameters` aligns the API
surface and reduces integration errors. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Confusing API shape for template parameters.
- ❌ Integration code may miss parameter propagation.
- ⚠️ Docs/examples may be inconsistent.
```
</details>
```suggestion
templateParameters?: Record<string, unknown>;
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Inspect the QueryContext interface in
superset-frontend/packages/superset-core/src/api/sqlLab.ts and note it
defines
`templateParameters: Record<string, any>;` on the context object delivered
to events (see
QueryContext section above QueryOptions).
2. Inspect the QueryOptions definition (at `sqlLab.ts:588-591`) and observe
the option is
named `templateParams?: Record<string, unknown>;`.
3. Create an integration that calls `executeQuery({ templateParams: { foo: 1
} })`
expecting values to be merged into `query.templateParameters` inside
QueryContext; because
of the naming mismatch, off-by-name integration code or documentation that
assumes the
same key will cause confusion or require manual mapping in the
implementation layer.
4. Reproducible by authoring code that uses `executeQuery` and reading
`templateParameters` from the QueryContext in a `onDidQueryRun` listener —
the developer
will have to verify that `templateParams` is actually propagated by the
implementation
(possible source of bugs). The inconsistency is verifiable by searching
these identifiers
in the API file.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/packages/superset-core/src/api/sqlLab.ts
**Line:** 592:592
**Comment:**
*Logic Error: The query execution options use a `templateParams`
property name while the corresponding field in the query context is
`templateParameters`, which is likely to cause confusion and mismatched
payloads; renaming the options field to `templateParameters` aligns the API
surface and reduces integration errors.
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.
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]