dbeaver/pro#6327 refactor: sql editor & vqb (#3742)

* dbeaver/pro#6327 refactor: sql editor & vqb

* fix: state sync

* fix: segment resolution

* chore: add comment

* refactor: remove old code, add todo about problem

* fix: yarn.lock

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Alexey Potsetsuev
2025-09-23 18:45:45 +08:00
committed by GitHub
co-authored by mr-anton-t Evgenia
parent 0fe432f45c
commit 4f53ece164
29 changed files with 558 additions and 434 deletions
@@ -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"
}
}
@@ -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';
@@ -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';
@@ -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<T extends (...a: any[]) => Promise<any>>(fn: T) {
return pMemoize(fn, {
cache: new QuickLRU({ maxSize: 5 }),
cacheKey,
});
}
+1
View File
@@ -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';
@@ -0,0 +1,59 @@
/*
* 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<void>, canSync = true): 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);
}
}
const data = useObjectRef(() => ({
markOutdated,
markUpdated,
}));
useEffect(() => {
if (outdated && !syncMutex.isLocked() && canSync) {
syncMutex
.runExclusive(async () => {
try {
await callback();
} finally {
data.markUpdated();
}
})
.catch(error => console.error(error));
}
});
return data;
}
@@ -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<ISqlEditorTabState>[] {
@@ -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<ISqlEditorTabState>): Promise<boolean> {
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<ISqlEditorTabState>) {
await this.sqlEditorModelService.unload(editorTab.handlerState.editorId);
await this.sqlDataSourceService.unload(editorTab.handlerState.editorId);
this.sqlResultTabsService.removeResultTabs(editorTab.handlerState);
}
private async handleTabCloseSilent(editorTab: ITab<ISqlEditorTabState>) {
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<ISqlEditorTabState>) {
await this.sqlEditorModelService.destroy(editorTab.handlerState.editorId);
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.executionContext) {
@@ -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<IEditorProps & IDefaultExtensions, IEditorRef>(
forwardRef(function SQLCodeEditorLoader(props, ref) {
return <ComplexLoader loader={loader}>{({ SQLCodeEditor }) => <SQLCodeEditor {...props} ref={ref} />}</ComplexLoader>;
}),
);
export const SQLCodeEditorLoader = importLazyComponent(() => import('./SQLCodeEditor.js').then(m => m.SQLCodeEditor));
@@ -86,11 +86,11 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps>
});
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<ISqlEditorModeProps>
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}
@@ -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,
});
}
@@ -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],
});
@@ -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);
@@ -45,6 +45,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",
@@ -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();
@@ -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, '_scripts' | 'script'>(this, {
actualScript: computed,
@@ -55,8 +59,36 @@ export class SQLParser {
});
}
parse<Args extends any[], TResult extends Promise<IQueryInfo[]> | IQueryInfo[]>(
parser: (script: string, ...args: Args) => TResult,
...args: Args
): TResult extends Promise<any> ? Promise<void> : 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<ISQLScriptSegment>(query => ({
query: this.script.substring(query.start, query.end),
query: this.actualScript.substring(query.start, query.end),
begin: query.start,
end: query.end,
}));
@@ -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<SqlDialectInfo | undefined> {
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);
}
}
@@ -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<boolean>;
readonly onSegmentExecute: ISyncExecutor<ISegmentExecutionData>;
readonly onFormat: ISyncExecutor<[ISQLScriptSegment, string]>;
readonly onUpdate: ISyncExecutor;
readonly onMode: ISyncExecutor<ISQLEditorData>;
/** displays if last getHintProposals call ended with limit */
readonly hintsLimitIsMet: boolean;
updateParserScriptsDebounced(): Promise<void>;
setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void;
init(): void;
destruct(): void;
setCursor(begin: number, end?: number): void;
formatScript(): Promise<void>;
executeQuery(): Promise<void>;
executeQueryNewTab(): Promise<void>;
showExecutionPlan(): Promise<void>;
executeScript(): Promise<void>;
switchEditing(): void;
getHintProposals(position: number, simple: boolean): Promise<SQLProposal[]>;
getResolvedSegment(): Promise<ISQLScriptSegment | undefined>;
executeQueryAction<T>(
segment: ISQLScriptSegment | undefined,
action: (query: ISQLScriptSegment) => T | Promise<T>,
@@ -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<ISQLEditorMode, ISQLEditorData> = 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,
};
};
@@ -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<ISqlEditorProps>(function SqlEditorLoader(props) {
return <ComplexLoader loader={loader}>{({ SqlEditor }) => <SqlEditor {...props} />}</ComplexLoader>;
});
export const SqlEditorLoader = importLazyComponent(() => import('./SqlEditor.js').then(m => m.SqlEditor));
@@ -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':
@@ -5,32 +5,30 @@
* 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 type { SqlCompletionProposal, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk';
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';
interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly sqlDialectInfoService: SqlDialectInfoService;
@@ -42,20 +40,15 @@ interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly sqlEditorSettingsService: SqlEditorSettingsService;
readonly commonDialogService: CommonDialogService;
readonly sqlResultTabsService: SqlResultTabsService;
readonly dataSource: ISqlDataSource | undefined;
readonly getLastAutocomplete: LastPromiseGetter<SqlCompletionProposal[]>;
readonly parseScript: LastPromiseGetter<SqlScriptInfoFragment>;
cursor: ISqlEditorCursor;
readonlyState: boolean;
executingScript: boolean;
state: ISqlEditorTabState;
reactionDisposer: IReactionDisposer | null;
hintsLimitIsMet: boolean;
updateParserScripts(): Promise<void>;
loadDatabaseDataModels(): Promise<void>;
getExecutingQuery(script: boolean): ISQLScriptSegment | undefined;
getSubQuery(): ISQLScriptSegment | undefined;
}
const MAX_HINTS_LIMIT = 200;
@@ -69,42 +62,31 @@ 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 model = sqlEditorModelService.getOrCreate(state);
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<ISQLEditorDataPrivate>(
() => ({
get dataSource(): ISqlDataSource | undefined {
return sqlDataSourceService.get(this.state.editorId);
},
get dialect(): SqlDialectInfo | undefined {
const executionContext = this.dataSource?.executionContext;
if (!executionContext) {
return undefined;
}
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 +94,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 +155,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
}, 300),
async formatScript(): Promise<void> {
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 +168,38 @@ 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<void> {
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 =>
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<void> {
const query = this.getExecutingQuery(true);
// force script parsing, cursorSegment depends on it
await this.model.getResolvedSegment();
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 +209,32 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async executeQueryNewTab(): Promise<void> {
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<void> {
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 =>
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 {}
},
switchEditing(): void {
this.dataSource?.setEditing(!this.dataSource.isEditing());
},
async executeScript(): Promise<void> {
if (!this.isExecutionAllowed || this.isDisabled || this.isScriptEmpty) {
return;
@@ -336,8 +266,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 +293,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<T>(
@@ -425,64 +321,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<ISQLScriptSegment | undefined> {
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 +332,12 @@ 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,
state: observable.ref,
model: observable.ref,
dialect: observable.ref,
hintsLimitIsMet: observable.ref,
readonlyState: observable,
executingScript: observable,
@@ -506,6 +345,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
{
state,
model,
dialect: connectionDialectLoader.tryGetData,
connectionExecutionContextService,
sqlQueryService,
sqlDialectInfoService,
@@ -518,21 +359,8 @@ 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();
},
],
});
useExecutor({
executor: data.dataSource?.onDatabaseModelUpdate,
executor: model.dataSource?.onDatabaseModelUpdate,
handlers: [
function updateDatabaseModels() {
data.loadDatabaseDataModels();
@@ -540,34 +368,5 @@ 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(), []);
return data;
}
@@ -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<TDataSource extends QueryDataSource = QueryDataSource<IDataQueryOptions>> extends Disposable {
readonly cursor: ISqlEditorCursor;
readonly cursorSegment: ISQLScriptSegment | undefined;
readonly parser: SQLParser;
readonly state: ISqlEditorTabState | null;
readonly dataSource: ISqlDataSource<TDataSource> | undefined;
readonly onUpdate: SyncExecutor;
setState(state: ISqlEditorTabState): void;
getResolvedSegment(): Promise<ISQLScriptSegment | undefined>;
}
@@ -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<TDataSource extends QueryDataSource> extends Disposable implements ISqlEditorModel<TDataSource> {
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<TDataSource> | 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<IQueryInfo[]>;
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<ISQLScriptSegment | undefined> {
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> | void {
this.dataSourceListener();
this.dataSource?.onSetScript.removeHandler(this.syncScriptWithDataSource);
}
}
@@ -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<string, ISqlEditorModel<any>>;
constructor(private readonly serviceProvider: IServiceProvider) {
super();
this.models = new Map();
makeObservable<this, 'models'>(this, {
models: observable.shallow,
});
}
getOrCreate<TDataSource extends QueryDataSource>(state: ISqlEditorTabState): ISqlEditorModel<TDataSource> {
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<void> {
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<void> {
await this.destroy(editorId);
}
async unload(editorId: string): Promise<void> {
await this.destroy(editorId);
}
protected override async dispose(): Promise<void> {
for (const model of this.models.values()) {
await model[Symbol.asyncDispose]?.().catch(e => {
console.error(e);
});
}
this.models.clear();
}
}
@@ -33,13 +33,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';
@@ -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';
export default 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 @@ export default ModuleRegistry.add({
.addSingleton(SqlDialectInfoService)
.addSingleton(SqlDataSourceService)
.addSingleton(LocalStorageSqlDataSourceBootstrap)
.addSingleton(SqlEditorModelService)
.addSingleton(MenuBootstrap);
},
});
@@ -13,6 +13,9 @@
{
"path": "../../common-typescript/@dbeaver/cli"
},
{
"path": "../../common-typescript/@dbeaver/js-helpers"
},
{
"path": "../core-authentication"
},
@@ -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<DialogComponentProps<Payload>>(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<DialogComponentProps<Payload>>(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<DialogComponentProps<Payload>>(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<DialogComponentProps<Payload>>(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 (
<CommonDialogWrapper size="large">
<CommonDialogHeader title="app_shared_sql_generators_dialog_title" icon="sql-script" onReject={rejectDialog} />
+37 -2
View File
@@ -4012,6 +4012,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"
@@ -4629,13 +4630,16 @@ __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:
"@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"
@@ -14635,6 +14639,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"
@@ -15213,6 +15224,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"
@@ -15265,6 +15283,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"
@@ -15820,6 +15848,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"
@@ -18078,7 +18113,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