From efaa548ca6ab5d3bb914430f3e52a62df8450fe1 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 12 Sep 2025 19:01:40 +0800 Subject: [PATCH 1/6] dbeaver/pro#6327 refactor: sql editor & vqb --- .../@dbeaver/js-helpers/package.json | 5 +- .../js-helpers/src/debouncePromise.ts | 9 + .../@dbeaver/js-helpers/src/index.ts | 10 + .../@dbeaver/js-helpers/src/memoizeLast.ts | 19 ++ webapp/packages/core-blocks/src/index.ts | 1 + webapp/packages/core-blocks/src/useSync.ts | 56 ++++ .../src/SqlEditorTabService.ts | 8 + .../SQLCodeEditorPanel/SQLCodeEditorPanel.tsx | 8 +- .../SQLCodeEditorPanelService.ts | 4 +- .../useSQLCodeEditorPanel.ts | 4 +- .../packages/plugin-sql-editor/package.json | 1 + .../plugin-sql-editor/src/MenuBootstrap.ts | 41 +-- .../plugin-sql-editor/src/SQLParser.ts | 44 ++- .../src/SqlEditor/ISQLEditorData.ts | 19 +- .../src/SqlEditor/SQLEditorModeContext.ts | 26 -- .../src/SqlEditor/useActiveQuery.ts | 4 +- .../src/SqlEditor/useSqlEditor.ts | 297 +++--------------- .../src/SqlEditorModel/ISqlEditorModel.ts | 27 ++ .../src/SqlEditorModel/SqlEditorModel.ts | 168 ++++++++++ .../SqlEditorModel/SqlEditorModelService.ts | 70 +++++ .../packages/plugin-sql-editor/src/index.ts | 2 +- .../packages/plugin-sql-editor/src/module.ts | 4 + .../packages/plugin-sql-editor/tsconfig.json | 3 + webapp/yarn.lock | 37 ++- 24 files changed, 527 insertions(+), 340 deletions(-) create mode 100644 webapp/common-typescript/@dbeaver/js-helpers/src/debouncePromise.ts create mode 100644 webapp/common-typescript/@dbeaver/js-helpers/src/memoizeLast.ts create mode 100644 webapp/packages/core-blocks/src/useSync.ts delete mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditorModel/ISqlEditorModel.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModel.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModelService.ts diff --git a/webapp/common-typescript/@dbeaver/js-helpers/package.json b/webapp/common-typescript/@dbeaver/js-helpers/package.json index a3e947058cb..d3bc14e9094 100644 --- a/webapp/common-typescript/@dbeaver/js-helpers/package.json +++ b/webapp/common-typescript/@dbeaver/js-helpers/package.json @@ -32,6 +32,9 @@ "vitest": "^3" }, "dependencies": { - "async-mutex": "^0" + "async-mutex": "^0", + "p-debounce": "^4.0.0", + "p-memoize": "^8.0.0", + "quick-lru": "^7.1.0" } } diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/debouncePromise.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/debouncePromise.ts new file mode 100644 index 00000000000..e03829b27ea --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/debouncePromise.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export { default as debouncePromise } from 'p-debounce'; diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts index 2eae7dba071..8467ada1253 100644 --- a/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts @@ -1,3 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export * from './debouncePromise.js'; export * from './isDefined.js'; export * from './isNotNullDefined.js'; +export * from './memoizeLast.js'; export * from './mutex.js'; diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/memoizeLast.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/memoizeLast.ts new file mode 100644 index 00000000000..4d0d5fd2e01 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/memoizeLast.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import pMemoize from 'p-memoize'; +import QuickLRU from 'quick-lru'; + +const cacheKey = (args: unknown[]) => JSON.stringify(args); + +export function memoizeLast Promise>(fn: T) { + return pMemoize(fn, { + cache: new QuickLRU({ maxSize: 5 }), + cacheKey, + }); +} diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 0137386deab..4eeecc43c3a 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -34,6 +34,7 @@ export * from './DisplayError.js'; export * from './ErrorBoundary.js'; export * from './Icon.js'; export * from './useHotkeys.js'; +export * from './useSync.js'; export * from './ItemList/ItemList.js'; export * from './ItemList/ItemListSearch.js'; diff --git a/webapp/packages/core-blocks/src/useSync.ts b/webapp/packages/core-blocks/src/useSync.ts new file mode 100644 index 00000000000..ca303d50d58 --- /dev/null +++ b/webapp/packages/core-blocks/src/useSync.ts @@ -0,0 +1,56 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { mutex } from '@dbeaver/js-helpers'; +import { useEffect, useState } from 'react'; +import { useObjectRef } from './useObjectRef.js'; + +interface ISyncHook { + markOutdated(): void; +} + +export function useSync(callback: () => void | Promise): ISyncHook { + const [syncMutex] = useState(() => new mutex.Mutex()); + const [outdated, setOutdated] = useState(false); + const [delayedOutdated, setDelayedOutdated] = useState(false); + + function markOutdated() { + if (syncMutex.isLocked()) { + setDelayedOutdated(true); + } else { + setOutdated(true); + } + } + + function markUpdated() { + if (delayedOutdated) { + setDelayedOutdated(false); + setOutdated(true); + } else { + setOutdated(false); + } + } + + useEffect(() => { + if (outdated && !syncMutex.isLocked()) { + syncMutex + .runExclusive(async () => { + try { + await callback(); + } finally { + markUpdated(); + } + }) + .catch(error => console.error(error)); + } + }); + + return useObjectRef(() => ({ + markOutdated, + })); +} diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index 19793034a2a..ea06059a387 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -44,6 +44,7 @@ import { type ISqlEditorTabState, SQL_EDITOR_TAB_STATE_SCHEMA, SqlDataSourceService, + SqlEditorModelService, SqlEditorService, SqlResultTabsService, } from '@cloudbeaver/plugin-sql-editor'; @@ -67,6 +68,7 @@ const SqlEditorTab = importLazyComponent(() => import('./SqlEditorTab.js').then( ConnectionsManagerService, ContainerResource, CommonDialogService, + SqlEditorModelService, ]) export class SqlEditorTabService extends Bootstrap { get sqlEditorTabs(): ITab[] { @@ -89,6 +91,7 @@ export class SqlEditorTabService extends Bootstrap { private readonly connectionsManagerService: ConnectionsManagerService, private readonly containerResource: ContainerResource, private readonly commonDialogService: CommonDialogService, + private readonly sqlEditorModelService: SqlEditorModelService, ) { super(); @@ -305,6 +308,7 @@ export class SqlEditorTabService extends Bootstrap { private async handleTabRestore(tab: ITab): Promise { if (!SQL_EDITOR_TAB_STATE_SCHEMA.safeParse(tab.handlerState).success) { + await this.sqlEditorModelService.destroy(tab.handlerState.editorId); await this.sqlDataSourceService.destroy(tab.handlerState.editorId); return false; } @@ -531,12 +535,14 @@ export class SqlEditorTabService extends Bootstrap { } private async handleTabUnload(editorTab: ITab) { + await this.sqlEditorModelService.unload(editorTab.handlerState.editorId); await this.sqlDataSourceService.unload(editorTab.handlerState.editorId); this.sqlResultTabsService.removeResultTabs(editorTab.handlerState); } private async handleTabCloseSilent(editorTab: ITab) { + await this.sqlEditorModelService.destroySilent(editorTab.handlerState.editorId); const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId); if (dataSource?.executionContext) { @@ -548,6 +554,8 @@ export class SqlEditorTabService extends Bootstrap { } private async handleTabClose(editorTab: ITab) { + await this.sqlEditorModelService.destroy(editorTab.handlerState.editorId); + const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId); if (dataSource?.executionContext) { diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx index 12a6285dcb0..d62f177c171 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx @@ -86,11 +86,11 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent }); function applyIncoming() { - data.dataSource?.applyIncoming(); + data.model.dataSource?.applyIncoming(); } function keepCurrent() { - data.dataSource?.keepCurrent(); + data.model.dataSource?.keepCurrent(); } return ( @@ -99,8 +99,8 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent ref={setEditorRef} getValue={() => data.value} cursor={{ - anchor: data.cursor.anchor, - head: data.cursor.head, + anchor: data.model.cursor.anchor, + head: data.model.cursor.head, }} incomingValue={data.incomingValue} extensions={extensions} diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts index e53bc5c8889..8233622903f 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ export class SQLCodeEditorPanelService { key: 'sql-editor', icon: '/icons/sql_script_sm.svg', name: 'sql_editor_script_editor', - isHidden: (_, props) => props?.data.dataSource?.hasFeature(ESqlDataSourceFeatures.script) !== true, + isHidden: (_, props) => props?.data.model.dataSource?.hasFeature(ESqlDataSourceFeatures.script) !== true, panel: () => SQLCodeEditorPanel, }); } diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts index 1c503cdda79..66ab764246c 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts @@ -28,7 +28,7 @@ export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) { highlightActiveQuery() { this.editor.clearActiveQueryHighlight(); - const segment = this.data.activeSegment; + const segment = this.data.model.cursorSegment; if (segment) { this.editor.highlightActiveQuery(segment.begin, segment.end); @@ -52,7 +52,7 @@ export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) { ); useExecutor({ - executor: data.onUpdate, + executor: data.model.onUpdate, handlers: [updateHighlight], }); diff --git a/webapp/packages/plugin-sql-editor/package.json b/webapp/packages/plugin-sql-editor/package.json index 723427b608b..6bdb81b71a7 100644 --- a/webapp/packages/plugin-sql-editor/package.json +++ b/webapp/packages/plugin-sql-editor/package.json @@ -44,6 +44,7 @@ "@cloudbeaver/plugin-codemirror6": "workspace:*", "@cloudbeaver/plugin-data-viewer": "workspace:*", "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", "mobx": "^6", "mobx-react-lite": "^4", "react": "^19", diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 8db3a7f4839..bf0e3afd92c 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -161,11 +161,6 @@ export class MenuBootstrap extends Bootstrap { id: 'sql-editor-actions-more', actions: [ACTION_DOWNLOAD, ACTION_UPLOAD], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE], - isHidden: context => { - const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; - - return data.activeSegmentMode.activeSegmentMode; - }, isDisabled: (context, action) => { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; @@ -217,10 +212,7 @@ export class MenuBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [SQL_EDITOR_ACTIONS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE], - getItems: (context, items) => [ - ...items, - ...EXECUTIONS_ACTIONS, - ], + getItems: (context, items) => [...items, ...EXECUTIONS_ACTIONS], }); this.keyBindingService.addKeyBindingHandler({ @@ -242,12 +234,7 @@ export class MenuBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'sql-editor-actions', - actions: [ - ...EXECUTIONS_ACTIONS, - ACTION_SQL_EDITOR_FORMAT, - ACTION_REDO, - ACTION_UNDO, - ], + actions: [...EXECUTIONS_ACTIONS, ACTION_SQL_EDITOR_FORMAT, ACTION_REDO, ACTION_UNDO], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isActionApplicable: (contexts, action): boolean => { const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; @@ -256,25 +243,21 @@ export class MenuBootstrap extends Bootstrap { return false; } - if ( - !sqlEditorData.isExecutionAllowed && - EXECUTIONS_ACTIONS.includes(action) - ) { + if (!sqlEditorData.isExecutionAllowed && EXECUTIONS_ACTIONS.includes(action)) { return false; } if (action === ACTION_SQL_EDITOR_FORMAT) { - return !!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.script) && !sqlEditorData.activeSegmentMode.activeSegmentMode; + return !!sqlEditorData.model.dataSource?.hasFeature(ESqlDataSourceFeatures.script); } if (action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN) { - return !!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && - !!sqlEditorData.dialect?.supportsExplainExecutionPlan; + return !!sqlEditorData.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && !!sqlEditorData.dialect?.supportsExplainExecutionPlan; } // TODO we have to add check for output action ? if ( - !sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && + !sqlEditorData.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && [ACTION_SQL_EDITOR_EXECUTE, ACTION_SQL_EDITOR_EXECUTE_NEW, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN].includes(action) ) { return false; @@ -402,24 +385,16 @@ export class MenuBootstrap extends Bootstrap { data.executeQueryNewTab(); break; case ACTION_SQL_EDITOR_EXECUTE_SCRIPT: - if (data.activeSegmentMode.activeSegmentMode) { - return; - } - data.executeScript(); break; case ACTION_SQL_EDITOR_FORMAT: - if (data.activeSegmentMode.activeSegmentMode) { - return; - } - data.formatScript(); break; case ACTION_UNDO: - data.dataSource?.history.undo(); + data.model.dataSource?.history.undo(); break; case ACTION_REDO: - data.dataSource?.history.redo(); + data.model.dataSource?.history.redo(); break; case ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN: data.showExecutionPlan(); diff --git a/webapp/packages/plugin-sql-editor/src/SQLParser.ts b/webapp/packages/plugin-sql-editor/src/SQLParser.ts index 4bad6999808..ec8c98946d1 100644 --- a/webapp/packages/plugin-sql-editor/src/SQLParser.ts +++ b/webapp/packages/plugin-sql-editor/src/SQLParser.ts @@ -1,10 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { isArraysEqual } from '@cloudbeaver/core-utils'; import { action, computed, makeObservable, observable } from 'mobx'; export interface IQueryInfo { @@ -39,9 +40,12 @@ export class SQLParser { private _scripts: ISQLScriptSegment[]; private script: string; + private lastParsingArgs: any[]; + constructor() { this._scripts = []; this.script = ''; + this.lastParsingArgs = []; makeObservable(this, { actualScript: computed, @@ -55,8 +59,36 @@ export class SQLParser { }); } + parse | IQueryInfo[]>( + parser: (script: string, ...args: Args) => TResult, + ...args: Args + ): TResult extends Promise ? Promise : void { + const parsingScript = this.actualScript; + const parsingArgs = [parsingScript, parser, ...args]; + + if (isArraysEqual(this.lastParsingArgs, parsingArgs)) { + return undefined as any; + } + const result = parser(parsingScript, ...args); + + const applyResult = (queries: IQueryInfo[]) => { + if (this.actualScript === parsingScript) { + this.setQueries(queries); + this.lastParsingArgs = parsingArgs; + } + }; + + if (result instanceof Promise) { + return result.then(applyResult) as any; + } + + applyResult(result); + + return undefined as any; + } + getScriptSegment(): ISQLScriptSegment { - const script = this.script || ''; + const script = this.actualScript || ''; return { query: script, @@ -70,12 +102,8 @@ export class SQLParser { return this.getQueryAtPos(begin); } - if (end === -1) { - end = begin; - } - return { - query: (this.script || '').substring(begin, end), + query: this.actualScript.substring(begin, end), begin, end, }; @@ -103,7 +131,7 @@ export class SQLParser { setQueries(queries: IQueryInfo[]): this { this._scripts = queries.map(query => ({ - query: this.script.substring(query.start, query.end), + query: this.actualScript.substring(query.start, query.end), begin: query.start, end: query.end, })); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts index 0252f0ebeea..cc154e1e0e6 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts @@ -8,10 +8,10 @@ import type { ISyncExecutor } from '@cloudbeaver/core-executor'; import type { SqlDialectInfo } from '@cloudbeaver/core-sdk'; -import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; +import type { ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; import type { SQLProposal } from '../SqlEditorService.js'; -import type { ISQLScriptSegment, SQLParser } from '../SQLParser.js'; -import type { ISQLEditorMode } from './SQLEditorModeContext.js'; +import type { ISQLScriptSegment } from '../SQLParser.js'; +import type { ISqlEditorModel } from '../SqlEditorModel/ISqlEditorModel.js'; export interface ISegmentExecutionData { segment: ISQLScriptSegment; @@ -19,12 +19,8 @@ export interface ISegmentExecutionData { } export interface ISQLEditorData { - readonly cursor: ISqlEditorCursor; - activeSegmentMode: ISQLEditorMode; - readonly parser: SQLParser; + readonly model: ISqlEditorModel; readonly dialect: SqlDialectInfo | undefined; - readonly activeSegment: ISQLScriptSegment | undefined; - readonly cursorSegment: ISQLScriptSegment | undefined; readonly readonly: boolean; readonly editing: boolean; readonly isScriptEmpty: boolean; @@ -33,28 +29,21 @@ export interface ISQLEditorData { readonly value: string; readonly incomingValue?: string; readonly isExecutionAllowed: boolean; - readonly dataSource: ISqlDataSource | undefined; readonly onExecute: ISyncExecutor; readonly onSegmentExecute: ISyncExecutor; readonly onFormat: ISyncExecutor<[ISQLScriptSegment, string]>; - readonly onUpdate: ISyncExecutor; - readonly onMode: ISyncExecutor; /** displays if last getHintProposals call ended with limit */ readonly hintsLimitIsMet: boolean; updateParserScriptsDebounced(): Promise; setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void; - init(): void; - destruct(): void; setCursor(begin: number, end?: number): void; formatScript(): Promise; executeQuery(): Promise; executeQueryNewTab(): Promise; showExecutionPlan(): Promise; executeScript(): Promise; - switchEditing(): void; getHintProposals(position: number, simple: boolean): Promise; - getResolvedSegment(): Promise; executeQueryAction( segment: ISQLScriptSegment | undefined, action: (query: ISQLScriptSegment) => T | Promise, diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts deleted file mode 100644 index d446a47b9ca..00000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import type { ISyncContextLoader } from '@cloudbeaver/core-executor'; - -import type { ISQLScriptSegment } from '../SQLParser.js'; -import type { ISQLEditorData } from './ISQLEditorData.js'; - -export interface ISQLEditorMode { - activeSegment: ISQLScriptSegment | undefined; - activeSegmentMode: boolean; -} - -export const SQLEditorModeContext: ISyncContextLoader = function SQLEditorModeContext(context, data) { - const from = Math.min(data.cursor.anchor, data.cursor.head); - const to = Math.max(data.cursor.anchor, data.cursor.head); - - return { - activeSegment: data.parser.getSegment(from, to), - activeSegmentMode: false, - }; -}; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts index 3d449d135a8..4c08c544c2f 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts @@ -40,7 +40,7 @@ export function useActiveQuery(state: ISQLEditorData): void { return; } - const segment = await sqlEditorData.getResolvedSegment(); + const segment = await sqlEditorData.model.getResolvedSegment(); let query = data.update.query.trim(); if (segment) { @@ -65,7 +65,7 @@ export function useActiveQuery(state: ISQLEditorData): void { head: firstQueryPart.length, }); } else { - sqlEditorData.setScript(query, undefined, sqlEditorData.cursor); + sqlEditorData.setScript(query, undefined, sqlEditorData.model.cursor); } break; case 'append': diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 5eed008ea8a..f1deef242d5 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -5,32 +5,31 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, autorun, computed, type IReactionDisposer, observable, runInAction, untracked } from 'mobx'; -import { useEffect } from 'react'; +import { action, computed, type IReactionDisposer, observable } from 'mobx'; -import { ConfirmationDialog, useExecutor, useObservableRef } from '@cloudbeaver/core-blocks'; -import { ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections'; +import { ConfirmationDialog, getComputed, useExecutor, useObservableRef, useResource } from '@cloudbeaver/core-blocks'; +import { ConnectionDialectResource, ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { SyncExecutor } from '@cloudbeaver/core-executor'; import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; -import { createLastPromiseGetter, debounceAsync, type LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; +import { createLastPromiseGetter, type LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures.js'; -import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; -import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService.js'; +import type { ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; import { SqlDialectInfoService } from '../SqlDialectInfoService.js'; import { SqlEditorService } from '../SqlEditorService.js'; -import { type ISQLScriptSegment, SQLParser } from '../SQLParser.js'; +import { type ISQLScriptSegment } from '../SQLParser.js'; import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.js'; import { OUTPUT_LOGS_TAB_ID } from '../SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID.js'; import { SqlQueryService } from '../SqlResultTabs/SqlQueryService.js'; import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService.js'; import type { ISQLEditorData } from './ISQLEditorData.js'; -import { SQLEditorModeContext } from './SQLEditorModeContext.js'; import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js'; +import { SqlEditorModelService } from '../SqlEditorModel/SqlEditorModelService.js'; +import type { ISqlEditorModel } from '../SqlEditorModel/ISqlEditorModel.js'; interface ISQLEditorDataPrivate extends ISQLEditorData { readonly sqlDialectInfoService: SqlDialectInfoService; @@ -42,20 +41,15 @@ interface ISQLEditorDataPrivate extends ISQLEditorData { readonly sqlEditorSettingsService: SqlEditorSettingsService; readonly commonDialogService: CommonDialogService; readonly sqlResultTabsService: SqlResultTabsService; - readonly dataSource: ISqlDataSource | undefined; readonly getLastAutocomplete: LastPromiseGetter; readonly parseScript: LastPromiseGetter; - cursor: ISqlEditorCursor; readonlyState: boolean; executingScript: boolean; state: ISqlEditorTabState; reactionDisposer: IReactionDisposer | null; hintsLimitIsMet: boolean; - updateParserScripts(): Promise; loadDatabaseDataModels(): Promise; - getExecutingQuery(script: boolean): ISQLScriptSegment | undefined; - getSubQuery(): ISQLScriptSegment | undefined; } const MAX_HINTS_LIMIT = 200; @@ -69,16 +63,16 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const sqlExecutionPlanService = useService(SqlExecutionPlanService); const sqlResultTabsService = useService(SqlResultTabsService); const commonDialogService = useService(CommonDialogService); - const sqlDataSourceService = useService(SqlDataSourceService); const sqlEditorSettingsService = useService(SqlEditorSettingsService); + const sqlEditorModelService = useService(SqlEditorModelService); const data = useObservableRef( () => ({ - get dataSource(): ISqlDataSource | undefined { - return sqlDataSourceService.get(this.state.editorId); + get model(): ISqlEditorModel { + return sqlEditorModelService.getOrCreate(this.state); }, get dialect(): SqlDialectInfo | undefined { - const executionContext = this.dataSource?.executionContext; + const executionContext = this.model.dataSource?.executionContext; if (!executionContext) { return undefined; } @@ -86,25 +80,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return this.sqlDialectInfoService.getDialectInfo(createConnectionParam(executionContext.projectId, executionContext.connectionId)); }, - activeSegmentMode: { - activeSegment: undefined, - activeSegmentMode: false, - }, - - get activeSegment(): ISQLScriptSegment | undefined { - return this.activeSegmentMode.activeSegment; - }, - - get cursorSegment(): ISQLScriptSegment | undefined { - return this.parser.getSegment(this.cursor.anchor, -1); - }, - get readonly(): boolean { - return this.executingScript || this.readonlyState || !!this.dataSource?.isReadonly() || !this.editing; + return this.executingScript || this.readonlyState || !!this.model.dataSource?.isReadonly() || !this.editing; }, get editing(): boolean { - return this.dataSource?.isEditing() ?? false; + return this.model.dataSource?.isEditing() ?? false; }, get isScriptEmpty(): boolean { @@ -112,85 +93,47 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, get isDisabled(): boolean { - if (!this.dataSource?.executionContext || !this.dataSource.isLoaded()) { + if (!this.model.dataSource?.executionContext || !this.model.dataSource.isLoaded()) { return true; } - const context = this.connectionExecutionContextService.get(this.dataSource.executionContext.id); + const context = this.connectionExecutionContextService.get(this.model.dataSource.executionContext.id); return context?.executing || false; }, get isIncomingChanges(): boolean { - return this.dataSource?.isIncomingChanges ?? false; - }, - - get cursor(): ISqlEditorCursor { - return this.dataSource?.cursor ?? { anchor: 0, head: 0 }; + return this.model.dataSource?.isIncomingChanges ?? false; }, get value(): string { - return this.dataSource?.script ?? ''; + return this.model.dataSource?.script ?? ''; }, get incomingValue(): string | undefined { - return this.dataSource?.incomingScript; + return this.model.dataSource?.incomingScript; }, get isExecutionAllowed(): boolean { - return !!this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && this.sqlEditorSettingsService.scriptExecutionEnabled; + return !!this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && this.sqlEditorSettingsService.scriptExecutionEnabled; }, - onMode: new SyncExecutor(), onExecute: new SyncExecutor(), onSegmentExecute: new SyncExecutor(), - onUpdate: new SyncExecutor(), - parser: new SQLParser(), readonlyState: false, executingScript: false, reactionDisposer: null, hintsLimitIsMet: false, - init(): void { - if (this.reactionDisposer) { - return; - } - - this.parser.setScript(this.value); - - this.reactionDisposer = autorun(() => { - const executionContext = this.dataSource?.executionContext; - if (executionContext) { - const context = this.connectionExecutionContextService.get(executionContext.id)?.context; - if (context) { - const key = createConnectionParam(context.projectId, context.connectionId); - - untracked(() => { - this.sqlDialectInfoService.loadSqlDialectInfo(key).then(async () => { - try { - await this.updateParserScriptsDebounced(); - } catch {} - }); - }); - } - } - }); - }, - - destruct(): void { - this.reactionDisposer?.(); - }, - setCursor(begin: number, end = begin): void { - this.dataSource?.setCursor(begin, end); + this.model.dataSource?.setCursor(begin, end); }, getLastAutocomplete: createLastPromiseGetter(), - parseScript: createLastPromiseGetter(), getHintProposals: throttleAsync(async function getHintProposals(this: ISQLEditorDataPrivate, position, simple) { - const executionContext = this.dataSource?.executionContext; + const executionContext = this.model.dataSource?.executionContext; if (!executionContext) { return []; } @@ -211,13 +154,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, 300), async formatScript(): Promise { - if (this.isDisabled || this.isScriptEmpty || !this.dataSource?.executionContext) { + if (this.isDisabled || this.isScriptEmpty || !this.model.dataSource?.executionContext) { return; } - await this.updateParserScripts(); - const query = this.value; - const script = this.getExecutingQuery(false); + const script = await this.model.getResolvedSegment(); if (!script) { return; @@ -226,39 +167,37 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.onExecute.execute(true); try { this.readonlyState = true; - const formatted = await this.sqlDialectInfoService.formatScript(this.dataSource.executionContext, script.query); + const formatted = await this.sqlDialectInfoService.formatScript(this.model.dataSource.executionContext, script.query); - this.setScript(query.substring(0, script.begin) + formatted + query.substring(script.end)); - this.setCursor(script.begin + formatted.length); + const cursorAnchor = this.model.cursor.anchor; + this.setScript(this.value.substring(0, script.begin) + formatted + this.value.substring(script.end), 'format', { + anchor: cursorAnchor, + head: cursorAnchor, + }); } finally { this.readonlyState = false; } }, async executeQuery(): Promise { - const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isQuery = this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query); if (!isQuery || !this.isExecutionAllowed) { return; } - await this.updateParserScripts(); - const query = this.getSubQuery(); - try { - await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query => + await this.executeQueryAction(await this.executeQueryAction(this.model.cursorSegment, () => this.model.getResolvedSegment()), query => this.sqlQueryService.executeEditorQuery(this.state, query.query, false), ); } catch {} }, async loadDatabaseDataModels(): Promise { - const query = this.getExecutingQuery(true); - await this.executeQueryAction( - query, + this.model.cursorSegment, () => { - if (this.dataSource?.databaseModels.length) { + if (this.model.dataSource?.databaseModels.length) { this.sqlQueryService.initDatabaseDataModels(this.state); } }, @@ -268,43 +207,33 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async executeQueryNewTab(): Promise { - const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isQuery = this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query); if (!isQuery || !this.isExecutionAllowed) { return; } - await this.updateParserScripts(); - const query = this.getSubQuery(); - try { - await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query => + await this.executeQueryAction(await this.executeQueryAction(this.model.cursorSegment, () => this.model.getResolvedSegment()), query => this.sqlQueryService.executeEditorQuery(this.state, query.query, true), ); } catch {} }, async showExecutionPlan(): Promise { - const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isQuery = this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query); if (!isQuery || !this.isExecutionAllowed || !this.dialect?.supportsExplainExecutionPlan) { return; } - await this.updateParserScripts(); - const query = this.getSubQuery(); - try { - await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query => + await this.executeQueryAction(await this.executeQueryAction(this.model.cursorSegment, () => this.model.getResolvedSegment()), query => this.sqlExecutionPlanService.executeExecutionPlan(this.state, query.query), ); } catch {} }, - switchEditing(): void { - this.dataSource?.setEditing(!this.dataSource.isEditing()); - }, - async executeScript(): Promise { if (!this.isExecutionAllowed || this.isDisabled || this.isScriptEmpty) { return; @@ -336,8 +265,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.onExecute.execute(true); try { this.executingScript = true; - await this.updateParserScripts(); - const queries = this.parser.scripts; + await this.model.getResolvedSegment(); + const queries = this.model.parser.scripts; await this.sqlQueryService.executeQueries( this.state, @@ -363,41 +292,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void { - this.dataSource?.setScript(query, source, cursor); - }, - - updateParserScriptsDebounced: debounceAsync(async function updateParserScriptsThrottle() { - await data.updateParserScripts(); - }, 2000), - - async updateParserScripts() { - if (!this.dataSource?.hasFeature(ESqlDataSourceFeatures.script)) { - return; - } - const projectId = this.dataSource.executionContext?.projectId; - const connectionId = this.dataSource.executionContext?.connectionId; - const script = this.parser.actualScript; - - if (!projectId || !connectionId || !script) { - this.parser.setQueries([]); - this.onUpdate.execute(); - return; - } - - const { queries } = await this.parseScript([connectionId, script], async () => { - try { - return await this.sqlEditorService.parseSQLScript(projectId, connectionId, script); - } catch (exception: any) { - this.notificationService.logException(exception, 'Failed to parse SQL script'); - throw exception; - } - }); - - // check if script was changed while we were waiting for response - if (this.parser.actualScript === script) { - this.parser.setQueries(queries); - this.onUpdate.execute(); - } + this.model.dataSource?.setScript(query, source, cursor); }, async executeQueryAction( @@ -425,64 +320,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { throw exception; } }, - - getExecutingQuery(script: boolean): ISQLScriptSegment | undefined { - if (script) { - return this.parser.getScriptSegment(); - } - - return this.activeSegment; - }, - - async getResolvedSegment(): Promise { - const projectId = this.dataSource?.executionContext?.projectId; - const connectionId = this.dataSource?.executionContext?.connectionId; - - while (true) { - const currentScript = this.parser.actualScript; - // TODO: we updating parser scripts - // script may be changed this will lead to temporary wrong segments offsets - await data.updateParserScripts(); - if (currentScript !== this.parser.actualScript) { - continue; - } - - if (!projectId || !connectionId || this.cursor.anchor !== this.cursor.head) { - return this.getSubQuery(); - } - - if (this.activeSegmentMode.activeSegmentMode) { - return this.activeSegment; - } - - const result = await this.sqlEditorService.parseSQLQuery(projectId, connectionId, currentScript, this.cursor.anchor); - if (currentScript !== this.parser.actualScript) { - continue; - } - if (result.end === 0 && result.start === 0) { - return this.cursorSegment; - } - - // TODO: here we use parser that may be outdated and segment will return wrong value - const segment = this.parser.getSegment(result.start, result.end); - return segment; - } - }, - - getSubQuery(): ISQLScriptSegment | undefined { - const query = this.getExecutingQuery(false); - - if (!query) { - return undefined; - } - - query.query = query.query.trim(); - - return query; - }, setModeId(tabId: string): void { this.state.currentModeId = tabId; - this.onUpdate.execute(); }, }), { @@ -492,13 +331,10 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { executeQueryNewTab: action.bound, showExecutionPlan: action.bound, executeScript: action.bound, - switchEditing: action.bound, dialect: computed, isDisabled: computed, value: computed, readonly: computed, - cursor: computed, - activeSegmentMode: observable.ref, hintsLimitIsMet: observable.ref, readonlyState: observable, executingScript: observable, @@ -518,21 +354,19 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, ); - untracked(() => data.init()); - - useExecutor({ - executor: data.dataSource?.onSetScript, - handlers: [ - function setScript({ script }) { - data.parser.setScript(script); - data.updateParserScriptsDebounced().catch(() => {}); - data.onUpdate.execute(); - }, - ], + const key = getComputed(() => { + const executionContext = data.model.dataSource?.executionContext; + if (executionContext) { + const context = data.connectionExecutionContextService.get(executionContext.id)?.context; + if (context) { + return createConnectionParam(context.projectId, context.connectionId); + } + } + return null; }); useExecutor({ - executor: data.dataSource?.onDatabaseModelUpdate, + executor: data.model.dataSource?.onDatabaseModelUpdate, handlers: [ function updateDatabaseModels() { data.loadDatabaseDataModels(); @@ -540,34 +374,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { ], }); - useExecutor({ - executor: data.onUpdate, - handlers: [ - function updateActiveSegmentMode() { - // Probably we need to rework this logic - // we want to track active segment mode with mobx - // right now it's leads to bag when script changed from empty to not empty - // data.isLineScriptEmpty skips this change - const contexts = data.onMode.execute(data); - data.activeSegmentMode = contexts.getContext(SQLEditorModeContext); - }, - ], - }); - - useEffect(() => { - const subscription = autorun(() => { - const contexts = data.onMode.execute(data); - const activeSegmentMode = contexts.getContext(SQLEditorModeContext); - - runInAction(() => { - data.activeSegmentMode = activeSegmentMode; - }); - }); - - return subscription; - }, [data]); - - useEffect(() => () => data.destruct(), []); + useResource(useSqlEditor, ConnectionDialectResource, key); return data; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorModel/ISqlEditorModel.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorModel/ISqlEditorModel.ts new file mode 100644 index 00000000000..2979d07402f --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorModel/ISqlEditorModel.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { Disposable } from '@cloudbeaver/core-di'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import type { IDataQueryOptions, QueryDataSource } from '../QueryDataSource.js'; +import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; +import type { ISQLScriptSegment, SQLParser } from '../SQLParser.js'; +import type { SyncExecutor } from '@cloudbeaver/core-executor'; + +export interface ISqlEditorModel> extends Disposable { + readonly cursor: ISqlEditorCursor; + readonly cursorSegment: ISQLScriptSegment | undefined; + readonly parser: SQLParser; + readonly state: ISqlEditorTabState | null; + readonly dataSource: ISqlDataSource | undefined; + + readonly onUpdate: SyncExecutor; + + setState(state: ISqlEditorTabState): void; + getResolvedSegment(): Promise; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModel.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModel.ts new file mode 100644 index 00000000000..808feab9384 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModel.ts @@ -0,0 +1,168 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Disposable, injectable } from '@cloudbeaver/core-di'; +import type { ISqlEditorModel } from './ISqlEditorModel.js'; +import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService.js'; +import type { QueryDataSource } from '../QueryDataSource.js'; +import type { ISetScriptData, ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import { SQLParser, type IQueryInfo, type ISQLScriptSegment } from '../SQLParser.js'; +import { SyncExecutor } from '@cloudbeaver/core-executor'; +import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures.js'; +import { SqlEditorService } from '../SqlEditorService.js'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { computed, makeObservable, observable, reaction } from 'mobx'; +import { debouncePromise, memoizeLast } from '@dbeaver/js-helpers'; + +const ZERO_CURSOR: ISqlEditorCursor = { anchor: 0, head: 0 }; + +@injectable(() => [SqlDataSourceService, SqlEditorService, NotificationService]) +export class SqlEditorModel extends Disposable implements ISqlEditorModel { + state: ISqlEditorTabState | null; + get cursor(): ISqlEditorCursor { + return this.dataSource?.cursor ?? ZERO_CURSOR; + } + get cursorSegment(): ISQLScriptSegment | undefined { + const from = Math.min(this.cursor.anchor, this.cursor.head); + const to = Math.max(this.cursor.anchor, this.cursor.head); + + return this.parser.getSegment(from, to); + } + get dataSource(): ISqlDataSource | undefined { + if (this.state) { + return this.sqlDataSourceService.get(this.state.editorId); + } + + return undefined; + } + readonly parser: SQLParser; + + readonly onUpdate: SyncExecutor; + + private parserFn: (script: string, projectId: string, connectionId: string) => Promise; + private getSqlQueryAtPos: typeof this.sqlEditorService.parseSQLQuery; + private dataSourceListener; + private updateParserScriptsDebounced: typeof this.updateParserScripts; + + constructor( + private readonly sqlDataSourceService: SqlDataSourceService, + private readonly sqlEditorService: SqlEditorService, + private readonly notificationService: NotificationService, + ) { + super(); + this.state = null; + this.parser = new SQLParser(); + this.onUpdate = new SyncExecutor(); + this.parserFn = (script, projectId, connectionId) => + this.sqlEditorService.parseSQLScript(projectId, connectionId, script).then(({ queries }) => queries); + + this.getSqlQueryAtPos = memoizeLast(this.sqlEditorService.parseSQLQuery.bind(this.sqlEditorService)); + this.syncScriptWithDataSource = this.syncScriptWithDataSource.bind(this); + + this.updateParserScriptsDebounced = debouncePromise(() => this.updateParserScripts(), 2000); + + this.dataSourceListener = reaction( + () => this.dataSource, + (dataSource, prev) => { + prev?.onSetScript.removeHandler(this.syncScriptWithDataSource); + dataSource?.onSetScript.addHandler(this.syncScriptWithDataSource); + this.syncScriptWithDataSource({ script: dataSource?.script || '', cursor: dataSource?.cursor }); + }, + { fireImmediately: true }, + ); + + makeObservable(this, { + state: observable.shallow, + dataSource: computed, + cursor: computed, + }); + } + + setState(state: ISqlEditorTabState | null): void { + this.state = state; + this.onUpdate.execute(); + } + + async getResolvedSegment(): Promise { + const projectId = this.dataSource?.executionContext?.projectId; + const connectionId = this.dataSource?.executionContext?.connectionId; + + for (let attempts = 0; attempts < 5; attempts++) { + const currentScript = this.parser.actualScript; + + // TODO: we updating parser scripts + // script may be changed this will lead to temporary wrong segments offsets + await this.updateParserScripts(); + if (currentScript !== this.parser.actualScript) { + continue; + } + + if (!projectId || !connectionId || this.cursor.anchor !== this.cursor.head) { + return this.getQuery(); + } + + const result = await this.getSqlQueryAtPos(projectId, connectionId, currentScript, this.cursor.anchor); + + if (currentScript !== this.parser.actualScript) { + continue; + } + + if (result.end === 0 && result.start === 0) { + return this.cursorSegment; + } + + return this.parser.getSegment(result.start, result.end); + } + + return undefined; + } + + private async updateParserScripts() { + const projectId = this.dataSource?.executionContext?.projectId; + const connectionId = this.dataSource?.executionContext?.connectionId; + const script = this.parser.actualScript; + + if (!projectId || !connectionId || !script || !this.dataSource?.hasFeature(ESqlDataSourceFeatures.script)) { + this.parser.parse(() => []); + this.onUpdate.execute(); + return; + } + + try { + await this.parser.parse(this.parserFn, projectId, connectionId); + this.onUpdate.execute(); + } catch (exception: any) { + this.notificationService.logException(exception, 'Failed to parse SQL script'); + throw exception; + } + } + + private getQuery(): ISQLScriptSegment | undefined { + const query = this.cursorSegment; + + if (!query) { + return undefined; + } + + query.query = query.query.trim(); + + return query; + } + + private syncScriptWithDataSource({ script }: ISetScriptData) { + this.parser.setScript(script); + this.updateParserScriptsDebounced().catch(() => {}); + this.onUpdate.execute(); + } + + protected override dispose(): Promise | void { + this.dataSourceListener(); + this.dataSource?.onSetScript.removeHandler(this.syncScriptWithDataSource); + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModelService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModelService.ts new file mode 100644 index 00000000000..74a5acc1a35 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorModel/SqlEditorModelService.ts @@ -0,0 +1,70 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Disposable, injectable, IServiceProvider } from '@cloudbeaver/core-di'; +import type { ISqlEditorModel } from './ISqlEditorModel.js'; +import { makeObservable, observable } from 'mobx'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import { SqlEditorModel } from './SqlEditorModel.js'; +import type { QueryDataSource } from '../QueryDataSource.js'; + +@injectable(() => [IServiceProvider]) +export class SqlEditorModelService extends Disposable { + private readonly models: Map>; + + constructor(private readonly serviceProvider: IServiceProvider) { + super(); + this.models = new Map(); + + makeObservable(this, { + models: observable.shallow, + }); + } + + getOrCreate(state: ISqlEditorTabState): ISqlEditorModel { + let model = this.models.get(state.editorId); + if (!model) { + model = this.serviceProvider.getService(SqlEditorModel); + + model.setState(state); + this.models.set(state.editorId, model); + } + + return model; + } + + async destroy(editorId: string): Promise { + const model = this.models.get(editorId); + + if (model) { + await model[Symbol.asyncDispose]?.().catch(e => { + console.error(e); + }); + } + + this.models.delete(editorId); + } + + async destroySilent(editorId: string): Promise { + await this.destroy(editorId); + } + + async unload(editorId: string): Promise { + await this.destroy(editorId); + } + + protected override async dispose(): Promise { + for (const model of this.models.values()) { + await model[Symbol.asyncDispose]?.().catch(e => { + console.error(e); + }); + } + + this.models.clear(); + } +} diff --git a/webapp/packages/plugin-sql-editor/src/index.ts b/webapp/packages/plugin-sql-editor/src/index.ts index b371a69c732..17a0aad1d44 100644 --- a/webapp/packages/plugin-sql-editor/src/index.ts +++ b/webapp/packages/plugin-sql-editor/src/index.ts @@ -34,13 +34,13 @@ export * from './SqlEditor/ISQLEditorData.js'; export * from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.js'; export * from './SqlEditor/SQL_EDITOR_ACTIONS_MENU.js'; export * from './SqlEditor/SQL_EDITOR_TOOLS_MENU.js'; -export * from './SqlEditor/SQLEditorModeContext.js'; export * from './SqlEditor/SqlEditorStateContext.js'; export * from './SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID.js'; export * from './SqlResultTabs/SqlResultTabsService.js'; export * from './SqlResultTabs/OutputLogs/OutputLogsEventHandler.js'; export * from './SqlResultTabs/OutputLogs/OutputLogsResource.js'; export * from './SqlResultTabs/OutputLogs/OutputLogsService.js'; +export * from './SqlEditorModel/SqlEditorModelService.js'; export * from './DATA_CONTEXT_SQL_EDITOR_STATE.js'; export * from './getSqlEditorName.js'; export * from './QueryDataSource.js'; diff --git a/webapp/packages/plugin-sql-editor/src/module.ts b/webapp/packages/plugin-sql-editor/src/module.ts index c814d0a588b..ce662b8c498 100644 --- a/webapp/packages/plugin-sql-editor/src/module.ts +++ b/webapp/packages/plugin-sql-editor/src/module.ts @@ -25,12 +25,15 @@ import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; import { LocalStorageSqlDataSourceBootstrap } from './SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.js'; import { MenuBootstrap } from './MenuBootstrap.js'; import { LocaleService } from './LocaleService.js'; +import { SqlEditorModel } from './SqlEditorModel/SqlEditorModel.js'; +import { SqlEditorModelService } from './SqlEditorModel/SqlEditorModelService.js'; ModuleRegistry.add({ name: '@cloudbeaver/plugin-sql-editor', configure: serviceCollection => { serviceCollection + .addTransient(SqlEditorModel) .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, proxy(LocalStorageSqlDataSourceBootstrap)) .addSingleton(Bootstrap, proxy(MenuBootstrap)) @@ -54,6 +57,7 @@ ModuleRegistry.add({ .addSingleton(SqlDialectInfoService) .addSingleton(SqlDataSourceService) .addSingleton(LocalStorageSqlDataSourceBootstrap) + .addSingleton(SqlEditorModelService) .addSingleton(MenuBootstrap); }, }); diff --git a/webapp/packages/plugin-sql-editor/tsconfig.json b/webapp/packages/plugin-sql-editor/tsconfig.json index 268e9287810..61e16ca0b0a 100644 --- a/webapp/packages/plugin-sql-editor/tsconfig.json +++ b/webapp/packages/plugin-sql-editor/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../../common-typescript/@dbeaver/cli" }, + { + "path": "../../common-typescript/@dbeaver/js-helpers" + }, { "path": "../core-authentication" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index d20143ff317..f36a794b243 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -3869,6 +3869,7 @@ __metadata: "@cloudbeaver/plugin-navigation-tabs": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@dbeaver/cli": "workspace:*" + "@dbeaver/js-helpers": "workspace:^" "@dbeaver/react-tests": "workspace:^" "@types/react": "npm:^19" mobx: "npm:^6" @@ -4543,6 +4544,9 @@ __metadata: "@dbeaver/cli": "workspace:^" "@dbeaver/tsconfig": "workspace:^" async-mutex: "npm:^0" + p-debounce: "npm:^4.0.0" + p-memoize: "npm:^8.0.0" + quick-lru: "npm:^7.1.0" rimraf: "npm:^6" typescript: "npm:^5" vitest: "npm:^3" @@ -14532,6 +14536,13 @@ __metadata: languageName: node linkType: hard +"mimic-function@npm:^5.0.1": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: 10c0/f3d9464dd1816ecf6bdf2aec6ba32c0728022039d992f178237d8e289b48764fee4131319e72eedd4f7f094e22ded0af836c3187a7edc4595d28dd74368fd81d + languageName: node + linkType: hard + "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -15109,6 +15120,13 @@ __metadata: languageName: node linkType: hard +"p-debounce@npm:^4.0.0": + version: 4.0.0 + resolution: "p-debounce@npm:4.0.0" + checksum: 10c0/2d50bcb53659ad4345ef59b220581ccb40d581528949d8c63547c22b418e9a92f5138617eb2f41cd03bed293ee78a3dbb9b2795f572a4044434b89f058ccdfa2 + languageName: node + linkType: hard + "p-limit@npm:3.1.0, p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -15161,6 +15179,16 @@ __metadata: languageName: node linkType: hard +"p-memoize@npm:^8.0.0": + version: 8.0.0 + resolution: "p-memoize@npm:8.0.0" + dependencies: + mimic-function: "npm:^5.0.1" + type-fest: "npm:^4.41.0" + checksum: 10c0/cd4333c42f67dee273bff855e18cb974fac61136605ae06c30e779f71fb0e390fbab5c87160dfd8b5f905154b41c3f41bcc2e9870b2ce9b5e5c2c912012261ca + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -15716,6 +15744,13 @@ __metadata: languageName: node linkType: hard +"quick-lru@npm:^7.1.0": + version: 7.1.0 + resolution: "quick-lru@npm:7.1.0" + checksum: 10c0/ef2df33368a66e3d3b32a42c523fcd0939c03f6192ec7f29db36179e22f015d2c75790aa9ab530c719a0dd3fb510f972bc66acca4e92bc9f01f92f0e6c2084f2 + languageName: node + linkType: hard + "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -17988,7 +18023,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.21.0, type-fest@npm:^4.26.1": +"type-fest@npm:^4.21.0, type-fest@npm:^4.26.1, type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 From c8ef08ba43db7b23a4acab66a13fa49f06c9d39f Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 16 Sep 2025 22:50:03 +0800 Subject: [PATCH 2/6] fix: state sync --- webapp/packages/core-blocks/src/useSync.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/webapp/packages/core-blocks/src/useSync.ts b/webapp/packages/core-blocks/src/useSync.ts index ca303d50d58..d5a1755ca4f 100644 --- a/webapp/packages/core-blocks/src/useSync.ts +++ b/webapp/packages/core-blocks/src/useSync.ts @@ -14,7 +14,7 @@ interface ISyncHook { markOutdated(): void; } -export function useSync(callback: () => void | Promise): ISyncHook { +export function useSync(callback: () => void | Promise, canSync = true): ISyncHook { const [syncMutex] = useState(() => new mutex.Mutex()); const [outdated, setOutdated] = useState(false); const [delayedOutdated, setDelayedOutdated] = useState(false); @@ -36,21 +36,24 @@ export function useSync(callback: () => void | Promise): ISyncHook { } } + const data = useObjectRef(() => ({ + markOutdated, + markUpdated, + })); + useEffect(() => { - if (outdated && !syncMutex.isLocked()) { + if (outdated && !syncMutex.isLocked() && canSync) { syncMutex .runExclusive(async () => { try { await callback(); } finally { - markUpdated(); + data.markUpdated(); } }) .catch(error => console.error(error)); } }); - return useObjectRef(() => ({ - markOutdated, - })); + return data; } From 7854d5294b05ce2d37e9bbb8208df6e951447b2c Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 17 Sep 2025 18:02:24 +0800 Subject: [PATCH 3/6] fix: segment resolution --- .../plugin-sql-editor/src/SqlEditor/useSqlEditor.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index f1deef242d5..e39ef0f1741 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -187,13 +187,13 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { } try { - await this.executeQueryAction(await this.executeQueryAction(this.model.cursorSegment, () => this.model.getResolvedSegment()), query => - this.sqlQueryService.executeEditorQuery(this.state, query.query, false), - ); + const segment = await this.model.getResolvedSegment(); + await this.executeQueryAction(segment, query => this.sqlQueryService.executeEditorQuery(this.state, query.query, false)); } catch {} }, async loadDatabaseDataModels(): Promise { + await this.model.getResolvedSegment(); await this.executeQueryAction( this.model.cursorSegment, () => { @@ -228,9 +228,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { } try { - await this.executeQueryAction(await this.executeQueryAction(this.model.cursorSegment, () => this.model.getResolvedSegment()), query => - this.sqlExecutionPlanService.executeExecutionPlan(this.state, query.query), - ); + const segment = await this.model.getResolvedSegment(); + await this.executeQueryAction(segment, query => this.sqlExecutionPlanService.executeExecutionPlan(this.state, query.query)); } catch {} }, From c0e2f8f7fb2938982e9dd04a6e51ac086cb3853a Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 17 Sep 2025 18:56:59 +0800 Subject: [PATCH 4/6] chore: add comment --- webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index e39ef0f1741..87176fc8186 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -193,6 +193,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async loadDatabaseDataModels(): Promise { + // force script parsing, cursorSegment depends on it await this.model.getResolvedSegment(); await this.executeQueryAction( this.model.cursorSegment, From f1e9ba22c64e01df1d709e05db014103a27cd9ea Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 18 Sep 2025 23:48:05 +0800 Subject: [PATCH 5/6] refactor: remove old code, add todo about problem --- .../SQLCodeEditor/SQLCodeEditorLoader.tsx | 19 ++----- .../src/SQLEditor/useSqlDialectExtension.ts | 3 +- .../src/SqlDialectInfoService.ts | 21 +------- .../src/SqlEditor/SqlEditorLoader.tsx | 16 ++---- .../src/SqlEditor/useSqlEditor.ts | 50 ++++++++----------- .../src/SqlGenerators/GeneratedSqlDialog.tsx | 33 +++--------- 6 files changed, 38 insertions(+), 104 deletions(-) diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx index deb626b62de..51003c245fb 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx @@ -1,23 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { observer } from 'mobx-react-lite'; -import { forwardRef } from 'react'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks'; -import type { IDefaultExtensions, IEditorProps, IEditorRef } from '@cloudbeaver/plugin-codemirror6'; - -const loader = createComplexLoader(async function loader() { - const { SQLCodeEditor } = await import('./SQLCodeEditor.js'); - return { SQLCodeEditor }; -}); - -export const SQLCodeEditorLoader = observer( - forwardRef(function SQLCodeEditorLoader(props, ref) { - return {({ SQLCodeEditor }) => }; - }), -); +export const SQLCodeEditorLoader = importLazyComponent(() => import('./SQLCodeEditor.js').then(m => m.SQLCodeEditor)); diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts index 26a4665fb07..32593ba0d89 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ const codemirrorComplexLoader = createComplexLoader(() => import('@cloudbeaver/p const SQL_EDITOR_COMPARTMENT = new Compartment(); export function useSqlDialectExtension(dialectInfo: SqlDialectInfo | undefined): [Compartment, Extension] { + //TODO: probably we need to refactor it to lazy approach without triggering suspense const { SQLDialect, SQL_EDITOR } = useComplexLoader(codemirrorComplexLoader); const loader = getDialectLoader(dialectInfo?.name); const dialect = useComplexLoader(loader); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts b/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts index eff70592b5b..87df9c0578a 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts @@ -1,14 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ConnectionDialectResource, type IConnectionExecutionContextInfo, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConnectionDialectResource, type IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import type { SqlDialectInfo } from '@cloudbeaver/core-sdk'; @injectable(() => [ConnectionDialectResource, NotificationService]) export class SqlDialectInfoService { @@ -25,20 +24,4 @@ export class SqlDialectInfoService { } return query; } - - getDialectInfo(key: IConnectionInfoParams): SqlDialectInfo | undefined { - return this.connectionDialectResource.get(key); - } - - async loadSqlDialectInfo(key: IConnectionInfoParams): Promise { - if (!this.connectionDialectResource.has(key)) { - try { - return this.connectionDialectResource.load(key); - } catch (error: any) { - this.notificationService.logException(error, 'Failed to load SqlDialectInfo'); - } - } - - return this.getDialectInfo(key); - } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx index 1734a2239e3..f731c2c91d8 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx @@ -1,21 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { observer } from 'mobx-react-lite'; -import { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import type { ISqlEditorProps } from './ISqlEditorProps.js'; - -const loader = createComplexLoader(async function loader() { - const { SqlEditor } = await import('./SqlEditor.js'); - return { SqlEditor }; -}); - -export const SqlEditorLoader = observer(function SqlEditorLoader(props) { - return {({ SqlEditor }) => }; -}); +export const SqlEditorLoader = importLazyComponent(() => import('./SqlEditor.js').then(m => m.SqlEditor)); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 87176fc8186..6693e912f9b 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -13,7 +13,7 @@ import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { SyncExecutor } from '@cloudbeaver/core-executor'; -import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; +import type { SqlCompletionProposal, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; import { createLastPromiseGetter, type LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; @@ -29,7 +29,6 @@ import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService.js'; import type { ISQLEditorData } from './ISQLEditorData.js'; import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js'; import { SqlEditorModelService } from '../SqlEditorModel/SqlEditorModelService.js'; -import type { ISqlEditorModel } from '../SqlEditorModel/ISqlEditorModel.js'; interface ISQLEditorDataPrivate extends ISQLEditorData { readonly sqlDialectInfoService: SqlDialectInfoService; @@ -66,20 +65,22 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const sqlEditorSettingsService = useService(SqlEditorSettingsService); const sqlEditorModelService = useService(SqlEditorModelService); - const data = useObservableRef( - () => ({ - get model(): ISqlEditorModel { - return sqlEditorModelService.getOrCreate(this.state); - }, - get dialect(): SqlDialectInfo | undefined { - const executionContext = this.model.dataSource?.executionContext; - if (!executionContext) { - return undefined; - } + const model = sqlEditorModelService.getOrCreate(state); - return this.sqlDialectInfoService.getDialectInfo(createConnectionParam(executionContext.projectId, executionContext.connectionId)); - }, + const key = getComputed(() => { + const executionContext = model.dataSource?.executionContext; + if (executionContext) { + const context = connectionExecutionContextService.get(executionContext.id)?.context; + if (context) { + return createConnectionParam(context.projectId, context.connectionId); + } + } + return null; + }); + const connectionDialectLoader = useResource(useSqlEditor, ConnectionDialectResource, key); + const data = useObservableRef( + () => ({ get readonly(): boolean { return this.executingScript || this.readonlyState || !!this.model.dataSource?.isReadonly() || !this.editing; }, @@ -331,10 +332,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { executeQueryNewTab: action.bound, showExecutionPlan: action.bound, executeScript: action.bound, - dialect: computed, isDisabled: computed, value: computed, readonly: computed, + state: observable.ref, + model: observable.ref, + dialect: observable.ref, hintsLimitIsMet: observable.ref, readonlyState: observable, executingScript: observable, @@ -342,6 +345,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, { state, + model, + dialect: connectionDialectLoader.tryGetData, connectionExecutionContextService, sqlQueryService, sqlDialectInfoService, @@ -354,19 +359,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, ); - const key = getComputed(() => { - const executionContext = data.model.dataSource?.executionContext; - if (executionContext) { - const context = data.connectionExecutionContextService.get(executionContext.id)?.context; - if (context) { - return createConnectionParam(context.projectId, context.connectionId); - } - } - return null; - }); - useExecutor({ - executor: data.model.dataSource?.onDatabaseModelUpdate, + executor: model.dataSource?.onDatabaseModelUpdate, handlers: [ function updateDatabaseModels() { data.loadDatabaseDataModels(); @@ -374,7 +368,5 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { ], }); - useResource(useSqlEditor, ConnectionDialectResource, key); - return data; } diff --git a/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx b/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx index 83c86acbabe..acd05d4d95a 100644 --- a/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx +++ b/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { computed, observable } from 'mobx'; +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; @@ -21,15 +21,15 @@ import { useClipboard, useErrorDetails, useObservableRef, + useResource, useS, useTranslate, } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import { ConnectionDialectResource, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import type { DialogComponentProps } from '@cloudbeaver/core-dialogs'; -import { GQLErrorCatcher, type SqlDialectInfo } from '@cloudbeaver/core-sdk'; +import { GQLErrorCatcher } from '@cloudbeaver/core-sdk'; import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; -import { SqlDialectInfoService } from '@cloudbeaver/plugin-sql-editor'; import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new'; import style from './GeneratedSqlDialog.module.css'; @@ -45,7 +45,6 @@ export const GeneratedSqlDialog = observer>(functi const copy = useClipboard(); const styles = useS(style); - const sqlDialectInfoService = useService(SqlDialectInfoService); const sqlGeneratorsResource = useService(SqlGeneratorsResource); const connectionInfoResource = useService(ConnectionInfoResource); const connection = connectionInfoResource.getConnectionForNode(payload.pathId); @@ -55,13 +54,6 @@ export const GeneratedSqlDialog = observer>(functi query: '', loading: true, error: new GQLErrorCatcher(), - get dialect(): SqlDialectInfo | undefined { - if (!this.connection?.connected) { - return; - } - - return this.sqlDialectInfoService.getDialectInfo(createConnectionParam(this.connection)); - }, async load() { this.error.clear(); @@ -78,12 +70,12 @@ export const GeneratedSqlDialog = observer>(functi query: observable.ref, loading: observable.ref, connection: observable.ref, - dialect: computed, }, - { connection, sqlDialectInfoService }, + { connection }, ); + const connectionDialectResource = useResource(GeneratedSqlDialog, ConnectionDialectResource, connection ? createConnectionParam(connection) : null); + const sqlDialect = useSqlDialectExtension(connectionDialectResource.data); - const sqlDialect = useSqlDialectExtension(state.dialect); const extensions = useCodemirrorExtensions(); extensions.set(...sqlDialect); const error = useErrorDetails(state.error.exception); @@ -92,17 +84,6 @@ export const GeneratedSqlDialog = observer>(functi state.load(); }, []); - useEffect(() => { - if (!connection) { - return; - } - - sqlDialectInfoService.loadSqlDialectInfo(createConnectionParam(connection)).catch(exception => { - console.error(exception); - console.warn(`Can't get dialect for connection: '${connection.id}'. Default dialect will be used`); - }); - }); - return ( From 1e7737ea56ef8830b084950281f2363930fed221 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 23 Sep 2025 18:41:26 +0800 Subject: [PATCH 6/6] fix: yarn.lock --- webapp/yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 3c80ef2f803..c7b26292879 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4012,7 +4012,7 @@ __metadata: "@cloudbeaver/plugin-navigation-tabs": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@dbeaver/cli": "workspace:*" - "@dbeaver/js-helpers": "workspace:*" + "@dbeaver/js-helpers": "workspace:^" "@dbeaver/react-tests": "workspace:*" "@types/react": "npm:^19" mobx: "npm:^6" @@ -4630,7 +4630,7 @@ __metadata: languageName: unknown linkType: soft -"@dbeaver/js-helpers@workspace:*, @dbeaver/js-helpers@workspace:common-typescript/@dbeaver/js-helpers": +"@dbeaver/js-helpers@workspace:*, @dbeaver/js-helpers@workspace:^, @dbeaver/js-helpers@workspace:common-typescript/@dbeaver/js-helpers": version: 0.0.0-use.local resolution: "@dbeaver/js-helpers@workspace:common-typescript/@dbeaver/js-helpers" dependencies: